# Redis缓存和MySQL数据如何做到一致性? ## 一、 基础认知:更新缓存 vs 删除缓存 在修改数据库数据时,对于缓存是选择**更新(Update)**还是**删除(Delete)**? * **更新缓存(不推荐):** 每次更新数据库时,也去更新缓存中的值。 * **缺点:** 1. **并发写冲突:** 线程A和线程B同时写入,DB先A后B,缓存先B后A,导致数据不一致。 2. **计算开销:** 如果写入频繁但读取很少,或者缓存的值需要经过复杂计算才能得到,那么每次写入都更新缓存是资源的浪费。 * **删除缓存(推荐):** 更新数据库时,直接把缓存删掉,等待下一次读取时由读请求去回填缓存(Lazy Loading)。 * **优点:** 简单,避免了复杂的计算和部分并发问题。 --- ## 二、 进阶策略:操作顺序的博弈 既然选择了“删除缓存”,那么是**先删除缓存**,还是**先更新数据库**? ### 方案 1:先删除缓存,再更新数据库 * **流程:** 1. 线程 A 删除 Redis 缓存。 2. 线程 A 更新 MySQL。 * **问题(高并发下的脏数据):** 如果在线程 A 删除缓存后,更新 MySQL 之前,线程 B 进来读取数据。 * 线程 B 发现缓存空,去读 MySQL(此时还是旧数据)。 * 线程 B 把旧数据写入 Redis。 * 线程 A 终于将新数据写入 MySQL。 * **结果:** MySQL 是新数据,Redis 是旧数据,出现不一致。 ### 方案 2:先更新数据库,再删除缓存(Cache Aside Pattern 标准模式) * **流程:** 1. 线程 A 更新 MySQL。 2. 线程 A 删除 Redis 缓存。 * **问题(极端情况下的脏数据):** 虽然概率极低,但理论上存在: * 缓存刚好失效。 * 线程 B 读 MySQL(旧值)。 * 线程 A 更新 MySQL(新值)。 * 线程 A 删除缓存。 * 线程 B 将读到的旧值写入缓存。 * **注意:** 这种情况发生的条件是“写数据库”的操作比“读数据库+写缓存”的操作还要快,这在实际系统中几乎不可能,因为写DB通常比读DB慢得多。 * **真正的问题:** **第二步删除缓存失败了怎么办?** 如果 DB 更新成功,但 Redis 删除失败,那么 Redis 里存的就一直是旧数据。 --- ## 三、 高级解决方案:保证最终一致性 为了解决上述方案中的边缘Case(特别是删除失败或并发读写),业界有以下几种成熟方案: ### 1. 延时双删(解决“先删缓存”的并发问题) 如果你非要采用“先删缓存”的策略,或者为了保险起见,可以使用延时双删。 * **流程:** 1. 删除缓存。 2. 更新数据库。 3. **休眠一小段时间(比如 500ms)。** 4. **再次**删除缓存。 * **原理:** 休眠的时间是为了确保在步骤2执行期间,如果有并发的读请求把脏数据写入了缓存,步骤4可以把这个脏数据再次干掉。 * **缺点:** 增加了接口的响应时间(吞吐量降低)。 ### 2. 消息队列重试机制(解决“删除失败”问题) 针对“先更DB,后删缓存”中删除失败的情况。 * **流程:** 1. 更新数据库。 2. 删除缓存。 3. 如果删除失败,将要删除的 Key 发送到消息队列(MQ)。 4. 消费者监听 MQ,接收到 Key 后不断重试删除操作,直到成功。 * **优点:** 实现了重试逻辑,保证最终一定删除。 * **缺点:** 侵入业务代码,业务逻辑和重试机制耦合。 ### 3. 订阅 Binlog 变更(业界最佳实践 - 异步解耦) 这是目前大厂最常用的方案(如使用阿里的 **Canal** 中间件)。 * **流程:** 1. 业务代码只操作 MySQL,完全不管 Redis。 2. MySQL 更新数据后产生 Binlog。 3. Canal 伪装成 MySQL 的从节点,订阅并解析 Binlog。 4. Canal 将解析后的数据变更发送到消息队列(Kafka/RabbitMQ)。 5. 专门的服务消费 MQ,负责执行 Redis 的删除或更新操作。 * **优点:** * **完全解耦:** 业务代码不需要关心缓存一致性。 * **可靠性高:** 依赖 MySQL 的 Binlog,只要数据落库,缓存最终一定会被修正。 * **缺点:** 系统架构变复杂了,引入了新的中间件。 --- ## 四、 总结与对比表 | 策略 | 适用场景 | 一致性强度 | 复杂度 | 缺点 | | :--- | :--- | :--- | :--- | :--- | | **先更DB,后删缓存** | **中小型项目、低并发** | 中 | 低 | 极端情况下有脏数据;如果删缓存失败会有问题。 | | **延时双删** | **读多写少、由于网络原因可能产生脏数据** | 中高 | 中 | 增加请求耗时。 | | **订阅 Binlog (Canal)** | **大型项目、高并发、对一致性要求较高** | **高 (最终一致)** | **高** | 架构复杂,引入中间件维护成本。 | | **强一致性 (读写锁)** | **金融级别、绝对不能错** | 强 | 中 | **性能极差**,几乎退化为串行,不适合高并发缓存。 | --- ## 面试回答话术建议 在回答时,建议遵循以下逻辑: 1. **定调:** 首先说明,通常我们提到的一致性是指**最终一致性**。 2. **抛出标准方案:** 最通用的方案是 **Cache Aside Pattern**(先更DB,后删缓存)。 3. **指出漏洞:** 主动指出这个方案存在“删除失败”的风险。 4. **给出解决方案:** * 简单方案:利用消息队列进行**失败重试**。 * 进阶方案(加分项):利用 **Binlog + Canal + MQ** 进行异步更新,实现业务解耦和高可靠性。 5. **补充场景:** 如果面试官问“必须要强一致性怎么办”,回答“使用分布式锁(Redisson ReadWriteLock)或者直接不走缓存读 DB”,但要强调这会牺牲性能。