分布式系统中缓存雪崩的解决方案
前言
在中大型项目中经常会使用到 Redis,当 Redis 缓存服务(一主多从 + 哨兵架构或者集群架构)不可用时,所有请求会直接打到 MySQL,瞬间把数据库压垮(即缓存雪崩),导致整个系统不可用。那么如何设计系统架构,即使 Redis 缓存服务完全不可用,整个系统也可以正常运行(不至于完全挂掉)呢?
缓存雪崩的概念
- 缓存雪崩是指在设置缓存时采用了相同的过期时间,甚至缓存中间件挂掉,导致大量缓存在某一时刻同时失效,外部请求全部转发到数据库,而数据库由于瞬时压力过重导致雪崩。
- 简单的解决方案有以下几种:
- (1) 可以在原有的缓存过期时间基础上增加一个随机值,比如 1 ~ 5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,进而很难引发缓存集体失效的事件。
- (2) 使用本地缓存(Caffeine)+ Redis 哨兵 / 集群 + Hystrix 熔断 / 降级 / 限流
- 其中本地缓存(Caffeine)作为一级缓存兜底;
- Redis 哨兵 / 集群保证高可用,并设置 Key 错峰过期;
- Hystrix 在 Redis 服务异常时触发熔断,并降级到返回本地缓存(Caffeine)数据或默认数据,从而防止缓存雪崩导致大量请求涌入冲跨数据库。

