数据库与缓存13 分钟阅读更新于 2026-07-30

数据库和 Redis 缓存一致性:Cache Aside、删除缓存和补偿机制

数据库与缓存一致性章节,讲清 Cache Aside 的读写策略、为什么常用先更新数据库再删除缓存、更新缓存方案的并发风险,以及删除缓存失败后的消息队列重试和订阅 Binlog 补偿。

相关工具

一致性问题从写缓存开始

Redis 做缓存时,最常见的链路是应用先读 Redis,未命中再读 MySQL。这样能减少数据库压力,但也带来一个绕不开的问题:数据库里的数据变了,缓存里的数据怎么办?如果缓存没及时处理,用户就可能读到旧数据。

资料 在缓存异常之后讲数据库和缓存一致性,核心方案是 Cache Aside。它把策略拆成读策略和写策略。读策略负责缓存没有时怎么回源,写策略负责数据库更新时缓存怎么处理。

这篇先不追求绝对强一致,因为 Redis 缓存层大多数场景都不是强一致存储。更现实的目标是:让缓存和数据库在可接受时间内收敛,减少脏数据窗口,并且在删除缓存失败时有补偿机制。

数据库与 Redis 缓存一致性图解,展示 Cache Aside 读策略、写策略、更新缓存并发风险、分布式锁、短 TTL、消息队列重试删除缓存和订阅 MySQL Binlog 删除 Redis key
Cache Aside 读写与补偿流程

读策略是命中缓存直接返回,未命中查数据库再写缓存;写策略通常先更新数据库再删除缓存,删除失败时用消息队列重试或订阅 Binlog 做异步补偿。

Cache Aside 的读策略

资料 对读策略的描述很直接:如果读取的数据命中了缓存,就直接返回数据;如果没有命中缓存,就从数据库中读取数据,然后把数据写入缓存,并且返回给用户。

这个流程就是典型 Cache Aside 的读路径。应用不把 Redis 当成唯一数据源,而是把它放在数据库旁边。缓存有,就用缓存;缓存没有,就回到数据库拿真实数据,再把结果放回缓存。下次同样请求再来,就能命中 Redis。

读策略里要注意两个细节。第一,写入缓存时通常要设置过期时间,避免旧数据永远留在缓存里。第二,数据库查不到时,要结合缓存穿透处理,可以缓存空值或使用布隆过滤器,不要让同一个不存在的数据反复打到数据库。

Cache Aside 的写策略

已有的写策略步骤是:更新数据库中的数据;删除缓存中的数据。也就是先让 MySQL 里的业务事实发生变化,再把 Redis 里可能过时的缓存删掉。下次读请求过来时,如果缓存未命中,就会重新从数据库读取新数据并写回缓存。

这个方案听起来有点绕。为什么不是更新数据库后顺手更新缓存?为什么要删除?原因是删除比更新更不容易引入复杂并发问题。数据库更新完成后,把对应缓存删掉,让下一次读重新构建缓存,通常更稳。

资料 也明确说,“先更新数据库 + 再删除缓存”的方案可以保证数据一致性,但每次更新数据时缓存都会被删除,会影响缓存命中率。这个代价是真实存在的:写多的业务里,缓存可能经常被删,命中率会下降。

为什么不优先更新缓存

如果业务很看重缓存命中率,已有内容提到可以采用“更新数据库 + 更新缓存”的方案。因为更新缓存不会出现缓存未命中的情况,读请求继续能命中 Redis。看起来它比删除缓存更省事。

问题出在并发更新。假设两个请求同时更新同一条数据,请求 A 要把值改成 v1,请求 B 要把值改成 v2。它们更新数据库和更新缓存的执行顺序可能交错。数据库最终是 v2,但缓存可能因为 A 的更新缓存动作晚执行,最后留下 v1。这样就出现数据库和缓存不一致。

这类问题很隐蔽,因为单线程、低并发测试通常看不出来。只有两个写请求在时间上交错时,才会暴露。对大多数业务来说,删除缓存让下一次读重建,比并发更新缓存更容易控制。

更新缓存方案如何补救

资料 对“更新数据库 + 更新缓存”的并发问题给了两个处理办法。第一,在更新缓存前先加分布式锁,保证同一时间只运行一个请求更新缓存。这样并发更新会被串行化,降低旧值覆盖新值的风险。

分布式锁的代价是复杂度和性能开销。你要处理锁超时、锁释放、异常退出、重试等问题。它适合确实需要同步更新缓存、且缓存命中率非常重要的场景。否则,为了保命中率引入锁,可能让系统变得更难维护。

第二,在更新完缓存时,给缓存加上较短的过期时间。即使并发导致缓存短暂写错,错误数据也不会停留太久。这个办法不能消除不一致,只是缩短脏数据窗口。对允许短暂不一致的业务,它是一个实用的缓冲。

删除缓存失败怎么办

先更新数据库再删除缓存,最大的现实问题是:数据库更新成功了,但删除缓存失败怎么办?比如 Redis 短暂不可用、网络抖动、应用进程刚好异常退出。这个时候数据库已经是新值,缓存里还留着旧值。

相关概念还问了“如何保证删除缓存操作一定能成功”,给出的方向是重试机制,并引入消息队列。如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存。重试超过一定次数仍然失败,就需要向业务层发送报错信息。

