性保障Redis缓存和数据库数据同步一致性的那些折腾和实操经验分享
- 问答
- 2026-01-21 13:49:11
- 3
(引用来源:某电商平台后端开发团队的技术复盘文档)
我们团队当时遇到的情况是这样的,就是一个商品,比如一款新手机,它的价格在后台管理页面被运营人员修改了,理论上,这个新价格应该立刻更新到数据库,同时把Redis里缓存的老价格给删掉或者更新掉,这样用户下次来查询的时候,因为缓存里没了,就会去查数据库拿到新价格,再塞回缓存,一切就完美了。
但问题就出在这个“上,现实世界里,根本没有真正的“,我们最开始用的是一种特别想当然的方法,叫“先更新数据库,再删除缓存”,这听起来很合理对吧?数据库都改成功了,我再去把缓存里的脏数据删掉,下次查询自然就同步了,但我们就真碰上鬼了。
有一次大促,一个热门商品降价,运营点了保存,我们的程序逻辑是:第一步,事务提交,数据库里商品价格成功从2999改成了2699,就在这个事务刚提交成功、程序即将执行第二步“删除Redis缓存”这个指令之前的那个瞬间,真的是电光火石的一刹那,一个用户来查询这个商品了,因为缓存是独立于数据库的另一个服务,网络请求需要时间,所以这个查询请求发现Redis里还有缓存(此时缓存里还是老价格2999),于是就高高兴兴地把这个2999的老价格返回给用户了,这还没完,更巧的是,这个查询请求在拿到老价格数据后,它还有个逻辑叫“缓存回种”,意思是如果查询数据库得到数据,就要把它设置到Redis里以防下次再查库,这个查询请求顺手就把刚刚读到的老价格2999,又给写回到Redis里去了!紧接着,我们程序第二步的“删除缓存”指令才慢悠悠地到达Redis,但为时已晚,此时Redis里已经被那个查询请求用老数据给“污染”了。
结果就是,数据库里明明是2699的新价格,但Redis里鬼使神差地还是2999的老价格,后续所有用户来查询,全都看到的是2999,那个降价跟没降一样,直到这个缓存因为过期时间到了自动失效,我们被运营骂惨了,说技术bug导致促销活动失败。

这次教训让我们明白,就那么简单两步操作,因为顺序和时机没把握好,在超高并发的场景下就会出大问题,这也就是网上常说的“缓存双删”策略要解决的经典问题,所谓“双删”,就是在更新数据库的前后,都执行一次缓存删除操作。
(引用来源:团队内部针对上述问题的解决方案讨论会记录)
我们当时讨论了好几个方案,有人提出用“先删除缓存,再更新数据库”,这个能避免我们上面那种情况吗?我们仔细一想,也不行,假设用户A要更新数据,他先删了缓存,这时用户B来查询,发现缓存没了,就去查数据库,拿到的是老数据,然后B把老数据回种到缓存,之后用户A才更新完数据库,结果缓存里还是老数据,同样不一致。

所以后来我们借鉴了别人的经验,决定上“延迟双删”策略,具体操作是:
- 先删除一次Redis缓存。
- 再更新数据库。
- 等一小段时间(比如500毫秒到1秒),再删除一次Redis缓存。
这第三次删除,就是为了干掉在第二步更新数据库期间,可能被其他查询请求塞回缓存的老数据,这个延迟时间很难精确设定,我们是通过压测,估算出业务上“读数据库+回种缓存”这个操作的平均耗时,然后稍微加个余量,这个办法不是百分百严格,但能解决绝大部分情况,属于一种实践出来的妥协和平衡。
(引用来源:团队在引入消息队列后的运维总结)
“延迟双删”里的第二次删除,这个“延迟”动作本身也是个技术点,我们不可能在更新数据库的那个请求里真的用Thread.sleep去傻等,那会拖慢接口响应,所以我们引入了消息队列(我们用的是RocketMQ),流程变成了:更新数据库的事务成功提交后,立刻发一条消息到消息队列,消息内容就是“请延迟删除某个key”,然后有一个独立的消费者服务来消费这个消息,它收到消息后,会等待预设好的延迟时间,然后再去执行删除缓存的操作,这样就把解耦做得更彻底了,写请求的主流程不会阻塞。
这套东西搞下来,代码逻辑复杂了不少,还要多维护一个消息队列的消费者服务,我们也会在关键数据更新后,让前端用户手动刷新一下页面,强制触发一次新的数据库查询,作为一种补偿手段,保证缓存和数据库一致,没有一劳永逸的银弹,都是在具体业务场景下,根据对一致性的要求级别,权衡性能、复杂度和成本,折腾出来的一套组合拳,核心就是认识到数据库和缓存是两个不同的东西,对它们的一切操作都不是原子的,网络延迟和并发穿插无处不在,必须用一些技巧来应对这种不确定性。
本文由颜泰平于2026-01-21发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://www.haoid.cn/wenda/84004.html
