缓存与数据库一致性及故障降级一、业务场景某电商系统的商品详情页引入 Redis 缓存以降低数据库访问压力、提高查询性能。随着业务发展系统出现了以下问题商家修改商品价格后部分用户仍然看到旧价格。大促期间部分商品缓存频繁失效导致数据库负载上升。热门商品缓存过期后大量请求同时查询数据库造成请求超时。Redis 整体不可用时大量请求直接访问数据库可能导致数据库过载。多个应用实例使用本地缓存时缓存数据可能不一致。MQ 消息可能重复、乱序或丢失导致缓存无法及时更新。应用实例停机期间错过变更事件重启后可能继续使用旧数据。本场景的核心目标是在保证系统可用性和性能的同时合理控制缓存与数据库之间的数据一致性并具备完善的故障恢复能力。二、缓存与数据库一致性的基本原理2.1 为什么缓存与数据库容易不一致Redis 和 MySQL 是两个独立的数据存储系统通常无法通过一个普通的数据库事务同时保证两者的数据修改原子性。例如用户请求修改商品价格。MySQL 将价格更新为 80 元。Redis 中仍然保存着旧价格 100 元。后续请求直接读取 Redis得到旧价格。因此缓存一致性设计需要考虑数据更新顺序。并发读写。缓存删除失败。消息丢失与重复。缓存过期。多实例之间的数据同步。故障期间的恢复与补偿。2.2 Cache Aside Pattern旁路缓存模式这是业务系统中常见的缓存使用模式。读取数据先查询 Redis。缓存命中直接返回。缓存未命中查询 MySQL。将查询结果写入 Redis。返回结果。更新数据一种常见策略是先更新数据库再删除缓存。更新 MySQL。数据库事务提交成功。删除 Redis 中对应的缓存。后续请求缓存未命中从数据库读取新数据并重建缓存。这种策略的优点是实现简单不需要在每次数据修改时同步计算和写入完整缓存。但它并不天然保证强一致性仍然可能出现并发读写导致的旧数据回填。三、缓存更新策略3.1 先更新数据库再删除缓存这是常见的 Cache Aside 更新方式。优点数据库是权威数据源。缓存删除逻辑简单。不需要主动维护完整的缓存数据副本。风险数据库更新成功但 Redis 删除失败。并发读取可能在缓存删除之后将旧数据重新写入缓存。多实例本地缓存可能没有同步失效。因此需要结合重试、TTL、变更通知和必要的校验机制。3.2 延迟双删基本流程删除缓存。更新数据库。等待一段时间。再次删除缓存。延迟双删的目的是减少某些并发时序下旧数据重新进入缓存的概率。但它不是强一致性方案。例如第二次删除之后仍可能有一个读取请求把旧数据写回缓存。因此延迟时间需要根据实际业务时序确定而且不能仅依赖它保障正确性。3.3 分布式锁可以通过分布式锁协调同一商品的并发缓存重建或更新操作。但必须注意所有相关读写路径都需要遵守一致的协调协议。需要考虑锁超时、持锁进程暂停以及锁释放失败。锁本身不保证数据库与缓存之间的原子性。不能简单地认为加锁就实现了强一致性。对于商品详情等允许短暂不一致的业务通常不需要为所有读取请求引入复杂的全局锁。对于价格和库存等关键业务应在业务操作时执行必要的权威数据校验。四、缓存击穿、穿透与雪崩4.1 缓存击穿定义某个热点 Key 失效后大量并发请求同时访问数据库。解决方案互斥重建仅允许一个请求负责查询数据库并重建缓存。逻辑过期由后台任务刷新数据必要时暂时返回旧数据。热点缓存预热。配合数据库限流与查询超时。互斥重建的基本思路请求查询缓存。缓存未命中后尝试获取重建锁。获取锁成功的请求查询数据库并重建缓存。其他请求等待、重试或返回可接受的旧值。逻辑过期可以减少请求集中回源但可能返回旧数据只适用于业务允许短暂陈旧数据的场景。4.2 缓存穿透定义请求查询的数据根本不存在导致每次请求都访问数据库。解决方案对不存在的数据进行短 TTL 空值缓存。使用布隆过滤器拦截明显不存在的 Key。参数校验。限流与恶意请求防护。需要注意空值缓存也应考虑数据后来被创建的情况不能永久保留不存在的判断。4.3 缓存雪崩定义大量缓存同时过期或缓存集群整体故障导致大量请求集中访问数据库。解决方案对缓存 TTL 增加随机抖动。对热点数据提前预热。控制缓存重建并发。配置数据库限流、熔断和降级。为缓存集群建立容量监控与故障告警。五、大促期间的缓存设计5.1 缓存预热在大促开始前将预计会被大量访问的商品提前加载到 Redis。优点减少活动开始后的缓存未命中。降低数据库瞬时压力。提高热点商品的访问性能。但预热本身也需要控制并发避免大量商品集中加载导致数据库负载过高。5.2 TTL 策略对于热点数据可以采用较长 TTL降低频繁过期导致的数据库访问。不建议在没有配套更新与失效机制的情况下将所有缓存永久保存。可以结合较长 TTL。数据更新后主动删除缓存。变更通知。失败重试。版本校验。定期刷新。5.3 热点 Key 保护对于访问量极高的商品可以采用本地缓存。互斥重建。逻辑过期。热点 Key 监控。网关限流。需要根据业务是否允许旧数据选择返回旧值、等待重建或拒绝请求。六、Redis 故障时的降级与流量保护6.1 为什么不能直接让所有请求访问数据库假设正常情况下每秒有 10,000 个商品详情请求。其中 90% 由 Redis 承担。数据库每秒处理约 1,000 个相关查询。当 Redis 整体不可用时如果所有请求都直接回源数据库可能面对接近每秒 10,000 个查询。这会导致数据库响应变慢、连接池耗尽、应用线程堆积最终演变为整个系统的故障。核心原则缓存不可用不代表数据库必须接管所有请求。6.2 四层保护机制第一层本地缓存Redis 不可用时可以尝试读取应用实例内存中的本地缓存。适合商品描述。图片地址。相对静态的信息。局限各应用实例的数据可能不一致。容量有限。无法天然保障价格等数据的实时性。第二层限流限制缓存故障时访问数据库的请求总量。限流阈值需要通过压测、数据库容量和业务负载评估确定。注意分布式部署时不能只计算单个实例的限流值还需要考虑所有实例的总流量。第三层熔断当数据库查询延迟、超时率或错误率持续升高时暂时停止部分非核心查询。经过一段时间后允许少量探测请求验证数据库是否恢复再逐步放开流量。第四层业务降级根据业务重要性选择降级策略业务内容降级策略商品描述、图片等静态信息尝试使用可接受的旧数据商品推荐暂时关闭浏览量等非核心功能暂停更新或延迟处理商品展示价格使用可信数据或提示暂时无法获取最新价格下单价格与库存校验不得因降级而跳过必要的业务校验商品详情展示可以容忍有限的数据延迟但交易操作必须遵守业务正确性要求。七、多实例本地缓存的一致性7.1 问题背景假设系统有 A、B、C 三个应用实例各自维护本地缓存。商家修改商品价格后A 收到通知删除了本地缓存。B 没有收到通知仍然缓存旧价格。C 没有缓存该商品。由于本地缓存互不共享单纯更新某个实例的缓存不能保证其他实例同步更新。7.2 TTL 兜底为本地缓存设置合理的过期时间。例如设置 30 秒 TTL可以限制旧数据持续存在的时间。但 TTL 不能保证过期之前读到的数据一定是最新的。因此TTL 是兜底机制而不是强一致性保障。7.3 主动失效通知商品更新后通过消息通知各应用实例删除对应的本地缓存。通知可以由业务更新流程或数据库变更监听链路产生。消费者收到通知后可以删除本地缓存由后续请求重新加载数据。相比直接覆盖缓存失效通知通常更容易处理重复事件和数据格式变化。7.4 版本号校验为商品增加单调递增的版本号。例如商品状态价格版本号初始状态100 元10第一次修改80 元11第二次修改60 元12消费者可以根据版本号判断事件是否过期。例如本地缓存版本为 12 时收到版本 11 的事件不能再用旧数据覆盖当前状态。但必须注意版本号本身不会自动保证一致性。如果实例无法获得当前版本信息就不能仅凭本地缓存中的版本号判断数据是否最新。版本检查与缓存更新之间也需要具备适当的原子性或并发控制。八、使用 Binlog MQ 同步本地缓存8.1 整体架构业务更新商品 | v MySQL 提交事务 | v Binlog | v CDC 监听组件 | v MQ 发布商品变更事件 | ---------- | | v v A 实例 B 实例 | | v v 删除或更新各自的本地缓存常见的 Binlog 监听技术包括 Canal、Debezium 等。这种方式可以将数据库变更传播到多个应用实例降低对业务代码主动发送通知的依赖。8.2 为什么选择 Binlog业务代码主动发送消息时可能出现数据库事务提交成功。应用在发送 MQ 消息前发生故障。数据已修改但缓存失效消息没有发出。通过监听数据库变更日志可以从数据变更链路中捕获已提交的修改降低这类遗漏风险。但 Binlog MQ 仍需要考虑监听进度、消息发布可靠性和恢复能力不能认为接入 CDC 就自然实现端到端可靠性。8.3 消费端幂等消息可能重复投递因此消费者需要支持幂等处理。例如删除某个商品的本地缓存通常具有幂等性重复删除不会导致错误状态。如果采用直接更新缓存则需要额外考虑版本号和并发覆盖问题。8.4 消息顺序可以使用商品 ID 作为分区键或顺序键使同一商品的变更事件进入有序处理路径。需要注意并非所有 MQ 默认都保证业务顺序。生产端必须保持正确的事件顺序。顺序保证通常有范围限制。重试、并发消费和故障恢复仍可能影响实际执行顺序。因此顺序消息可以降低乱序风险但不应作为唯一的正确性保障。8.5 版本号防止旧事件覆盖新数据假设E1版本 11价格 80 元。E2版本 12价格 60 元。如果 B 已经处理版本 12随后收到版本 11 的旧事件就不应再次覆盖当前数据。可以在更新逻辑中检查事件版本但必须保证版本检查与实际更新之间不会被并发操作破坏。如果事件只用于删除缓存可以结合版本信息、幂等删除以及后续重新加载策略实现更简单的处理。九、故障恢复与缓存补偿9.1 应用实例重启后为什么需要恢复机制假设 B 实例停机了 10 分钟期间错过了多次商品变更事件。重启后如果继续使用持久化的旧本地缓存或者没有重新加载对应商品的数据就可能继续返回旧价格。因此需要设计明确的缓存恢复流程。9.2 恢复流程隔离或清理不可信的旧缓存。从权威数据源重新加载需要的数据。恢复消息消费或从可靠的事件位点继续消费。对无法补齐的变更进行重新同步。必要时通过版本校验和对账任务发现差异。控制预热并发避免多个实例重启时同时冲击数据库。9.3 全量预热与增量补偿全量或分批预热从数据库重新加载当前有效数据适合缓存规模可控或需要重新建立缓存的场景。增量事件重放从可靠的 Binlog 位点或事件偏移量恢复重放缺失的变更。适合能够准确记录消费进度、并可靠恢复事件的系统。对账与修复定期比较权威数据与缓存相关状态发现不一致后重新加载或删除缓存。这是对通知丢失、消费异常等问题的额外兜底。9.4 预热时需要注意什么预热的目标不仅是填充数据还要确保加载的是当前有效状态。需要考虑预热过程中是否仍有新的商品修改。多个实例是否会同时大量查询数据库。旧数据是否可能覆盖刚加载的新数据。缓存恢复期间是否应该开放全部请求。因此缓存预热应该结合限流、版本校验和增量同步而不是简单地在启动时无条件加载所有商品。十、方案选型与设计边界机制主要作用不能单独解决的问题Cache Aside组织缓存读写流程并发下的所有一致性问题延迟双删降低部分旧值回填概率强一致性TTL限制缓存存活时间TTL 期间的数据陈旧互斥重建防止大量请求同时回源Redis 整体故障逻辑过期减少热点数据失效时的等待返回旧数据的风险本地缓存提供快速读取和应急能力多实例天然一致MQ 通知传播缓存失效或变更事件通知一定送达Binlog CDC捕获数据库变更端到端消息绝对可靠顺序消息降低同一 Key 的事件乱序风险所有故障下的业务状态正确性版本号识别新旧数据防止状态回退无法获取最新版本的情况预热与补偿加快恢复并修复遗漏数据不受控制的恢复流量限流与熔断保护数据库和应用缓存数据一致性本身推荐的组合思路对于允许短暂不一致的商品详情场景可以考虑使用 Cache Aside 组织缓存读写。数据更新后删除 Redis 缓存。使用合理 TTL 作为兜底。使用互斥重建保护热点 Key。使用本地缓存降低 Redis 故障时的访问压力。使用 Binlog MQ 将变更传播到多个应用实例。结合版本号避免旧事件覆盖新状态。使用限流、熔断和业务降级保护数据库。通过事件重放、预热和对账修复故障期间遗漏的数据。对于下单价格、库存等关键数据必须根据业务规则校验权威数据不能仅依赖可能陈旧的缓存。十一、架构评审与面试高频问题1. 为什么通常选择先更新数据库再删除缓存因为数据库是权威数据源删除缓存后可以让后续请求重新加载数据。但这种方式仍然存在并发旧值回填和缓存删除失败等问题需要配合其他机制处理。2. 延迟双删能否保证强一致性不能。它只能降低部分并发时序下的旧值回填风险不能证明所有并发场景都不存在旧数据。3. 缓存击穿、穿透和雪崩有什么区别击穿热点 Key 失效大量请求同时回源。穿透查询不存在的数据每次都访问数据库。雪崩大量 Key 同时失效或缓存系统整体故障导致下游压力骤增。4. Redis 不可用时为什么不能直接访问数据库因为缓存原本承担了大量请求。缓存故障会将这些请求转移到数据库可能引发连接池耗尽、超时和级联故障。应结合本地缓存、限流、熔断和业务降级控制影响范围。5. 本地缓存如何保持多实例一致可以通过主动失效通知、TTL、版本号校验以及定期同步等方式组合实现。必须考虑通知丢失、实例离线和事件乱序等问题。6. Binlog MQ 为什么仍然需要补偿机制因为监听器可能中断、消息发布可能失败、消费者可能离线事件也可能无法及时补齐。可靠的消费位点、事件重放、重新同步和对账修复可以降低遗漏造成的长期影响。7. MQ 保证顺序后为什么还需要版本号顺序保证通常有明确的范围和前提。重试、并发处理及故障恢复可能造成实际执行顺序与业务期望不一致。版本号可以为消费者提供额外的状态回退防护。8. TTL 越短越好吗不是。TTL 越短缓存更新可能越及时但缓存命中率可能降低数据库访问压力也可能增大。TTL 应结合业务允许的数据延迟、访问频率和数据库容量确定。9. 本地缓存为什么不能直接替代 Redis本地缓存容量有限各实例之间也不共享数据难以天然保证一致性。它更适合作为热点加速层或故障时的有限缓冲。10. 商品展示价格与下单价格能否使用相同的一致性策略不一定。展示价格可以根据业务要求容忍有限延迟下单时则需要根据交易规则校验价格及其他关键状态避免因陈旧缓存造成错误交易。十二、场景总结缓存架构设计不仅要考虑正常情况下如何提升查询性能还要考虑并发访问、数据更新、组件故障和系统恢复。需要重点掌握以下原则数据库是权威数据源缓存是性能优化层。缓存一致性通常依赖多种机制组合不能只依赖延迟双删或 TTL。热点缓存失效时应控制并发回源避免数据库过载。Redis 故障时应主动实施限流、熔断和业务降级。多实例本地缓存需要主动同步机制和兜底机制。Binlog MQ 可以传播数据变更但仍需要可靠性、幂等、顺序控制和补偿设计。版本号有助于识别旧事件但只有在版本信息可靠且更新操作得到适当协调时才能有效防止状态回退。缓存恢复需要考虑预热、事件重放、流量控制和对账。对交易正确性有影响的数据必须在业务操作中执行必要的权威数据校验。最终目标不是让缓存永远不出错而是让系统在缓存短暂不一致、缓存整体故障或变更事件丢失时仍然能够控制故障影响、保护数据库并最终恢复到正确状态。