“先更新数据库还是先删缓存”这个问题,面试常问,实际项目里也真的会出错。这篇把几种方案摊开说,重点是各自的失败场景。
方案一:先更新 DB,再删缓存
最常用的 Cache Aside。
def update(id, data):
db.update(id, data)
redis.delete(f"item:{id}")
失败场景:DB 更新成功,删缓存失败(网络抖动、Redis 挂了),缓存里是旧数据,会一直旧到过期。
缓解:删除失败重试(扔消息队列),或者给缓存设一个不太长的 TTL 兜底。
方案二:先删缓存,再更新 DB
并发下更容易出问题:
1. A 删缓存
2. B 读缓存未命中,读 DB 得到旧值,写回缓存
3. A 更新 DB 为新值
结果:缓存里是旧值,DB 是新值
概率不高但会发生,尤其是读请求量大的时候。除非加延迟双删,但延迟多久不好定。
方案三:延迟双删
redis.delete(key)
db.update(id, data)
time.sleep(0.5)
redis.delete(key)
思路是第二次删除把并发期间写回的旧值清掉。问题是 sleep 阻塞线程,放异步线程里又不好控制时序;0.5 秒是拍的,读请求慢一点就失效。
能解决一部分问题,但不能保证。
方案四:订阅 binlog
用 Canal / Debezium 解析 MySQL binlog,异步删缓存。优点是业务代码零侵入,不管谁改了数据库都能触发;缺点是链路长了,多了组件要维护,延迟从毫秒到秒级。
中大型项目常见做法,小项目上这个属于过度设计。
根本问题:这是最终一致,不是强一致
上面所有方案都是最终一致。要强一致,只能:
- 读写都走数据库(缓存只做只读、可过期的场景,比如文章列表)
- 用分布式锁把读写串行化(性能代价大)
- 把“缓存”做成数据库的一部分(比如 Redis 作为主存储 + AOF 持久化,但这又引入新的问题)
我的实际做法
按数据类型分开:
- 能容忍几秒不一致:文章列表、统计数据 → Cache Aside + 短 TTL + 删除失败进重试队列
- 不能容忍:余额、库存、订单状态 → 不缓存,或只缓存不可变的快照
- 热点但少变:配置、分类 → 本地缓存 + 定时刷新 + 手动刷新入口
最忌讳的是“所有数据都往 Redis 里塞一层”,然后花大量精力解决一致性问题。先问一句:这个数据真的需要缓存吗?多数时候答案是不需要。
- THE END -
最后修改:2023年11月11日
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://mi-blog.cn/archives/677
共有 0 条评论