这里的重点是把删除缓存从一次网络调用,变成一个可重试的任务。删除成功,就把消息从队列里移除,避免重复操作;删除失败,就继续重试。这样即使 Redis 短暂故障,系统也有机会在后面把旧缓存清掉。

消息队列重试要注意幂等

用消息队列重试删除缓存时,删除操作本身最好是幂等的。删除一个已经不存在的 Redis key,不应该被当成业务错误。因为同一条删除消息可能重试多次,也可能第一次已经删除成功,但确认消息时失败,后面又被消费一次。

重试次数也要有上限。文中提到,如果重试超过一定次数还是没有成功,需要向业务层发送报错信息。这个报错不一定是直接打断用户请求,而是进入告警、人工处理、补偿任务或数据修复流程。

实际设计时,可以把要删除的缓存 key、业务主键、变更时间、重试次数写进消息。这样后面排查时能知道到底是哪条数据删缓存失败,而不是只看到 Redis 和 MySQL 不一致,却找不到来源。

订阅 Binlog 是另一条路

同时给了另一种方案:订阅 Binlog。订阅 MySQL 的 Binlog 日志,拿到具体变化的数据,然后再执行缓存删除。它的思路是把数据库变更本身作为事实来源,缓存删除服务根据数据库日志异步处理。

文中描述得很形象:删除服务可以把自己伪装成一个 MySQL 从节点,向 MySQL 主节点发送 dump 请求。主节点收到请求后开始推送 Binlog。删除服务解析 Binlog 字节流,把它转换成便于读取的结构化数据,再删除对应缓存。

这种方式的好处是和业务代码解耦。业务只负责写数据库,缓存删除由独立服务订阅数据库变更来完成。缺点是链路更长,要维护 Binlog 订阅、解析、位点、失败重试和延迟监控。适合数据变更来源多、希望统一处理缓存失效的大型系统。

为什么是删除缓存,不是先删缓存再更新数据库

资料 主要强调的是“先更新数据库,再删除缓存”。有些人会问,能不能先删缓存再更新数据库?这个顺序在并发读写下容易出现旧值回填。比如应用先删了缓存,还没来得及更新数据库;另一个读请求发现缓存没有,就查旧数据库值并写回缓存;随后写请求才把数据库改成新值。结果缓存里又留下旧数据。

先更新数据库再删除缓存,虽然也不是绝对无窗口,但风险更小。数据库先变成新值,缓存被删后,下次读会回源拿新值。即使删除缓存失败,也可以用消息队列或 Binlog 补偿。

所以在 Cache Aside 里,常见建议不是“每次写都更新缓存”,也不是“先删缓存再写库”,而是先写数据库,再删缓存,并准备好删除失败后的补偿机制。这个顺序更符合大多数读多写少的缓存场景。

一致性不是只有技术动作

缓存一致性方案要结合业务容忍度。用户昵称、商品介绍、文章内容,短时间旧一点通常问题不大;余额、订单支付状态、库存扣减,就要更谨慎。不是所有数据都适合用同一种缓存策略。

对允许短暂不一致的数据,可以用 Cache Aside,加合理 TTL,再配合删除失败重试。对一致性要求高的数据,可以减少缓存层参与,或让关键读走数据库,或者把缓存只作为旁路加速而不是判断事实的依据。

还要监控缓存删除失败、消息队列积压、Binlog 订阅延迟、缓存命中率和数据库回源量。缓存一致性最怕悄悄坏:页面还能打开,接口还能返回,但用户看到的是旧数据。监控能让这种问题早点露出来。

面试可以这样回答

如果被问如何保证数据库和 Redis 缓存一致性,可以先说常见方案是 Cache Aside。读策略是先读缓存,命中就返回;未命中就查数据库,把结果写入缓存,再返回给用户。写策略是先更新数据库,再删除缓存。

然后解释为什么删除缓存:更新缓存虽然能提高命中率,但两个更新请求并发执行时,可能出现数据库是新值、缓存被旧请求覆盖成旧值的问题。如果确实要更新缓存,可以加分布式锁,或者更新后设置较短 TTL,缩短不一致窗口。

最后说删除缓存失败的补偿:可以引入消息队列做重试,删除成功后移除消息,重试多次失败则告警;也可以订阅 MySQL Binlog,由独立服务解析数据库变更并删除相关 Redis key。这样回答既讲了流程,也讲了失败场景。

常见问题

Cache Aside 的读策略是什么?

先读 Redis,命中缓存就直接返回;未命中就查数据库,把数据库结果写入 Redis,再返回给用户。

为什么常用先更新数据库再删除缓存?

因为删除缓存比并发更新缓存更容易控制。数据库更新后删除旧缓存,下次读会回源数据库并重建新缓存。

删除缓存失败怎么办?

可以用消息队列做重试,删除成功后移除消息,连续失败则告警;也可以订阅 MySQL Binlog,根据数据库变更异步删除相关缓存。

更新缓存方案为什么有并发风险?

两个写请求并发时,数据库最终可能是后一个请求的新值,但前一个请求的更新缓存动作晚执行,把缓存覆盖成旧值,导致缓存和数据库不一致。

MySQL 与 Redis

继续阅读

返回专题