缓存

Frappe 框架内置的电池之一就是使用 Redis 进行缓存。Redis 是一种快速、简单易用的内存键值存储。将 Redis 与 Frappe 框架结合使用,可以加速重复的长耗时计算,或者避免对不常变化的数据进行数据库查询。

Redis 由 bench 作为独立服务启动,每个 Frappe Web/后台工作进程都可以使用 frappe.cache 连接到它。

注意:在旧版本的 Frappe 中,你可能需要使用 frappe.cache() 而不是 frappe.cache 来访问 Redis 连接。

Redis 数据类型与用法

Redis 支持多种数据类型,但这里我们只介绍几种最常用的数据类型:

  1. 字符串(Strings) 是 Redis 中最流行、最常用的数据类型。它们以键值对的形式存储。
In [1]: frappe.cache.set_value("key", "value")

In [2]: frappe.cache.get_value("key")
Out[2]: 'value'
  1. 哈希(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

缓存失效

建议采用两种策略来避免缓存过期问题:

  1. 在设置缓存值时,设置较短的 TTL(生存时间)。
# This cached value will automatically expire in one hour
frappe.cache.set_value(key, result, expires_in_sec=60*60)
  1. 当缓存值被修改时,手动清除缓存。
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 中的缓存值失效。

  1. 设置适当的过期时间(以秒为单位的 TTL),以便缓存值在一段时间后自动失效。例如:@redis_cache(ttl=60) 将导致缓存值在 60 秒后过期。
  2. 手动清除缓存。这通过调用函数的 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 模块中找到此代码。