Cache Aside 是什么
旁路缓存是最主流的缓存用法:读时先查缓存,未命中再查数据库并回填;写时先写库、再删除缓存。之所以删缓存而不是更新缓存,是因为并发写入时「更新库」的顺序与「更新缓存」的顺序可能交错,删除能让下次读自然加载到最新值。它提供的是最终一致性,TTL 是兜底。
基本信息
| 读路径 | 缓存优先,未命中查库并回填,通常带 TTL |
|---|---|
| 写路径 | 先写数据库,再删除缓存键 |
| 一致性 | 最终一致,靠 TTL 收敛 |
| TTL 建议 | 按数据变化频率设 5 分钟到数小时 |
| 失效兜底 | 删缓存失败要有重试或消息补偿 |
典型使用场景
- 商品详情、用户资料等读多写少数据:命中率通常能到 90% 以上
- 热点列表首页数据:用 TTL 控制刷新频率,扛住突发流量
- 配置类低频变更数据:变更时主动删键,平时全走缓存
- 作为多级缓存的一级:本地缓存之上再加 Redis 共享层
- 写扩散场景的反例判断:数据写多读少时缓存收益有限,先算命中率
- 跨服务共享读:多个服务读同一份数据,用统一 key 规则避免重复回源
读写伪代码
读写伪代码
# 读:缓存优先
def get_user(uid):
v = redis.get('user:%s' % uid) # 1) 先查缓存
if v is None: # 2) 未命中(不是空值)
row = db.query(uid) # 3) 回源查库
redis.set('user:%s' % uid, json.dumps(row), ex=300) # 4) 回填并设 TTL
return row
return json.loads(v)
# 写:先写库,再删缓存
def update_user(uid, data):
db.update(uid, data) # 1) 先落库
def _del(uid):
redis.delete('user:%s' % uid) # 2) 再删缓存,不是更新缓存
time.sleep(0.5)
redis.delete('user:%s' % uid) # 3) 延迟双删,压低回填竞态影响写策略对照
| 写策略 | 风险 | 结论 |
|---|---|---|
| 先写库再删缓存 | 删缓存失败会残留旧值直到 TTL 到期 | 主流做法,需保证删除有重试 |
| 先删缓存再写库 | 删除后回源读到旧库值,脏数据被回填 | 并发下明显更脏,不推荐 |
| 直接更新缓存 | 并发写顺序交错,缓存值可能永久错 | 除计数器等特殊场景外不推荐 |
| 缓存加过期时间 | 过期瞬间并发回源压力上升 | 必须设置,是最终一致性的底线 |
为什么删缓存而不是更新缓存:并发写时先写库 A 后写库 B 与缓存的更新顺序可能交错,删除让下次读自然加载最新值。
删除缓存失败要有补偿:可写进消息队列重试,或依赖 TTL 收敛。对强一致要求的数据(如账户余额)不要走旁路缓存,直接在数据库上做。