先更新数据库还是先更新缓存:经典问题深度剖析
这个问题,几乎每个做交易系统的同学都会遇到。我记得刚带团队那会儿,有个同事拍着胸脯说「先更新缓存肯定快啊」,结果上线第一天就出了数据不一致的故障。嗯,今天咱们就把这个经典问题彻底聊透。
问题的本质是什么?
说白了,就是数据库和缓存之间,存在一个时间窗口。你更新了其中一个,另一个还没来得及更新,这时候如果有并发请求进来,读到的就是脏数据。
我习惯把这个问题拆成两个维度来看:
- 一致性要求:你的业务能容忍短暂的不一致吗?
- 并发场景:读写请求的冲突概率有多大?
交易订单的状态变更,恰恰是强一致性场景。你想想看,用户支付成功,结果页面还显示「待支付」,这谁能忍?
两种经典策略的对比
| 策略 | 操作顺序 | 风险点 | 适用场景 |
|---|---|---|---|
| 先更新数据库 | 更新DB → 删除/更新缓存 | 缓存删除失败导致脏数据 | 强一致性要求高 |
| 先更新缓存 | 更新缓存 → 更新DB | DB更新失败,缓存与DB永久不一致 | 弱一致性、可补偿 |
我个人习惯在交易系统里坚决选择「先更新数据库」。为什么?因为数据库是最终权威,缓存只是加速手段。数据库写成功了,哪怕缓存没更新,最多就是多查一次库,数据不会错。
核心原则:数据库是真理之源,缓存是加速副本。永远不要让缓存成为数据不一致的源头。
交易订单状态变更的案例分析
咱们拿一个真实的订单状态变更流程来说。假设订单从「已支付」变更为「已发货」:
// 错误示范:先更新缓存
redis.set("order:1001:status", "已发货");
db.update("UPDATE orders SET status='已发货' WHERE id=1001");
// 如果DB更新失败,缓存里永远显示「已发货」,实际订单还是「已支付」
我曾经在一个物流系统中见过这种写法,结果DB超时回滚了,缓存却一直显示已发货。用户查了三天物流没动静,投诉电话打爆了。
正确的做法应该是:
// 正确做法:先更新数据库
db.update("UPDATE orders SET status='已发货' WHERE id=1001");
redis.del("order:1001:status"); // 删除缓存,下次读取时回源DB
这里有个细节:删除缓存而不是更新缓存。为什么?因为删除操作是幂等的,而且避免了并发写缓存时的竞争条件。你想想看,如果两个线程同时更新缓存,后写的可能覆盖了前写的正确值,但删除就不存在这个问题。
避坑指南:缓存删除失败怎么办?
先更新数据库再删除缓存,听起来完美,但实际项目中我踩过一个坑——缓存删除操作可能失败。比如Redis宕机、网络闪断,这时候缓存里还是旧数据。
我的解决方案是引入延迟双删策略:
// 延迟双删
db.update("UPDATE orders SET status='已发货' WHERE id=1001");
redis.del("order:1001:status"); // 第一次删除
// 延迟一段时间后再次删除
Thread.sleep(500); // 根据业务调整延迟时间
redis.del("order:1001:status"); // 第二次删除
为什么要延迟?因为在这500毫秒内,可能有其他线程读取了旧数据并写回了缓存。第二次删除就是兜底,确保最终一致性。
注意:延迟时间不是随便设的。我一般建议设置为「业务读耗时 + 网络抖动余量」的2倍。比如读缓存平均5ms,网络抖动按100ms算,延迟设200-300ms就够了。太长了会影响用户体验。
更可靠的方案:订阅Binlog
如果你们团队有中间件能力,我强烈建议用订阅数据库Binlog的方式来同步缓存。这是目前交易系统里最成熟的方案。
流程是这样的:
- 业务代码只更新数据库
- 数据库的Binlog被Canal等组件捕获
- 异步消费Binlog,更新或删除缓存
这样做的好处是:业务代码完全不用关心缓存,缓存一致性由中间件层保证。我在上一家公司主导的订单中心,就是用这个方案扛住了日均千万级的订单状态变更。
个人经验:Binlog方案虽然可靠,但引入了消息队列的延迟。对于秒级一致性的场景完全够用,但如果你的业务要求毫秒级强一致,那还是得用分布式事务或者TCC方案。
核心流程图
下面这张图是我自己总结的决策流程,每次做技术方案评审时我都会拿出来对照:
总结一下我的建议
交易系统的缓存一致性,没有银弹。但有几个原则可以帮你少走弯路:
- 优先保证数据库正确,缓存可以异步补偿
- 删除缓存优于更新缓存,幂等且安全
- 引入延迟双删或Binlog,应对删除失败的极端情况
- 不要过度设计,如果业务能接受秒级不一致,简单方案就够了
我曾经在一个高并发抢购系统中,为了追求「绝对一致」引入了分布式锁,结果锁竞争导致系统吞吐量直接腰斩。后来改成先更新DB再异步删除缓存,配合Binlog兜底,既保证了最终一致,又把性能提了上来。
嗯,说到底,架构设计就是取舍的艺术。希望今天的内容能帮你少踩几个坑。
公众号:蓝海数据掘金营,微信deep3321