缓存一致性怎么选
缓存与数据库之间天然存在不一致窗口。主流做法是 Cache Aside:读时先查缓存,未命中则查库并回填;写时先更新数据库,再删除缓存。删除而不是更新缓存,是为了避免两个并发写的覆盖顺序错乱。要更强的一致性,就要付出延迟双删、订阅 binlog 或直读主库的代价。
基本信息
| 主流方案 | Cache Aside:写库后删除缓存 |
|---|---|
| 失效方式 | 删除优先于更新,避免并发写覆盖顺序错乱 |
| 更强一致 | 延迟双删、订阅 binlog 删除、读主库 |
| 失败补偿 | 删除失败要有重试队列或 binlog 兜底 |
| 设计前提 | 先定义业务能容忍多久的旧数据 |
典型使用场景
- 商品详情、用户资料这类读多写少的缓存场景
- 活动配置变更后要求秒级生效,需要主动失效
- 余额、库存等强一致数据,评估是否应该绕过缓存
- 删除缓存失败后的补偿设计,避免脏数据留到 TTL 结束
- 排查「明明改了库,页面还是旧值」的缓存链路问题
读写范式
读写范式
def read(uid):
v = redis.get(f'user:{uid}') # 1) 先查缓存
if v is None:
v = db.query(uid) # 2) 未命中查库
redis.set(f'user:{uid}', v, ex=300) # 3) 回填并设 TTL
return v
def update(uid, data):
db.update(uid, data) # 1) 先写库
redis.delete(f'user:{uid}') # 2) 再删缓存,失败要重试方案对比
| 方案 | 一致性强度 | 成本与风险 |
|---|---|---|
| 先写库再删缓存 | 秒级不一致可接受 | 低,主流选择 |
| 延迟双删 | 把窗口压得更小 | 中,依赖 sleep,不够优雅 |
| 订阅 binlog 删除 | 接近最终一致 | 高,需要 CDC 链路与运维投入 |
| 强一致读主库 | 一致 | 压力回到数据库,缓存价值下降 |
| 双写库与缓存 | 看似一致 | 风险最高,并发写会长期留下脏数据 |
讨论一致性之前先定义「业务能容忍多久的旧数据」:多数场景几秒完全可以接受,此时任何复杂方案都是过度设计。
缓存与主从复制叠加时会读到更旧的数据:写完主库后从库还没同步,读请求打到从库并把旧值回填进缓存,脏数据就被固化到 TTL 到期为止。
删除缓存必须有补偿:删除失败要进重试队列,或用 binlog 订阅兜底,否则脏数据会一直留到过期时间才消失,期间用户看到的一直是旧值。