第一章:缓存一致性问题全景

大家好,我是老张。在交易系统这个行当摸爬滚打了十几年,踩过的坑比写过的代码还多。今天咱们来聊聊缓存一致性——这个让无数架构师半夜惊醒的话题。

说白了,缓存一致性就是:缓存里的数据和数据库里的数据,到底能不能对上号。你想想看,交易系统里每一笔订单、每一个余额,都牵涉着真金白银。数据对不上,轻则用户体验差,重则资金损失。

核心观点:缓存一致性不是技术问题,而是业务问题。技术方案的选择,取决于你能接受多大的数据偏差。

1.1 不一致的根源:并发写入

先说说最常见的场景——并发写入。我记得刚带团队那会儿,有个同事问我:“张哥,我们用了Redis缓存用户余额,为什么有时候用户看到的余额和实际对不上?”

我让他画了个时序图,问题一下就清楚了:

并发写入导致缓存不一致 时间 线程A:读取余额(100元) 线程A:更新数据库(80元) 线程A:更新缓存(80元) 线程B:读取余额(100元) 线程B:更新数据库(90元) 线程B:更新缓存(90元) 结果:缓存=90元,数据库=80元 → 不一致!

看到了吗?线程A和线程B几乎同时读取了旧值100元,然后各自更新。最后缓存里是90元,数据库里却是80元。这就是典型的读写并发冲突

我的经验:我曾经在一个高并发秒杀项目中,就因为没处理好这个场景,导致库存数据差了2000多件。排查了一整天才找到根因——两个线程同时扣减库存,缓存里扣了,数据库没扣成。

1.2 网络延迟:看不见的杀手

网络延迟这事儿,说实话比并发写入更隐蔽。你想想看,一个请求从客户端到服务器,再到数据库和缓存,中间经过多少跳?

我给大家列个典型场景:

操作步骤 耗时(ms) 风险点
客户端发起请求 1-5 网络抖动
应用层处理 2-10 线程调度延迟
更新数据库 5-50 锁等待、事务冲突
更新缓存 1-3 网络闪断
返回客户端 1-5 连接池耗尽

你看,一个简单的更新操作,总耗时可能达到几十毫秒。这期间如果发生网络闪断,缓存更新失败,数据库却已经提交了——数据就不一致了。

避坑指南:我曾经遇到过一个案例,某支付系统的缓存更新成功率是99.9%,看起来很高对吧?但每天几百万笔交易,就有几千笔缓存没更新成功。用户看到的余额和实际差了十几块钱,投诉电话被打爆了。

1.3 宕机恢复:最头疼的场景

宕机恢复,嗯,这个我得好好说说。系统崩了不可怕,可怕的是崩完之后数据对不上。

常见的宕机场景有三种:

  • 缓存宕机:Redis挂了,重启后数据全丢了。数据库里还有数据,但缓存是空的。这时候如果直接读数据库重建缓存,可能读到旧数据。
  • 数据库宕机:数据库挂了,缓存里还有数据。等数据库恢复后,缓存里的数据可能已经过期了。
  • 应用宕机:正在更新缓存时应用挂了,数据库更新了,缓存没更新。重启后缓存里还是旧数据。

我记得有一次线上事故,凌晨三点缓存集群挂了两个节点。自动恢复后,有部分用户的订单状态显示“已支付”,但数据库里还是“待支付”。原因就是缓存恢复时读到了过期的数据。

关键认知:宕机恢复不是技术问题,是策略问题。你要决定:是优先保证可用性(允许短暂不一致),还是优先保证一致性(拒绝服务直到恢复)。

1.4 不一致的几种模式

根据我多年的经验,缓存不一致可以归纳为四种模式:

模式 描述 典型场景 影响程度
脏读 缓存读到未提交的数据 事务未提交,缓存先更新了
不可重复读 同一次请求中读到不同值 缓存过期,数据库已更新
幻读 缓存中数据不存在,数据库存在 缓存淘汰策略导致
数据丢失 缓存和数据库数据完全对不上 并发写入+网络延迟 极高

你想想看,脏读和数据丢失是最要命的。尤其是交易系统,一笔订单的金额对不上,可能就是几万块的损失。

1.5 我的解决思路

说了这么多问题,总得给点解决方案。我个人习惯把缓存一致性分为三个层次:

  1. 强一致性:使用分布式锁或事务,保证缓存和数据库同步更新。代价是性能下降,适合核心资金类数据。
  2. 最终一致性:允许短暂不一致,通过异步补偿机制最终对齐。适合非核心数据,比如用户昵称。
  3. 弱一致性:缓存只做读加速,写操作直接走数据库。适合读多写少的场景。

我的建议:别一上来就追求强一致性。先问问业务方:你能接受多少秒的数据延迟?5秒?30秒?还是1分钟?根据这个答案选方案,比拍脑袋靠谱得多。

好了,这一章咱们把缓存一致性的根源问题梳理清楚了。下一章我会详细讲讲具体的解决方案,包括延迟双删、异步补偿、分布式锁这些实战技巧。到时候我会拿我踩过的坑当案例,保证让你少走弯路。

本章小结:

  • 并发写入是缓存不一致的头号元凶
  • 网络延迟让问题更加隐蔽
  • 宕机恢复需要提前制定策略
  • 根据业务容忍度选择一致性级别

专注资料整理