1. 完整回答
缓存一致性主要指缓存和数据库中的数据保持一致,避免数据库已经更新,但缓存仍然返回旧数据的问题。
在使用 MySQL 和 Redis 的系统中,缓存主要用于提高查询性能、减轻数据库压力。但由于 Redis 和 MySQL 是两个独立的存储系统,更新操作很难天然保证原子性,再加上并发读写、网络异常等情况,就可能出现数据不一致。
实际开发中,比较常用的是 Cache Aside(旁路缓存)模式。
读取数据时,先查询 Redis,命中就直接返回;如果没有命中,再查询 MySQL,并将结果写入 Redis。
更新数据时,通常采用先更新数据库,再删除缓存的策略。数据库更新成功后,将对应缓存删除,后续读请求会重新查询数据库,并加载最新数据。
选择删除缓存,主要是为了降低并发更新时旧数据覆盖新数据的风险,同时避免维护不常访问的数据。
不过,这种方案仍然存在两个问题:一是缓存删除失败,可能导致旧数据持续存在;二是并发读写时,旧数据可能在缓存删除之后重新写入。
针对这些问题,可以通过删除失败重试、消息队列、订阅数据库 Binlog、设置合理的过期时间等方式提高可靠性。对于并发回填问题,还可以通过版本控制等机制进一步处理。
整体来说,普通业务通常采用最终一致性,允许短暂的数据延迟;对于资金、库存扣减等要求严格正确性的操作,则需要以数据库事务和业务校验为准,不能只依赖缓存。
2. 核心原理与关键概念
2.1 核心原理
缓存一致性的核心思路是:以数据库作为权威数据源,通过缓存失效机制,让后续读取重新获取最新数据。
(1)读取流程
查询 Redis → 判断缓存是否命中:
-
命中:直接返回缓存数据。
-
未命中:查询 MySQL → 写入 Redis 并设置过期时间 → 返回数据。
(2)更新流程
更新 MySQL → 数据库事务提交成功 → 删除 Redis 对应缓存 → 后续读取重新加载数据。
这里有三个关键环节:
-
数据库优先更新:数据库负责持久化,保证权威数据先完成变更。
-
缓存主动失效:删除对应缓存,避免继续命中更新前的数据。
-
按需重新加载:只有后续发生查询时,才将最新数据写回缓存。
(3)异常处理流程
数据库提交成功 → 删除缓存失败 → 记录失败任务 → 异步重试删除 → 必要时通过过期时间兜底。
如果通过 Binlog 进行同步,流程是:
MySQL 事务提交 → 产生 Binlog → CDC 组件捕获变更 → 消费变更事件 → 删除对应 Redis 缓存。
这类同步属于异步处理,通常提供最终一致性,实际效果取决于消息延迟、重试机制和异常恢复能力。
2.2 关键概念
| 概念 | 含义 |
|---|---|
| Cache Aside | 应用主动管理缓存,读时按需加载,写时更新数据库并使缓存失效 |
| 缓存失效(Invalidation) | 数据发生变更后,主动删除或标记对应缓存不可用 |
| 最终一致性 | 允许短时间存在新旧数据差异,经过同步或修复后最终收敛 |
| TTL(过期时间) | 设置缓存有效期,限制旧缓存持续存在的时间 |
| CDC(变更数据捕获) | 通过数据库变更日志获取数据更新事件,驱动缓存同步 |
| 延迟双删 | 在更新数据库前后执行两次缓存删除,第二次延迟执行,以降低旧数据回填的风险 |
3. 高频面试追问
3.1 为什么推荐先更新数据库,再删除缓存?
主要是为了降低并发读写造成数据不一致的风险。
如果先删除缓存,再更新数据库,可能出现下面的执行顺序:
-
线程 A 删除 Redis 缓存。
-
线程 B 查询缓存未命中,从 MySQL 读取旧数据。
-
线程 B 将旧数据写回 Redis。
-
线程 A 完成数据库更新。
这样数据库已经是新数据,但缓存仍然保存旧数据。
采用先更新数据库、再删除缓存的方式,可以减少这种情况。数据库提交后再删除缓存,后续正常的缓存重建就能够获取最新数据。
但这个方案仍有极端并发问题,所以降低风险不等于完全保证强一致性。
3.2 为什么选择删除缓存,而不直接更新缓存?
主要有两个原因:降低并发覆盖风险,以及减少无效更新。
假设两个线程同时修改同一条数据:
-
线程 A 将数据库更新为 100。
-
线程 B 将数据库更新为 200。
-
线程 B 将缓存更新为 200。
-
线程 A 将缓存更新为 100。
最终数据库是 200,缓存却变成了 100。
如果采用删除缓存的方式,后续查询会重新从数据库加载数据,可以减少这种乱序更新产生的问题。
另外,有些数据更新后可能长时间无人访问,直接更新缓存会消耗额外资源。
因此,对于以查询为主、数据更新相对较少的业务,删除缓存通常更简单,也更容易维护。
3.3 先更新数据库,再删除缓存,就一定能保证一致性吗?
不能保证绝对一致,仍然可能出现并发回填旧数据的问题。
假设数据库原来的值是 100,缓存刚好失效:
-
线程 A 查询 Redis 未命中,从数据库读取到 100。
-
线程 B 更新数据库,将值修改为 200。
-
线程 B 删除 Redis 缓存。
-
线程 A 将之前读取的 100 写入 Redis。
最终数据库为 200,缓存为 100。
这种情况需要缓存未命中、读写并发以及特定的执行顺序同时出现,通常发生概率相对较低,但确实存在。
如果对一致性要求更高,可以采用版本号校验、按 Key 协调缓存重建和失效操作等方案,防止旧数据在新数据更新后重新覆盖缓存。
版本控制需要保证版本信息本身可靠,并通过原子校验阻止旧版本写入,单纯给缓存增加一个版本字段还不够。
3.4 数据库更新成功,但删除缓存失败怎么办?
需要保证缓存删除操作能够重试,避免旧数据长期存在。
一般有三种处理方式:
-
同步重试:删除失败后进行有限次数的重试,适合短暂的网络异常。
-
异步重试:通过消息队列或持久化任务记录失败操作,由消费者持续重试。
-
Binlog 同步:订阅数据库提交后的变更日志,由独立组件执行缓存失效操作。
实际系统中,还会设置合理的 TTL,作为遗漏删除时的兜底措施。
需要注意,数据库提交成功之后,即使缓存删除失败,也无法通过普通数据库事务直接回滚已经提交的数据。
因此,核心是可靠地记录待处理的失效操作,并保证最终能够执行成功。
3.5 什么是延迟双删?为什么不能完全依赖它?
延迟双删是在更新数据库前后执行两次缓存删除,用来降低并发读取旧数据并回填缓存的风险。
基本流程是:
第一次删除缓存 → 更新数据库并提交 → 延迟一段时间 → 第二次删除缓存。
第一次删除后,可能有其他线程读取旧数据库数据并写回缓存。
第二次删除的作用,就是尽量清除这段时间内重新产生的旧缓存。
但它存在两个问题:
-
延迟时间很难准确确定,可能受到数据库查询耗时、线程调度和主从复制延迟的影响。
-
第二次删除本身也可能失败,而且无法覆盖所有极端并发情况。
所以,延迟双删可以作为一种补偿措施,但不能直接视为强一致性保证。
3.6 如何通过 Binlog 保证缓存最终一致性?
可以利用 CDC 组件订阅 MySQL Binlog,在数据库提交变更后异步删除缓存。
典型流程是:
MySQL 更新数据并提交事务 → Binlog 记录变更 → Debezium 或 Canal 捕获事件 → 消息队列传递事件 → 消费者删除 Redis 缓存。
这种方案的优势在于:
-
能感知不同服务或其他程序产生的数据库变更。
-
将数据库写入逻辑和缓存同步逻辑解耦。
-
可以结合消息持久化、消费重试提高可靠性。
但需要注意,Binlog 同步存在一定延迟,消息还可能重复消费。
因此,消费者应具备幂等处理能力,并保证必要的事件顺序和失败重试。
如果直接使用事件中的新值更新缓存,还需要处理乱序覆盖;单纯删除对应 Key 通常更容易处理。
继续追问:直接发送 MQ 和订阅 Binlog 有什么区别?
直接发送 MQ 需要解决数据库事务和消息发送之间的一致性问题。例如数据库提交成功,但消息发送失败,就可能遗漏缓存删除。
可以采用事务消息或本地消息表(Outbox),提高消息发送可靠性。
订阅 Binlog 则直接基于数据库已经提交的变更产生事件,适合存在多个数据写入入口的系统,但需要额外维护 CDC 组件和消费链路。
3.7 数据库事务还没提交,可以先删除缓存吗?
一般应该在数据库事务成功提交之后,再执行缓存删除。
如果事务尚未提交就删除缓存,其他线程可能发现缓存未命中,并从数据库读取到事务提交前的旧数据,再写回 Redis。
如果事务最终回滚,还可能造成多余的缓存失效和数据库查询。
在 Spring 项目中,可以利用事务提交后的回调,或者事务事件监听机制,在提交成功之后触发缓存删除。
但要注意,事务提交回调只能保证执行时机,不能保证缓存删除一定成功。
对于可靠性要求较高的场景,还需要持久化重试任务或通过 CDC 等机制补偿。
3.8 MySQL 主从复制延迟会影响缓存一致性吗?
会,读写分离可能让已经删除的缓存重新加载旧数据。
例如:
-
主库数据从 100 更新为 200。
-
更新成功后删除 Redis 缓存。
-
新的查询请求未命中 Redis。
-
从库尚未完成复制,仍然返回 100。
-
旧数据被重新写入 Redis。
这时即使缓存删除操作完全成功,仍然可能出现数据不一致。
常见处理方式包括:更新后的关键读取走主库、等待从库追上相应复制位点,或者在必要时增加延迟失效措施。
对于订单状态、用户刚修改的资料等需要立即看到更新结果的场景,通常要优先考虑读主库或写后读一致性机制。
3.9 设置缓存过期时间,能解决一致性问题吗?
TTL 可以限制旧缓存的存活时间,但不能保证数据实时一致。
例如缓存设置 5 分钟过期,数据库更新后,如果缓存删除失败,旧数据可能继续被读取,直到缓存过期。
因此,TTL 更适合作为兜底机制。
实际设置时需要结合业务特点:
-
商品展示、文章内容等允许短暂延迟的数据,可以设置相对较长的过期时间。
-
更新频繁且要求较高实时性的数据,应缩短过期时间,并配合主动失效。
-
对大量缓存设置过期时间时,可以适当加入随机偏移,减少集中失效造成的数据库压力。
另外,TTL 只能约束单次缓存写入的生命周期。如果旧数据被不断重新写入缓存,仍然需要解决底层的并发和数据源问题。
3.10 对一致性要求很高的业务,应该怎么设计?项目中如何落地?
需要根据数据的重要程度选择不同的一致性策略,关键业务操作应以权威数据库的事务结果为准。
例如电商系统可以分成两类:
商品详情展示:以读取性能为主,可以采用 Redis + Cache Aside,更新数据库后删除缓存,并配合 TTL 和失败重试,实现最终一致性。
库存扣减和订单支付:正确性要求更高,需要在数据库事务中使用条件更新、行锁或其他并发控制方式,确保不会超卖或重复处理。缓存可以用于展示库存,但不能直接作为最终扣减依据。
具体项目中,还需要关注缓存删除失败次数、CDC 消费延迟、缓存与数据库抽样校验结果,并通过日志和告警及时发现异常。
继续追问:Redis 使用分布式锁,能直接解决缓存一致性吗?
不能直接保证。分布式锁可以协调特定 Key 的并发操作,例如避免大量请求同时重建同一份缓存。
但如果只锁住缓存写入,没有覆盖数据库更新和缓存失效之间的竞争,仍然可能出现旧数据回填。
因此,需要根据实际竞争过程设计锁的范围,并同时考虑超时、异常释放和旧请求覆盖等问题。
4. 易混淆概念对比
4.1 先删除缓存与先更新数据库
| 对比维度 | 先删除缓存,再更新数据库 | 先更新数据库,再删除缓存 |
|---|---|---|
| 执行顺序 | 删除缓存 → 更新数据库 | 更新数据库 → 删除缓存 |
| 主要风险 | 数据库更新前,旧数据可能被重新加载 | 删除失败,或旧查询结果延迟回填 |
| 并发风险 | 通常更容易出现旧数据回填 | 风险相对较低,但无法完全避免 |
| 实际选择 | 一般不优先使用 | Cache Aside 常用方案 |
4.2 删除缓存与更新缓存
| 对比维度 | 删除缓存 | 更新缓存 |
|---|---|---|
| 核心操作 | 删除旧值,等待下次读取重建 | 主动写入新值 |
| 并发处理 | 减少旧版本直接覆盖新版本的情况 | 并发乱序写入可能覆盖新数据 |
| 资源消耗 | 按需重新加载 | 即使无人读取也可能更新 |
| 适用情况 | 普通读多写少业务 | 有可靠同步、版本控制或统一写入机制的业务 |
4.3 Cache Aside、Write Through 与 Write Behind
| 模式 | 工作方式 | 主要特点 |
|---|---|---|
| Cache Aside(旁路缓存) | 读取时按需加载;更新数据库后使缓存失效 | 简单灵活,常用于读多写少场景 |
| Write Through(写穿) | 写入时同步维护缓存和数据库 | 数据更新较及时,但增加写入延迟,仍需处理部分失败 |
| Write Behind(写回) | 先写缓存,再异步持久化到数据库 | 写入性能较好,但存在数据丢失和延迟持久化风险 |
4.4 延迟双删与 Binlog 同步
| 对比维度 | 延迟双删 | Binlog 同步 |
|---|---|---|
| 核心机制 | 延迟执行第二次删除 | 根据数据库变更事件触发删除 |
| 触发依据 | 预设延迟时间 | 数据库已提交的变更日志 |
| 主要问题 | 延迟时间难确定,仍有并发风险 | 存在消费延迟、故障恢复和运维成本 |
| 适用场景 | 特定并发风险的补偿 | 多服务写入、较复杂的缓存同步系统 |
4.5 强一致性与最终一致性
| 对比维度 | 强一致性 | 最终一致性 |
|---|---|---|
| 数据可见性 | 满足约定的一致性保证,成功写入后不能被后续读取观察为旧版本 | 允许短暂读取旧数据 |
| 实现成本 | 通常需要更严格的协调和并发控制 | 更容易兼顾性能与可用性 |
| 适用场景 | 资金扣减、核心交易状态校验 | 商品详情、用户资料展示、文章内容 |
| 缓存策略 | 关键操作绕过缓存或引入严格一致性协议 | 缓存失效、异步同步、重试及 TTL 兜底 |
5. 总结
缓存一致性主要解决 Redis 和 MySQL 之间的数据同步问题,避免数据库更新后,缓存仍然返回旧数据。
实际开发中,最常用的是 Cache Aside 模式,读取时优先访问缓存,未命中再查询数据库;更新时采用先更新数据库,再删除缓存的策略,让后续请求按需加载最新数据。
这种方案实现相对简单,适合读多写少的业务,但仍然存在删除失败、并发旧数据回填以及主从复制延迟等问题。
通常可以结合删除重试、Binlog 订阅、TTL 兜底和必要的并发控制提高一致性。
最终需要根据业务对数据实时性和正确性的要求进行取舍。普通查询可以采用最终一致性,资金和库存等关键操作则应依靠数据库事务和业务约束来保证正确性。