关于 frappe-core-cache
面向 Frappe 开发者的缓存与锁定参考技能,汇总 Redis、请求级及文档缓存的 API、失效方式和分布式锁用法。
主要功能
- 01缓存键值与哈希操作
- 02TTL 和生成器读取
- 03函数及文档缓存
- 04请求级缓存用法
- 05Redis 分布式锁
- 06缓存失效指南
适用场景
- 01缓存重复调用的函数结果
- 02在单个请求中复用数据
- 03控制并发执行关键任务
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-core-cache安装后在 Claude Code 中输入/frappe-core-cache确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/.gitkeepanti-patterns.mdapi-reference.mdexamples.md详细介绍
Frappe 缓存与锁
快速参考
| 操作 | 方法 | 说明 |
|---|---|---|
| 设置值 | frappe.cache.set_value(key, val) | 可选设置 TTL |
| 获取值 | frappe.cache.get_value(key) | 缺失时返回 None |
| 获取或生成 | frappe.cache.get_value(key, generator=fn) | 缓存未命中时调用 fn() |
| 删除值 | frappe.cache.delete_value(key) | 单个键或键列表 |
| 按模式删除 | frappe.cache.delete_keys(pattern) | 使用通配符 * 匹配 |
| 哈希设置 | frappe.cache.hset(name, key, val) | Redis 哈希字段 |
| 哈希获取 | frappe.cache.hget(name, key) | 单个哈希字段 |
| 获取所有哈希值 | frappe.cache.hgetall(name) | 完整哈希,返回 dict |
| 删除哈希字段 | frappe.cache.hdel(name, key) | 移除哈希字段 |
| 检查哈希字段是否存在 | frappe.cache.hexists(name, key) | 返回 bool |
| 缓存文档 | frappe.get_cached_doc(dt, dn) | 从缓存获取完整文档 |
| 清除文档缓存 | frappe.clear_document_cache(dt, dn) | 使缓存的文档失效 |
| 缓存装饰器 | @redis_cache | 自动缓存函数结果 |
| 请求缓存 | frappe.local.cache | 每个请求对应的 dict(非 Redis) |
决策树
What caching pattern do you need?
│
├─ Cache a function result automatically?
│ ├─ Pure function (same args → same result) → @redis_cache
│ └─ Need custom key/TTL → manual get_value/set_value
│
├─ Cache a document?
│ ├─ Read-only access → frappe.get_cached_doc()
│ └─ Need to invalidate → frappe.clear_document_cache()
│
├─ Cache structured data (multiple fields)?
│ └─ Redis hash → hset/hget/hgetall
│
├─ Per-request cache (avoid repeated DB calls in one request)?
│ └─ frappe.local.cache dict
│
├─ Prevent concurrent execution?
│ └─ Distributed lock → frappe.lock("resource_name")
│
└─ Invalidate cache?
├─ Single key → delete_value(key)
├─ Pattern → delete_keys("prefix*")
└─ All site cache → frappe.clear_cache()
字符串操作
设置与获取
# Set a value (persists until evicted or deleted)
frappe.cache.set_value("exchange_rate_USD", 1.08)
# Set with TTL (expires after N seconds)
frappe.cache.set_value("exchange_rate_USD", 1.08, expires_in_sec=3600)
# Get value (returns None if missing)
rate = frappe.cache.get_value("exchange_rate_USD")
# Get with generator (calls function on cache miss, stores result)
rate = frappe.cache.get_value(
"exchange_rate_USD",
generator=lambda: fetch_exchange_rate("USD"),
)
按用户限定的值
# Store per-user preference
frappe.cache.set_value("dashboard_layout", "compact", user="[email protected]")
# Retrieve for specific user
layout = frappe.cache.get_value("dashboard_layout", user="[email protected]")
删除
# Single key
frappe.cache.delete_value("exchange_rate_USD")
# Multiple keys
frappe.cache.delete_value(["exchange_rate_USD", "exchange_rate_EUR"])
# Pattern-based deletion (wildcard)
frappe.cache.delete_keys("exchange_rate*")
哈希操作
使用哈希将相关字段归在同一个键下。
# Set hash fields
frappe.cache.hset("config|notifications", "email_enabled", True)
frappe.cache.hset("config|notifications", "sms_enabled", False)
frappe.cache.hset("config|notifications", "max_retries", 3)
# Get single field
email_on = frappe.cache.hget("config|notifications", "email_enabled")
# Get all fields as dict
config = frappe.cache.hgetall("config|notifications")
# {"email_enabled": True, "sms_enabled": False, "max_retries": 3}
# Delete field
frappe.cache.hdel("config|notifications", "sms_enabled")
# Check existence
exists = frappe.cache.hexists("config|notifications", "email_enabled")
使用生成器的哈希
# hget with generator — calls function on miss
value = frappe.cache.hget(
"user|permissions",
"[email protected]",
generator=lambda: compute_permissions("[email protected]"),
)
@redis_cache 装饰器
根据参数自动缓存函数返回值。
from frappe.utils.caching import redis_cache
@redis_cache
def get_item_price(item_code, price_list):
"""Expensive query — cached automatically."""
return frappe.db.get_value("Item Price",
{"item_code": item_code, "price_list": price_list},
"price_list_rate",
)
# First call — hits database, stores in Redis
price = get_item_price("ITEM-001", "Standard Selling")
# Second call — returns from cache
price = get_item_price("ITEM-001", "Standard Selling")
# Clear all cached results for this function
get_item_price.clear_cache()
设置 TTL
@redis_cache(ttl=300) # expires after 5 minutes
def get_exchange_rate(from_currency, to_currency):
return fetch_rate_from_api(from_currency, to_currency)
@redis_cache 使用规则:
- 始终确保参数可哈希(字符串、数字、元组)。绝不要将 dict 或列表作为参数传入。
- 底层数据发生变化时,始终调用
.clear_cache()。 - 绝不要用于有副作用的函数——命中缓存时不会执行该函数。
frappe.local.cache:请求范围的缓存
frappe.local.cache 是一个普通的 Python dict,其生命周期为单个 HTTP 请求。它不会存储在 Redis 中。
def get_user_settings():
"""Avoid repeated DB calls within a single request."""
if "user_settings" not in frappe.local.cache:
frappe.local.cache["user_settings"] = frappe.get_doc(
"User Settings", frappe.session.user
)
return frappe.local.cache["user_settings"]
在以下情况使用 frappe.local.cache:
- 同一个请求中需要多次使用相同数据
- 数据无需在不同请求间持久化
- 希望避免任何 Redis 开销
文档缓存
# Get cached document (read-only, no permission check)
settings = frappe.get_cached_doc("System Settings")
item = frappe.get_cached_doc("Item", "ITEM-001")
# Invalidate when document changes
frappe.clear_document_cache("Item", "ITEM-001")
# Cached single value
val = frappe.db.get_value("Item", "ITEM-001", "item_name", cache=True)
绝不要修改 frappe.get_cached_doc() 返回的文档——它返回的是共享引用。修改会破坏缓存,影响之后的所有读取操作。
分布式锁
使用基于 Redis 的锁,防止关键代码段并发执行。
# Context manager (recommended)
with frappe.lock("process_payroll"):
# Only one worker executes this block at a time
process_all_salary_slips()
# Lock auto-released on exit
# Manual lock/unlock
frappe.lock("inventory_sync")
try:
sync_inventory()
finally:
frappe.unlock("inventory_sync") # ALWAYS unlock in finally
规则:
- 始终使用
with frappe.lock()(上下文管理器),确保锁得到释放。 - 持锁时间绝不要超过几秒——长时间持锁会导致工作进程饥饿。
- 始终使用描述性锁名称,避免冲突。
缓存失效模式
模式 1:基于 TTL(生存时间)
frappe.cache.set_value("dashboard_stats", compute_stats(), expires_in_sec=300)
适用于:允许略微过时的数据(汇率、仪表盘汇总数据)。
模式 2:基于事件的失效
# In hooks.py
doc_events = {
"Item Price": {
"on_update": "my_app.cache.invalidate_price_cache",
"on_trash": "my_app.cache.invalidate_price_cache",
}
}
# In my_app/cache.py
def invalidate_price_cache(doc, method):
frappe.cache.delete_keys("item_price*")
# Or clear specific function cache:
# get_item_price.clear_cache()
适用于:发生变化后必须立即保持最新的数据。
模式 3:混合模式(TTL + 事件)
@redis_cache(ttl=600)
def get_pricing_rules():
return frappe.get_all("Pricing Rule", fields=["*"])
# Event hook clears cache immediately on change
def on_pricing_rule_update(doc, method):
get_pricing_rules.clear_cache()
适用于:读取频繁、偶尔更新的数据。
常见缓存键(内部)
| 键模式 | 内容 |
|---|---|
doctype::meta::{dt} | DocType 元数据 |
user_permissions::{user} | 用户权限缓存 |
bootinfo::{user} | 用户引导信息 |
notifications::{user} | 通知计数 |
document_cache::{dt}::{dn} | 缓存的文档 |
绝不要直接写入内部缓存键。始终使用文档化的 API 方法(get_cached_doc、clear_document_cache 等)。
性能指南
- 务必为源自外部数据的缓存值设置 TTL——不设置 TTL,过时数据会一直保留,直到手动失效或被 Redis 淘汰。
- 切勿缓存大型对象(>1 MB)——Redis 使用 pickle 序列化,大型值会增加序列化开销和内存用量。
- 务必使用
frappe.local.cache保存单个请求中需要多次使用的数据——这样可以完全避免 Redis 往返开销。 - 切勿将
frappe.clear_cache()用作常规失效策略——它会清除该站点的所有缓存键,导致缓存冷启动带来的性能损失。 - 务必为自定义缓存键添加前缀,使用你的应用名称(例如,
myapp|exchange_rate),以避免与 Frappe 内部键冲突。
Redis 配置
默认配置:{bench}/config/redis_cache.conf
| 设置 | 默认值 | 说明 |
|---|---|---|
| 端口 | 13000 | Redis 缓存端口 |
| 绑定地址 | 127.0.0.1 | 监听地址 |
| maxmemory-policy | allkeys-lru | 淘汰策略 |
| maxmemory | 256mb | 最大内存(可调整) |
键命名空间
Frappe 会自动为所有缓存键添加站点名称前缀:
# You write:
frappe.cache.set_value("my_key", "value")
# Redis stores:
# "mysite.localhost|my_key"
frappe.cache.make_key(key, user, shared) 负责添加前缀。shared=True 参数会移除站点前缀,以便跨站点使用键(较少见的用例)。
版本差异
| 功能 | v14 | v15 | v16 |
|---|---|---|---|
frappe.cache.set_value | 可用 | 可用 | 可用 |
@redis_cache | 不可用 | 可用 | 可用 |
@redis_cache(ttl=) | 不可用 | 可用 | 可用 |
frappe.lock 上下文管理器 | 可用 | 可用 | 可用 |
frappe.local.cache | 可用 | 可用 | 可用 |
使用生成器的 hget | 可用 | 可用 | 可用 |
另请参阅
- references/examples.md — 缓存实现模式
- references/anti-patterns.md — 常见缓存错误
- references/api-reference.md — 完整的 API 签名
frappe-core-database— 可从缓存中受益的数据库查询frappe-core-permissions— 用户权限缓存
评论
支持 Markdown