1. 缓存一致性问题概述
为什么需要缓存一致性?
先问大家一个问题:你写代码的时候,有没有遇到过明明刚存进去的数据,读出来却是旧的?
我在2015年做一个分布式KV存储项目时,就踩过这个坑。当时系统跑着跑着,数据就开始"串位"了——节点A写入了"value=100",节点B读到的却是"value=50"。排查了整整两天,最后发现是缓存一致性没处理好。
说白了,缓存一致性要解决的就是一个核心矛盾:数据只有一份,但访问它的路径有很多条。
你想想看,现代存储系统里,数据从磁盘到内存,从内存到CPU缓存,从本地节点到远端节点,每一层都在做缓存。缓存越多,性能越好,但数据不一致的风险也越大。
核心观点:缓存一致性不是"要不要"的问题,而是"怎么做到"的问题。只要系统里有多个数据副本,一致性就是绕不开的坎。
缓存一致性问题产生的根本原因
根本原因其实就三个字:共享了。
多核CPU共享同一块内存,多节点共享同一份数据。共享本身没问题,问题出在——每个核、每个节点都有自己的"小算盘"(本地缓存)。
我习惯把这个问题拆成两个层面来看:
1. 多核场景下的缓存不一致
先看一个最简单的例子:
// 假设两个CPU核同时执行这段代码
// 变量 x 初始值为 0
// CPU核0执行:
x = 1;
// CPU核1执行:
print(x); // 可能打印出 0,而不是 1
为什么会这样?因为每个核都有自己的L1/L2缓存。核0把x改成1,但修改还留在自己的缓存里,没写回内存。核1读x的时候,从自己的缓存或者内存里读到的还是0。
嗯,这里要注意:缓存一致性协议(比如MESI)就是用来解决这个问题的。但协议本身也有开销,不是所有场景都能完美覆盖。
2. 多节点场景下的缓存不一致
分布式系统里更复杂。每个节点都有自己的内存和磁盘缓存,节点之间通过网络通信。
我记得有一次做异地多活架构,两个数据中心各自缓存了一份用户数据。结果用户在北京修改了昵称,上海那边读到的还是旧昵称。这就是典型的分布式缓存不一致。
| 场景 | 不一致原因 | 典型例子 |
|---|---|---|
| 多核CPU | 每个核有私有缓存,写操作未及时同步 | MESI协议失效 |
| 多节点服务器 | 各节点独立缓存,网络延迟导致同步滞后 | Redis集群缓存雪崩 |
| 客户端-服务端 | 浏览器/CDN缓存与源站数据不一致 | 静态资源更新后未刷新 |
| 数据库与缓存 | 缓存层与持久层数据不同步 | 缓存穿透/击穿 |
缓存一致性的代价
你可能会想:那我把所有缓存都去掉,直接读写原始数据不就行了?
理论上可以,但实际没人这么干。因为没有缓存的系统,性能根本扛不住。我做过测试,一个100万QPS的查询系统,去掉缓存层后直接掉到5万QPS,差了20倍。
所以,缓存一致性本质上是在性能和正确性之间找平衡。
我的经验:不要追求100%的强一致性。大多数业务场景下,最终一致性就够用了。比如用户发了一条朋友圈,好友晚几秒看到,没人会在意。但如果是银行转账,那必须强一致。
缓存一致性的分类
业内一般把缓存一致性分成几个等级:
- 强一致性:写操作完成后,任何后续读操作都能读到最新值。代价是性能最差。
- 弱一致性:不保证立即读到最新值,但最终会一致。性能最好。
- 最终一致性:弱一致性的一个特例,保证"如果不再有更新,最终所有读操作都能返回最新值"。
我个人习惯把最终一致性再细分成几个变种:
- 因果一致性:有因果关系的操作,顺序必须一致。比如先发帖再评论,别人看到评论时一定也能看到帖子。
- 读己之写一致性:用户自己总能读到自己的最新写入。这个在用户系统里特别重要。
- 单调读一致性:一个用户多次读取同一个数据,不会读到越来越旧的值。
避坑指南:我曾经在一个金融项目中,为了追求强一致性,给每个写操作都加了分布式锁。结果系统吞吐量直接腰斩,业务方差点没把我骂死。后来改成读己之写一致性,配合版本号校验,既保证了正确性,性能也只下降了15%。
本章知识体系
下面这张图是我自己画的,把缓存一致性的核心脉络梳理了一下:
小结
缓存一致性这个问题,说白了就是:数据副本多了,怎么让它们看起来像只有一份。
我做了十几年存储系统,最大的体会是:没有银弹。不同的业务场景,要选不同的一致性模型。关键是要搞清楚你的业务到底能容忍多大的不一致,然后选择性价比最高的方案。
下一章我会深入讲MESI协议,那是多核CPU缓存一致性的基石。嗯,先消化这些吧。