一、引言:为什么会有双写一致性问题?
在互联网高并发系统中,Redis 作为缓存层几乎已经成为标配。引入缓存的初衷很简单——减轻数据库压力,提升读取性能。但缓存一旦引入,一个绕不开的问题就浮出水面:数据库中的数据更新了,Redis 中的缓存怎么办?
这就是所谓的“双写一致性问题”——当业务数据同时存在于数据库和 Redis 缓存中时,如何保证两者数据的一致性。
先看一个典型的场景:用户修改了自己的昵称,你更新了 MySQL,但 Redis 中还存着旧的昵称。下一次用户查询时,读到的是旧数据——这就是不一致。如果运气不好,这种不一致可能持续很长时间,直到缓存过期或被主动淘汰。
二、主流解决方案详解
方案一:Cache-Aside Pattern(旁路缓存模式)
这是最经典的缓存策略,核心思路是:应用程序在更新数据时,先更新数据库,再删除缓存。
工作流程:
- 读请求:先查缓存,命中则直接返回;未命中则查数据库,回填缓存
- 写请求:先更新数据库,再删除缓存
为什么是“删缓存”而不是“更新缓存”?
更新缓存存在明显的并发问题:线程A更新数据库为100,线程B更新数据库为200,如果两者以不同顺序更新缓存,最终缓存值可能是错的。而删除缓存则简单得多——下次读请求自然会从数据库加载最新值。
存在的问题:
先更新数据库再删除缓存,如果删除失败怎么办?缓存中会长期保留旧数据。另外,在极端并发下可能出现这种情况:线程A更新数据库后、删除缓存前,线程B读到旧缓存并回填,导致旧值覆盖新值。
改进思路:引入重试机制——删除缓存失败时,将需要删除的 key 发送到消息队列,由消费者不断重试直到成功。
方案二:延迟双删
延迟双删是对 Cache-Aside 的改良,流程如下:
- 先删除缓存
- 更新数据库
- 延迟 N 秒后再次删除缓存
这个“延迟再删一次”的目的是什么?主要是为了清除在更新数据库期间,其他读请求可能写入的旧缓存数据。
适用场景:对一致性要求不高的非高并发业务。
需要注意:延迟时间怎么定?一般设置为“读请求耗时 + 几百毫秒”的缓冲时间。但这个方案无法保证100%可靠,延迟处理也可能不满足实时性需求。
方案三:订阅 Binlog 异步同步(Canal + Redis)
这是目前生产环境中广泛使用的方案,核心思想是:应用程序只负责操作数据库,缓存由独立的同步服务自动维护。
整体架构:
应用写入 MySQL → MySQL 生成 binlog → Canal 伪装成 MySQL Slave 拉取 binlog → 解析后推送给 Redis 客户端 → 更新或删除 Redis 缓存。
Canal 的工作原理:Canal 是阿里巴巴开源的 MySQL binlog 增量订阅组件,它模拟 MySQL Slave 的交互协议,向 Master 发送 dump 协议接收 binlog,再将原始 byte 流解析为结构化的 INSERT/UPDATE/DELETE 事件。
关键设计要点:
- MySQL 必须开启 binlog 并设置为 ROW 模式
- Redis Key 设计要规范,建议格式为
{业务}:{实体}:{ID} - 设置合理的 TTL,防止冷数据长期驻留
- 同一 Key 的变更要按顺序处理(如 Kafka 分区按主键哈希)
优势:业务解耦、高吞吐低延迟、最终一致性有保障。
局限性:增加了组件复杂度(需部署 Canal、可能还需引入 MQ),且依赖 MySQL binlog。
方案四:分布式锁
在并发更新场景下,可以使用分布式锁(如 Redisson)在更新数据前锁定资源,确保同一时刻只有一个操作能进行。
优点:强一致性有保障。
缺点:性能开销大,不适合高并发场景。
三、方案对比与选型建议
| 方案 | 一致性级别 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| Cache-Aside + 重试 | 最终一致性 | 低 | 通用场景,大多数业务 |
| 延迟双删 | 最终一致性 | 低 | 非高并发、一致性要求不高 |
| Canal + Binlog | 最终一致性 | 中 | 需要解耦、变更频繁的场景 |
| 分布式锁 | 强一致性 | 中 | 并发冲突多、对一致性要求极高 |
选型建议:
- 大多数业务场景:Cache-Aside + 删除重试机制就足够了,简单可靠
- 数据变更频繁、希望解耦:Canal + Redis 是更好的选择
- 对一致性要求极高但并发不高:可以考虑分布式锁
- 终极兜底:无论采用哪种方案,都应该给缓存设置合理的过期时间,这是最后一道防线
四、总结
缓存一致性没有银弹。强一致性、高性能、高可用三者往往不可兼得。
在实际工程中,我们需要根据业务场景做取舍:
- 对于用户维度的数据(订单、用户信息),并发冲突概率低,Cache-Aside 足矣
- 对于商品、菜单等基础数据,Canal 订阅 binlog + 过期时间能覆盖绝大部分需求
- 秒杀、库存等对一致性要求极高的场景,则需要更严谨的方案组合
最后记住一点:缓存是性能优化的手段,不是数据的主存储。接受“最终一致性”的理念,在绝大多数场景下,用合理的方案+兜底机制,就能把不一致的风险控制在可接受范围内。