第一章:缓存一致性问题全景
大家好,我是老张。在交易系统这个行当摸爬滚打了十几年,踩过的坑比写过的代码还多。今天咱们来聊聊缓存一致性——这个让无数架构师半夜惊醒的话题。
说白了,缓存一致性就是:缓存里的数据和数据库里的数据,到底能不能对上号。你想想看,交易系统里每一笔订单、每一个余额,都牵涉着真金白银。数据对不上,轻则用户体验差,重则资金损失。
核心观点:缓存一致性不是技术问题,而是业务问题。技术方案的选择,取决于你能接受多大的数据偏差。
1.1 不一致的根源:并发写入
先说说最常见的场景——并发写入。我记得刚带团队那会儿,有个同事问我:“张哥,我们用了Redis缓存用户余额,为什么有时候用户看到的余额和实际对不上?”
我让他画了个时序图,问题一下就清楚了:
看到了吗?线程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 我的解决思路
说了这么多问题,总得给点解决方案。我个人习惯把缓存一致性分为三个层次:
- 强一致性:使用分布式锁或事务,保证缓存和数据库同步更新。代价是性能下降,适合核心资金类数据。
- 最终一致性:允许短暂不一致,通过异步补偿机制最终对齐。适合非核心数据,比如用户昵称。
- 弱一致性:缓存只做读加速,写操作直接走数据库。适合读多写少的场景。
我的建议:别一上来就追求强一致性。先问问业务方:你能接受多少秒的数据延迟?5秒?30秒?还是1分钟?根据这个答案选方案,比拍脑袋靠谱得多。
好了,这一章咱们把缓存一致性的根源问题梳理清楚了。下一章我会详细讲讲具体的解决方案,包括延迟双删、异步补偿、分布式锁这些实战技巧。到时候我会拿我踩过的坑当案例,保证让你少走弯路。
本章小结:
- 并发写入是缓存不一致的头号元凶
- 网络延迟让问题更加隐蔽
- 宕机恢复需要提前制定策略
- 根据业务容忍度选择一致性级别