缓存策略
指导 Agent 按层级(浏览器、CDN、应用、分布式缓存)给出键空间、TTL 与主动/被动失效组合,并显式标注一致性代价;本页含读路径总览与键名草稿工具。
先识别数据新鲜度:强一致路径避免跨层缓存或缩短 TTL 并配合版本号;最终一致可接受 stale-while-revalidate、异步刷新与单飞回填。
SKILL 须写出「失效触发点」:写库成功后删哪些键、是否延迟双删、消息乱序或延迟时何时强制读主库或拒绝陈旧版本。
- 键设计包含租户、资源类型、语言/区域、schema 版本等维度,避免隐式全局键。
- 热点键考虑分片、本地二级缓冲与限流;与限流、熔断策略在文档中交叉引用。
一页摘要
- 分层:浏览器 → CDN → 应用内存 / 进程 → Redis 等 → DB;每层声明是否允许缓存与最大陈旧度。
- 键:可预测的命名空间 + 版本段;写放大与键数量上限在 SKILL 中写明。
- 失效:写路径维护删除列表或版本 bump;必要时延迟双删与兜底读主。
- 风险:穿透(空值/布隆)、击穿(互斥/单飞)、雪崩(TTL 抖动 + 预热 + 降级)。
- 度量:命中率、回源率、延迟分位与「缓存未命中风暴」告警。
# Cache-Aside 模式(Python + Redis)
import json, time, random
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
def get_product(product_id: int) -> dict:
key = f"v2:product:{product_id}"
cached = r.get(key)
if cached:
return json.loads(cached) # Cache hit
product = db_get_product(product_id) # Cache miss → 回源 DB
if product:
# TTL 基础 300s + 随机抖动 ±30s,防雪崩
ttl = 300 + random.randint(-30, 30)
r.setex(key, ttl, json.dumps(product))
else:
# 空值缓存防穿透,TTL 短(30s)
r.setex(key, 30, json.dumps(None))
return product
# Redis 5 种数据结构及典型缓存用途
# String: JSON 序列化对象(单实体缓存)r.setex("user:1", 300, json_str)
# Hash: 字段级更新(计数器、配置) r.hset("session:abc", "uid", 42)
# List: 最近 N 条记录、消息队列 r.lpush("recent:views", prod_id); r.ltrim("recent:views", 0, 99)
# Set: 去重标签、黑名单 r.sadd("blacklist:ip", "1.2.3.4")
# Sorted Set: 排行榜、带分数的优先级队列 r.zadd("leaderboard", {uid: score})
分层读路径(skill-flow-block)
[ 请求进入:携带 Accept-Language / 租户上下文 ]
│
┌───────────┴───────────┐
▼ ▼
[ 浏览器私有缓存 ] [ CDN / 边缘 ]
Cache-Control 私有策略 SWR、stale-if-error(若适用)
│ │
└───────────┬───────────┘
▼
[ 应用内:进程 LRU / 本地 map ]
注意:多实例一致性,宜短 TTL 或事件失效
│
▼
[ 分布式:Redis / Memcached 等 ]
单飞回填、管道批量、序列化格式约定
│
┌────────┴────────┐
▼ ▼
[ 命中:返回 + 逐层回填(按需)] [ 未命中:回源 DB / 服务 ]
│ │
└────────┬────────┘
▼
[ 可选:异步写回各层,记录陈旧度元数据 ]
Agent 输出应标明「哪一层可省略」:例如强一致账户余额通常不进 CDN;只读热点列表可 CDN 长 TTL + 应用短 TTL 组合。
各层职责与缓存头
浏览器 / CDN
Cache-Control:private/public、max-age、s-maxage。ETag/Last-Modified与条件请求减少带宽。- 需登录或含 PII 的响应默认
private,避免共享缓存泄漏。
应用 / 分布式
- 序列化:JSON、MessagePack、压缩;大 value 分块或旁路对象存储。
- 连接与超时:避免缓存层拖垮主链路;熔断后行为写清。
- 多租户:键前缀隔离,防止 SCAN 式调试在生产误用。
键空间、TTL 与抖动
推荐形态示例:{tenant}:{schemaVer}:{entity}:{id} 或 {svc}:{cacheVer}:{自然键哈希};在 SKILL 中禁止依赖「隐式默认租户」。
- TTL:按业务 SLA 分档;与下游刷新任务频率对齐,避免永久陈旧。
- 抖动:在基准 TTL 上增加随机偏移(如 ±10%),降低同一时刻集体过期。
- 负缓存:对合法但不存在的结果使用较短 TTL 的占位值,减轻穿透压力(注意与权限边界)。
写路径与失效清单
写库成功后同步或异步删除相关键;若采用「先删再写」,需评估并发读导致的长窗口不一致,必要时配合版本号或延迟双删。
- 维护「实体 → 影响键集合」表或代码生成清单,避免遗漏关联列表缓存。
- 消息总线延迟时:定义超时后读主、或返回带
X-Cache-Stale元信息的显式陈旧响应(与产品约定一致)。 - 批量导入/迁移:提供整命名空间 flush 或版本 bump 策略,并评估对在线流量的冲击。
穿透 · 击穿 · 雪崩
- 穿透:恶意或稀疏键导致反复回源;空值缓存、布隆过滤器、接口层校验与速率限制组合使用。
- 击穿:单热点键过期瞬间并发打穿;互斥锁、单飞(singleflight)、永不过期 + 异步刷新。
- 雪崩:大量键同时过期或缓存集群故障;TTL 抖动、预热、分级降级与队列削峰。
# 缓存击穿防御:互斥锁(Redis SETNX 实现)
import threading, time, json
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
_local_locks: dict[str, threading.Lock] = {}
def get_with_mutex(key: str, loader, ttl: int = 300):
"""单飞模式:同一进程内同一 key 只有一个 goroutine 回源"""
cached = r.get(key)
if cached is not None:
return json.loads(cached)
# 获取或创建本地锁(同进程内去重)
lock = _local_locks.setdefault(key, threading.Lock())
with lock:
# Double check:拿到锁后再查一次
cached = r.get(key)
if cached is not None:
return json.loads(cached)
value = loader() # 只有一个线程执行回源
ttl_jitter = ttl + random.randint(-int(ttl * 0.1), int(ttl * 0.1))
r.setex(key, ttl_jitter, json.dumps(value))
return value
# 缓存穿透防御:Redis Bloom Filter(redis-py-bloom 或 RedisBloom 模块)
# 推荐使用 Redis Stack 的 BF.ADD / BF.EXISTS 命令
def bloom_get_user(user_id: int) -> dict | None:
# 1. 先查布隆过滤器(误判率 0.1%,无漏判)
exists = r.execute_command("BF.EXISTS", "users:bloom", user_id)
if not exists:
return None # 确定不存在,直接返回,不查 DB
# 2. 存在(可能误判)→ 走正常缓存逻辑
key = f"user:{user_id}"
cached = r.get(key)
if cached:
return json.loads(cached)
user = db_get_user(user_id)
if user:
r.setex(key, 300, json.dumps(user))
return user
# 新增用户时同步写入布隆过滤器
def create_user(user_id: int, data: dict):
db_insert_user(user_id, data)
r.execute_command("BF.ADD", "users:bloom", user_id)
# 分布式缓存一致性:先更新 DB 再删缓存(推荐模式)vs 先删缓存再更新 DB
# 模式 A:先更新 DB,再删缓存(Cache-Aside + 延迟双删)
def update_product_a(product_id: int, data: dict):
db_update_product(product_id, data) # 1. 先更新 DB
cache_key = f"v2:product:{product_id}"
r.delete(cache_key) # 2. 删缓存(主删)
# 延迟双删:500ms 后再删一次,防止并发读写期间旧值回填
time.sleep(0.5)
r.delete(cache_key)
# 模式 B:先删缓存再更新 DB(不推荐)
# 问题:删除后到更新完成期间,并发读请求回填旧值到缓存 → 脏数据
# 取舍:
# - 模式 A:极短窗口(DB 更新到删除之间)可能读到旧缓存,但一般可接受
# - 模式 A 延迟双删:防止 DB 主从延迟期间副本读写旧值回填
# - 更强保证:消息队列异步删除,或 Binlog CDC 驱动失效(最终一致)
一致性与降级
在 SKILL 中为每条 API 标注期望一致性:线性一致读、因果一致、或最终一致可接受的最大延迟上界。
- 跨层缓存时,下层失效必须能驱动上层可见更新,或通过极短 TTL 吸收差异。
- 降级:缓存不可用时直接回源并可能触发更严限流;禁止无限重试压垮 DB。
Agent 遇到「金融级强一致」与「首页大流量」并存时,应拆路径:关键写读走主库短链路,展示类走可陈旧缓存。
可观测与告警
- 指标:命中率、未命中 QPS、回源率、缓存操作延迟(p95/p99)、内存/连接数。
- 追踪:在 trace 上标注 cache hit/miss、键前缀(脱敏),便于定位错误失效或热点。
- 告警:命中率骤降、单键 QPS 异常、回源超时激增与「缓存层错误率」联动 on-call 手册。
键与 TTL 草稿工具
填写下方字段,生成分层缓存键草稿与新鲜度档位的 TTL 建议区间(示意,落地前须与团队约定对齐)。抖动按均匀分布计算示例区间。
键段经安全化:仅保留字母数字与 _ - .;空段以 unknown 代替。实际环境请再叠加 region、locale 等维度。
---
name: caching-strategy
description: 为读写场景设计分层缓存、失效规则与防护模式
---
# 分层策略
浏览器: Cache-Control private/public + max-age;含 PII 的响应强制 private
CDN: public + s-maxage + stale-while-revalidate;需登录内容不走 CDN
应用进程: 短 TTL LRU(如 60s),多实例一致性靠 TTL 自然过期
Redis: Cache-Aside 模式;TTL=基准+随机抖动(±10%);空值缓存 30s 防穿透
# Redis 数据结构选择
String: 单对象 JSON 序列化(用户信息、商品详情)
Hash: 会话字段、配置(支持单字段 HSET 更新)
List: 最近 N 条记录(LPUSH + LTRIM 维护固定长度)
Set: 标签去重、黑名单(O(1) SISMEMBER)
Sorted Set: 排行榜、带 TTL 优先级队列(ZADD + ZRANGEBYSCORE)
# 防护模式(代码级)
击穿: threading.Lock + Double Check → 同进程内只有一个线程回源
穿透: BF.EXISTS(Redis Bloom Filter)→ 确定不存在直接返回
雪崩: TTL = base + random.randint(-base*0.1, base*0.1)
# 写路径一致性
推荐: 先更新 DB → 删缓存 → 延迟 500ms 再删(防主从延迟回填)
强一致: 消息队列 / Binlog CDC 驱动异步删除 + 最终一致兜底
禁止: 两个服务各自写同一缓存 key 而无协调机制