首页 / 业界 / 正文

分布式缓存一致性保障一线踩坑实录

上周帮朋友排查一个订单系统的诡异bug。

用户付完款,刷订单页永远显示”待支付”,查数据库明明已经是”已发货”。

折腾了3小时才发现,是缓存更新的顺序错了。

所有的坑都来自对银弹的迷信

很多人刚接触分布式缓存的时候,上来就背一堆理论:CAP定理、BASE模型、延迟双删、缓存穿透击穿雪崩。真到生产环境落地,全是经不起推敲的细节。

说白了,分布式缓存一致性保障,从来不是靠某一个方案能一劳永逸解决的。它本质上是一道权衡题——你愿意为了一致性,砍掉多少性能;或者说,你能容忍多长时间的脏数据窗口期。

分布式缓存双写顺序错误时序图

就说这个朋友的系统吧。他们之前照着网上的教程,写的逻辑是先删缓存,再更新数据库。听着没毛病对吧?问题出在数据库更新的那几百毫秒里。有个读请求刚好在这个间隙进来,发现缓存空了,就去数据库捞了当时还没更新完的旧值,顺手写回了缓存。等数据库终于更新完,缓存里已经是被写回去的旧值了,再也没人触发更新,就一直卡着。

很多人这个时候会说,哦,那加个延迟双删不就完了?

真没那么简单。

你说延迟时间设多少?设100毫秒?万一当时数据库刚好遇到慢查询,更新花了200毫秒,你100毫秒就去删缓存,等于白删。设2秒?那这2秒里的所有读请求,都能拿到脏数据。我之前见过一个团队,把延迟值设成了5秒,结果大促当天数据库主从同步延迟飙到8秒,等他们5秒去删缓存的时候,主从的新数据还没同步到从库,读请求又把从库的旧值写回缓存了。折腾到半夜,全是无效操作。

没有统一标准只有业务容忍度

别上来就追求什么强一致。

强一致意味着什么?你更新数据的时候,要把所有节点的所有缓存都锁死,所有读请求都得等你写完才能继续。那性能直接掉到原来的十分之一都不到。你做个用户头像、商品详情页的缓存,犯得上吗?用户刚换完头像,刷出来还是旧的,最多吐槽一句,过个几十秒自动刷新就完了,没人会因为这个较真。

但要是你做的是账户余额、库存扣减这类核心数据,那哪怕只有100毫秒的脏数据窗口期,都可能造成真金白银的损失。

分布式缓存一致性分级业务场景对照表
分布式缓存一致性分级业务场景对照表

我之前接触过一个做生鲜电商的团队,为了追求高并发,库存更新用了异步的最终一致方案。大促当天,缓存的窗口期刚好赶上一波秒杀流量,同一个库存被重复扣减了十几次,最后临到发货才发现超卖,赔了供应商一大笔钱。事后复盘的时候他们说,当初选型的时候只看到了”最终一致”四个字,完全忘了中间的窗口期,足够把所有库存卖穿。

那些听起来高级的方案都有隐藏成本

现在很多技术文章吹得天花乱坠,什么用消息队列做异步同步,什么用Canal监听binlog自动更新缓存,仿佛用了这些方案就能彻底解决一致性问题。

麻烦在于,这些方案全是有代价的。

用MQ做同步?那你得考虑消息丢失、消息乱序、消费失败重试的问题。要是没做全链路的幂等校验,丢一条消息,缓存就永久脏了。监听binlog?听起来是比自己写双写逻辑靠谱,毕竟是数据库层面的变更,顺序不会乱。但你要想清楚,binlog的同步是有固定延迟的,而且很多缓存更新的逻辑是和业务上下文绑定的——比如你更新订单状态的时候,要顺带计算用户的积分等级,要触发优惠券的核销逻辑,这些东西靠binlog同步是根本做不到的,你还是得绕回业务代码里写逻辑,反而把链路拉得更长,出问题的点更多。

其实做久了技术就会发现,很多时候你纠结半天分布式缓存一致性保障,本质上是当初为了扛流量上了缓存,现在又不想为这个性能红利付出对应的维护成本。要是你的业务真的对一致性要求高到极致,那干脆别用缓存,直接读数据库行不行?当然行。我见过做金融核心账务的团队,人家根本不上缓存,所有余额查询直接走数据库,配好读写分离和连接池,性能完全够,根本不需要纠结缓存和数据库的同步问题。

上次和那个踩坑的朋友吃饭,他说现在他们团队再也不追求什么完美方案了。给所有缓存操作加了不一致率的监控,只要数据偏差超过0.1%就立刻报警,把缓存和数据库的同步窗口期严格控制在100毫秒以内。也不搞什么复杂的延迟双删逻辑,就老老实实把数据库更新完,再删缓存,配合短时间的过期兜底。

毕竟,哪有什么银弹啊。