缓存雪崩的解决
不依赖单一防线,而是用「网关限流 → 应用熔断 / 熔断 / 本地缓存 → 请求合并 → 后端拒绝」多层保护,同时提升 Redis 的 HA(高可用)能力,才能有效避免 Redis 故障时的缓存雪崩把 MySQL 打垮。
整体的防护思路
- 事前降级:
- 在缓存不可用时允许返回 “可接受的弱一致性或降级结果”(例如:部分功能只返回缓存数据、或返回静态提示页)。
- 保护后端(削峰限流):
- 对外限流、熔断、队列化,防止大量并发请求落到数据库。
- 多级缓存 + 回退策略:
- 本地缓存(LRU) + 远端 Redis + 数据库回源。
- 请求合并:
- 避免大量相同 Key 的并发请求回源到数据库。
- 提高 Redis 可用性:
- 部署多副本、Sentinel/Cluster、跨可用区和异地多活(长期方案)。
- 可观测性 + 自动化恢复:
- 指标告警(QPS、错误率、数据库连接数、Redis 失败率)、并配合自动限流 / 熔断规则。
具体方案与可选框架
(1) 边缘 / 网关限流(第一道防线)
- 目的:
- 在流量激增或缓存不可用时,尽早拒绝 / 排队请求,保护后端。
- 常用方案:
- API 网关:Kong、Nginx + Lua(OpenResty)、Envoy、Traefik。
- CDN / 边缘:使用 CDN 缓存静态或部分接口响应结果(可作为快速降级)。
- 实现:
- Nginx + Lua(OpenResty)实现令牌桶 / 漏桶限流算法,非常高效,放在 LB(负载均衡)层。
- Envoy 支持熔断机制(Circuit-Breaking) + 全局限流器(Global Rate-Limiter),适合微服务网格。
- 目的:
(2) 服务端限流与令牌桶(应用层)
- 目的:
- 对热点接口进行精细限流,允许重要请求先行。
- 常用库:
- Java:Bucket4j、Guava RateLimiter、Resilience4j Rate Limiter、Sentinel Rate Limiter、Spring Cloud Gateway Rate Limiter。
- Go:Ratelimit, Uber/Ratelimit。
- 要点:
- 支持按用户 / 按 key / 按接口限流。
- 与下游熔断器联动:当 Redis 故障探测到上升,网关自动降低阈值。
- 目的:
(3) 熔断 / 降级(保护数据库)
- 目的:
- 当 Redis / 数据库或下游服务错误率(异常、超时等)上升时,自动短路请求并返回降级结果或缓存的旧数据。
- 常用库:
- Sentinel(阿里开源):支持熔断、降级、限流、系统保护,尤其适合高并发微服务场景,可与 Spring Cloud Alibaba 集成。
- Resilience4j(推荐,替代 Hystrix),支持断路器、舱壁隔离、重试、限流等。
- Spring Cloud Circuit Breaker(抽象层,可对接 Resilience4j)。
- Hystrix(已逐步退役 / 不推荐新项目使用)。
- 策略:
- 熔断器触发条件:比如,10 秒内,请求数达到阈值 20,且错误率 >= 50%。
- 熔断后:返回降级结果(例如默认值 / 本地缓存数据 / 静态页面),同时异步收集重试 / 探测请求。
- 目的:
(4) 多级缓存(本地 + 远端)
- 目的:
- Redis 故障时,使用本地近端缓存(Caffeine)降低数据库回源率;
- 利用 TTL + stale-while-revalidate 策略保证可用。
- stale-while-revalidate 策略允许在缓存过期后,先从本地缓存(Caffeine)返回旧数据保证系统可用性,同时后台异步刷新最新数据写回本地缓存和 Redis,从而避免请求阻塞和缓存雪崩。
- 实现:
- 应用内 LRU(本地缓存):Caffeine、Guava Cache。
- 远端缓存:Redis Cluster(Lettuce / Redisson 客户端)。
- 读写策略:
- 读取流程:本地缓存 -> Redis -> DB;
- 写入流程:DB -> Redis -> 本地缓存。
- 失效策略:
- 当 Redis 不可用时,从本地缓存(Caffeine)返回 “过期但可接受的旧数据”,并异步刷新最新数据写回本地缓存和 Redis。
- 对于强一致性场景需要谨慎考虑,必须有业务允许范围。
- 目的:
(5) 请求合并(防止大量请求回源数据库)
- 目的:
- 对同一 key 的高并发请求只允许一个请求回源到数据库,其余请求等待或返回本地缓存数据。
- 实现:
- Java 自行实现,或者借助 Guava 的
LoadingCache、Caffeine 的AsyncLoadingCache+ 互斥锁。
- Java 自行实现,或者借助 Guava 的
- 目的:
(6) 后端拒绝(数据库层保护)
- 目的:
- 限制数据库的并发连接数 / QPS,保证数据库不会被压垮,必要时拒绝请求或请求排队。
- 实现:
- 数据库代理中间件:ProxySQL、MySQL Router,用于数据库连接限流、优先级控制。
- 应用层的连接池设置:合理设置数据库的最大连接数、超时、最大等待队列。
- 在应用层使用熔断 / 限流机制,且当 DB 连接池耗尽时直接快速失败(不阻塞调用线程)。
- 目的:
(7) 健康检查 + 自动切换(提高 Redis 可用性)
- 目的:
- 及时检测 Redis 故障并自动降级到备用逻辑(或使用备用缓存)。
- 实现:
- 短期方案:Redis Sentinel(一主多从)或者 Redis Cluster(多主多从、分片存储),支持自动故障转移。
- 长期方案:跨可用区 / 跨区域多活。
- 使用客户端(Lettuce / Redisson)配置连接超时、重试与负载均衡策略。
- 目的:
具体可落地的架构与实现
提示
本节将给出一个典型的 Java 微服务防护栈(分层)与核心代码示例。
整体业务流程(简化)
- Client -> API Gateway(Nginx + Lua、Envoy 限流) -> Service A
- Service A 读取缓存:本地缓存(Caffeine)-> Redis -> DB。
- 系统保护组件:Resilience4j(熔断 + 舱壁隔离)、Bucket4j(限流)、Singleflight(请求合并)、异步队列(重建缓存)
边缘 / 网关限流(基于 Nginx + Lua)
限流实现方式
- 在 Nginx 层使用
lua-resty-limit-traffic模块 - 或者自定义实现令牌桶(Token-Bucket)算法
- 在 Nginx 层使用
限流粒度
- 按 IP 地址
- 按用户 ID
- 按 API Key(通常代表不同的调用者或应用)
动态调整
- 通过健康探测检测后端 Redis 状态
- 当 Redis 健康下降时,动态降低令牌桶的速率
实现目的
- 控制请求流量,防止后端过载
- 提高系统可用性和稳定性
SpringBoot + Resilience4j + Caffeine + Lettuce(伪代码)
- 实现要点
readFromRedisOrDB的 Redis 读需要设置短超时(比如 50 ~ 200ms),避免阻塞线程池。- Resilience4j 的 CB 配置要与业务 QPS 级别匹配:最小请求数、错误率阈值、滚动窗口大小等。
- Singleflight(请求合并)可以避免短时间内 N 个相同 Key 的请求并发回源到数据库。
1 | // 1. 本地缓存(Caffeine) |
最佳实践(落地顺序建议)
短期(1 ~ 2 周)
- 在网关层加全局限流(简单的令牌桶),保护数据库。
- 在应用内增加本地缓存(Caffeine),并使用
stale-while-revalidate策略。 - 给 Redis 客户端设置短超时 + 快速失败策略。
- 在关键读取接口实现 SingleFlight(请求合并)。
- 配置 Resilience4j 的熔断与降级(Fallback)机制。
中期(1 ~ 3 个月)
- 精细化限流(按接口 / 用户 / 业务),使用 Bucket4j 或 Envoy Rate-Limit。
- 部署数据库代理中间件(比如 ProxySQL),实现数据库连接限流与优先级控制。
- 增加监控 / 告警(Redis 执行命令的错误率、数据库连接数、服务断路器的开启和关闭状态)。
长期(3 个月以上)
- 优化 Redis 高可用架构(Sentinel 、Cluster + 跨可用区副本、多活 / 读写分离)。
- 建立灰度降级策略与用户体验方案(部分用户可以看到降级内容)。
- 容灾演练:模拟 Redis 故障,并验证熔断 / 降级 / 降级链路是否按预期工作。
推荐使用技术的组合
边缘 / 网关限流:
- Nginx + Lua(OpenResty)或 Envoy(支持熔断和限流)
Spring Cloud 微服务:
- 本地缓存:Caffeine
- 熔断 / 降级 / 限流:Resilience4j + Spring Cloud Circuit Breaker
- 请求合并:Caffeine AsyncLoading / 自行实现 SingleFlight
- Redis 客户端:Lettuce / Redisson(配置短超时)
- API 网关:Spring Cloud Gateway + Redis 限流,或者 Envoy(支持熔断和限流)
- 精细限流:Bucket4j,或者 Hazelcast / Redis 限流)
数据库保护:
- ProxySQL / MySQL Router 做数据库连接管理与慢查询保护
常见误区与注意事项
不要把分布式锁依赖在 Redis 的可用性上:
- 当 Redis 不可用时分布式锁会失效,不能用分布式锁作为保护数据库的唯一手段。
- 在应用内使用 SingleFlight(请求合并)更可靠,用于热点请求合并。
熔断参数太松或太紧都会有问题:
- 需结合真实流量做压测与迭代调整。
本地缓存生存期太久会造成一致性问题:
- 对于强一致性场景,务必权衡本地缓存的有效时间(或在写路径强制失效 / 异步同步)。
确保快速失败:
- 使用快速失败机制,不等待、不重试、不排队。
- 一旦线程池 / 连接池资源耗尽,会导致系统级雪崩,应避免使用无限等待。
多级缓存的读写流程
在多级缓存(本地缓存 → Redis → 数据库)架构中,读取和更新操作的流程有所不同。下面将简单梳理一下常见模式,包含缓存命中、缓存未命中、缓存更新策略以及缓存异步刷新机制。
读取流程
读取流程(Read)
- (1) 访问本地缓存(Caffeine)
- 如果缓存命中 → 直接返回数据(延迟最低)。
- 如果缓存未命中 → 继续访问 Redis。
- (2) 访问 Redis(远程缓存)
- 如果缓存命中 → 写回本地缓存(Caffeine) → 返回数据。
- 如果缓存命中 → 继续访问数据库。
- (3) 访问数据库
- 获取数据库最新的数据 → 写回 Redis → 写回本地缓存(Caffeine) → 返回数据。
- 注意,这里先写 Redis,后写本地缓存(Caffeine);优先保证全局缓存一致性,最后加速本节点访问。
- (1) 访问本地缓存(Caffeine)
可选优化(
stale-while-revalidate策略)- 当本地缓存(Caffeine)或 Redis 缓存的数据已过期:
- 先返回过期数据保证系统可用;
- 在后台异步刷新缓存数据,即从数据库加载最新数据,然后写回本地缓存(Caffeine)和 Redis。
- 当本地缓存(Caffeine)或 Redis 缓存的数据已过期:
工作流程图
![]()
更新流程
多级缓存的更新通常有三种策略:写穿(Write-through)、写回(Write-behind)、缓存失效(Cache-Aside)。
写穿策略(Write-through)
- 概述:
- 更新数据库的同时立即更新缓存。
- 流程:
![]()
- 优点:
- 缓存与数据库保持一致。
- 缺点:
- 写操作延迟增加。
- 概述:
写回策略(Write-behind)
- 概述:
- 先写缓存,数据库异步刷新。
- 流程:
![]()
- 优点:
- 写入延迟低,适合高吞吐写场景。
- 缺点:
- 短时间内缓存与数据库可能不一致,需要保证异步刷新数据库的可靠性。
- 概述:
失效策略(Cache-Aside)
- 概述:
- 更新数据库后,主动删除缓存,下一次读取数据时重建缓存。
- 流程:
![]()
- 优点:简单、保证最终一致性。
- 缺点:第一次访问会有缓存未命中,可能带来突发的数据库压力,建议使用分布式锁进行控制。
- 概述:
最佳实践组合
读取操作:
- 本地缓存 + Redis + 数据库回源
- 可选结合
stale-while-revalidate策略提升性能和可用性。
更新操作:
- 读多写少的场景 → 缓存失效策略(Cache-Aside)
- 高并发写的场景 → 写回策略(Write-behind)
- 对一致性要求高的场景 → 写穿策略(Write-through)
方案总结对比
| 操作 | 流程概述 | 优点 | 缺点 |
|---|---|---|---|
| 读取 | 读本地缓存 → 读 Redis → 读数据库 | 延迟低,高可用 | 复杂性高,需要多级缓存同步 |
| 写穿策略 | 写数据库 → 写 Redis → 写本地缓存 | 数据一致性高 | 写延迟高 |
| 写回策略 | 写本地缓存 → 写 Redis → 异步写数据库 | 写性能高 | 数据短期不一致,需要保证异步写数据库的可靠性 |
| 失效策略 | 写数据库 → 删除 Redis → 删除本地缓存 | 简单,最终一致性 | 大量缓存突发未命中时,数据库压力大 |




