Frappe 框架内置的电池之一就是使用 Redis 进行缓存。Redis 是一种快速、简单易用的内存键值存储。将 Redis 与 Frappe 框架结合使用,可以加速重复的长耗时计算,或者避免对不常变化的数据进行数据库查询。
Redis 由 bench 作为独立服务启动,每个 Frappe Web/后台工作进程都可以使用 frappe.cache 连接到它。
注意:在旧版本的 Frappe 中,你可能需要使用 frappe.cache() 而不是 frappe.cache 来访问 Redis 连接。
Redis 数据类型与用法
Redis 支持多种数据类型,但这里我们只介绍几种最常用的数据类型:
- 字符串(Strings) 是 Redis 中最流行、最常用的数据类型。它们以键值对的形式存储。
In [1]: frappe.cache.set_value("key", "value")
In [2]: frappe.cache.get_value("key")
Out[2]: 'value'
- 哈希(Hashes) 用于表示复杂的对象,其字段可以单独更新,而无需发送整个对象。你可以将哈希想象成存储在 Redis 上的字典。
# Get fields separately
In [1]: frappe.cache.hset("user|admin", "name", "Admin")
In [2]: frappe.cache.hset("user|admin", "email", "[email protected]")
# Get single field value
In [3]: frappe.cache.hget("user|admin", "name")
Out[3]: 'Admin'
# Or retrieve all fields at once.
In [4]: frappe.cache.hgetall("user|admin")
Out[4]: {'name': 'Admin', 'email': '[email protected]'}
缓存文档
Frappe 有一个内置函数,用于从缓存而非数据库中获取文档。
从缓存中获取文档通常更快,因此当文档不经常变化时,你应该使用它们。一个常见的用途是获取用户配置的设置。
system_settings = frappe.get_cached_doc("System Settings")
缓存文档会通过“尽力而为”的缓存失效策略自动清除。
每当 Frappe 的 ORM 使用 doc.save 或 frappe.db.set_value 遇到更改时,它都会清除相关文档的缓存。但是,如果对数据库执行了原始查询,则无法实现此操作。
注意:可以使用 frappe.clear_document_cache(doctype, name) 手动进行缓存失效。
实现自定义缓存
当你处理一个耗时很长的昂贵计算,且对于相同的输入,其结果是确定性的,那么缓存输出可能是有意义的。
让我们尝试在这个运行缓慢的示例函数中实现一个自定义缓存。
def slow_add(a, b):
import time; time.sleep(1) # Fake workload
return a + b
实现自定义缓存最重要的部分是生成一个唯一的键。在此示例中,缓存值的结果依赖于两个输入变量,因此它们应该是键的一部分。
def slow_add(a, b):
key = f"slow_addition|{a}+{b}" # unique key representing this computation
# If this key exists in cache, then return value
if cached_value := frappe.cache.get_value(key):
return cached_value
import time; time.sleep(1) # Fake workload
result = a + b
# Set the computed value in cache so next time we dont have to do the work
frappe.cache.set_value(key, result)
return result
缓存失效
建议采用两种策略来避免缓存过期问题:
- 在设置缓存值时,设置较短的 TTL(生存时间)。
# This cached value will automatically expire in one hour
frappe.cache.set_value(key, result, expires_in_sec=60*60)
- 当缓存值被修改时,手动清除缓存。
frappe.cache.delete_value(key) # `frappe.cache.hdel` if using hashes.
@redis_cache 装饰器
Frappe 提供了一个装饰器,用于自动缓存函数调用的结果。
你可以使用它快速在任何可能缓慢的现有函数之上实现缓存。
In [1]: def slow_function(a, b):
...: import time; time.sleep(1) # fake expensive computation
...: return a + b
...:
In [2]: # This takes 1 second to execute every time.
...: %time slow_function(40, 2)
...:
Wall time: 1 s
Out[2]: 42
In [3]: %time slow_function(40, 2)
Wall time: 1 s
Out[3]: 42
In [4]: from frappe.utils.caching import redis_cache
...:
In [5]: @redis_cache
...: def slow_function(a, b):
...: import time; time.sleep(1) # fake expensive computation
...: return a + b
...:
In [6]: # Now first call takes 1 second, but all subsequent calls return instantly.
...: %time slow_function(40, 2)
...:
Wall time: 1 s
Out[6]: 42
In [7]: %time slow_function(40, 2)
...:
Wall time: 897 µs
Out[7]: 42
缓存失效
有两种方法可以使 @redis_cache 中的缓存值失效。
- 设置适当的过期时间(以秒为单位的 TTL),以便缓存值在一段时间后自动失效。例如:
@redis_cache(ttl=60)将导致缓存值在 60 秒后过期。 - 手动清除缓存。这通过调用函数的
clear_cache方法来完成。
from frappe.utils.caching import redis_cache
@redis_cache
def slow_function(...):
...
def invalidate_cache():
slow_function.clear_cache()
Frappe 的 Redis 设置
Bench 默认会设置 Redis。你可以在 {bench}/config/ 目录中找到 Redis 配置。
Bench 还会配置 Procfile 和 supervisor 配置文件,以便在 bench 启动时启动 Redis 服务器。
redis_cache: redis-server config/redis_cache.conf
示例配置如下所示:
dbfilename redis_cache.rdb
dir /home/user/benches/develop/config/pids
pidfile /home/user/benches/develop/config/pids/redis_cache.pid
bind 127.0.0.1
port 13000
maxmemory 737mb
maxmemory-policy allkeys-lru
appendonly no
save ""
你可以修改此配置中的 maxmemory 来增加分配给缓存的最大内存。我们建议不要修改其他任何内容。
请参阅官方配置文档以了解更多信息:
实现细节
多租户
frappe.cache 在内部会按站点上下文为键添加前缀。因此,在同一 bench 上的两个不同站点调用 frappe.cache.set_value("key") 将为每个站点创建两个独立的条目。
要查看实现细节,请参阅 frappe.cache.make_key 函数。
复杂类型
Frappe 使用 pickle 模块将文档等复杂对象序列化为字节。因此,使用 frappe.cache 时,你无需担心值的序列化/反序列化问题。
在此处阅读更多关于 pickle 的信息:
客户端缓存
Frappe 在 frappe.local.cache 内部的 Redis 缓存之上实现了客户端缓存,以避免对 Redis 的重复调用。
在同一请求/任务中对 Redis 的任何重复调用都将从客户端缓存返回数据,而不是再次调用 Redis。
RedisWrapper
所有与 Frappe 相关的更改都是通过包装默认的 Redis 客户端并扩展其方法来实现的。你可以在 frappe.utils.redis_wrapper 模块中找到此代码。