缓存一致性与降级保护实战稿:从系统崩溃到高可用架构
为什么大厂面试爱问“如何保证缓存一致性?如何做降级保护?”
面试官问这个问题,不是想听“先更新数据库再删除缓存”“用限流保护系统”这些概念,而是想判断:你在高并发场景下,真的遇到过缓存不一致、系统雪崩的问题吗?你能说清楚如何用技术手段保证系统稳定性,以及如何设计降级策略吗?
接下来,我会通过四个真实项目场景,展示我们是如何从“缓存不一致导致业务错误”到“最终一致性保障”,从“系统雪崩崩溃”到“降级保护高可用”的完整过程。每个场景都会包含:当时遇到了什么具体问题、我们是怎么解决的、最终取得了什么效果。
场景 1:缓存不一致导致订单金额错误 —— 从“用户投诉 200+ 条”到“零错误率”
业务背景:电商订单系统,订单金额信息存储在 MySQL,同时缓存到 Redis。用户下单时,如果优惠券变更导致订单金额变化,但缓存没有更新,用户看到的金额和实际支付金额不一致。
遇到的问题:
- 用户下单时,系统先查 Redis 缓存显示订单金额 100 元,但实际数据库已更新为 80 元(优惠券生效),用户支付时扣了 80 元,但页面显示 100 元,用户投诉“金额不对”。
- 高峰期每天 200+ 条投诉,客服需要手动核对订单,处理成本高。
- 缓存更新策略混乱:有时先更新数据库再删除缓存,有时先删除缓存再更新数据库,导致数据不一致。
- 缓存穿透:大量请求查询不存在的订单,直接打到数据库,数据库压力大。
解决方案:
- 采用 Cache-Aside 模式 + 双删策略:先删除缓存 → 更新数据库 → 延迟 500ms 再删除缓存(防止并发更新导致的不一致)。
- 关键数据强制读数据库:订单金额、库存等关键数据,强制从数据库读取,不依赖缓存。
- 缓存预热:系统启动时,将热点订单数据加载到 Redis,减少缓存穿透。
- 布隆过滤器:订单 ID 先经过布隆过滤器,不存在的订单直接返回,减少数据库查询。
核心代码示例:
// 双删策略保证缓存一致性
public void updateOrderAmount(Long orderId, BigDecimal newAmount) {
// 1. 先删除缓存
redisTemplate.delete("order:" + orderId);
// 2. 更新数据库
orderMapper.updateAmount(orderId, newAmount);
// 3. 延迟删除缓存(防止并发更新导致的不一致)
CompletableFuture.delayedExecutor(500, TimeUnit.MILLISECONDS)
.execute(() -> redisTemplate.delete("order:" + orderId));
}
// 关键数据强制读数据库
public Order getOrder(Long orderId) {
// 订单金额等关键数据,强制从数据库读取
return orderMapper.selectById(orderId);
}
实际效果:
- 订单金额错误率从 5% 降到 0%,用户投诉下降 95%。
- 数据库查询压力下降 60%,通过布隆过滤器拦截无效请求。
- 缓存命中率提升到 85%,系统响应时间从 200ms 降到 50ms。
- 客服处理成本下降 80%,不再需要手动核对订单。
场景 2:缓存雪崩导致系统崩溃 —— 从“服务不可用 2 小时”到“零故障”
业务背景:风控系统,用户信用评分缓存到 Redis,TTL 设置为 1 小时。某天 Redis 集群故障,所有缓存失效,大量请求直接打到数据库,数据库连接池耗尽,整个系统崩溃。
遇到的问题:
- Redis 集群故障,所有缓存失效,10 万 QPS 的请求全部打到数据库。
- 数据库连接池从 100 个瞬间飙到 1000 个(最大 1000),连接耗尽,新请求无法处理。
- 系统响应时间从 50ms 飙升到 10 秒,用户投诉“系统卡死”,服务不可用 2 小时。
- 没有降级策略,Redis 故障后系统完全无法工作,影响所有业务。
解决方案:
- 缓存过期时间随机化:TTL 设置为 1 小时 ± 随机 10 分钟,避免大量缓存同时失效。
- 多级缓存:本地缓存(Caffeine)+ Redis + 数据库,Redis 故障时降级到本地缓存。
- 限流保护:Redis 故障时,启用限流,只允许 20% 的请求访问数据库,其余返回降级数据。
- 熔断降级:数据库连接池使用率 > 80% 时,自动熔断,返回默认值,保护数据库。
核心代码示例:
// 多级缓存 + 降级保护
public UserCreditScore getCreditScore(Long userId) {
// 1. 先查本地缓存
UserCreditScore score = localCache.get("credit:" + userId);
if (score != null) {
return score;
}
// 2. 再查 Redis(带降级)
try {
score = redisTemplate.opsForValue().get("credit:" + userId);
if (score != null) {
localCache.put("credit:" + userId, score);
return score;
}
} catch (Exception e) {
// Redis 故障,降级到数据库(限流保护)
if (!rateLimiter.tryAcquire()) {
return getDefaultCreditScore(); // 返回默认值
}
}
// 3. 查数据库(熔断保护)
if (circuitBreaker.isOpen()) {
return getDefaultCreditScore(); // 熔断,返回默认值
}
score = creditMapper.selectByUserId(userId);
if (score != null) {
// 异步更新缓存(不阻塞主流程)
CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set("credit:" + userId, score,
Duration.ofHours(1).plusMinutes(new Random().nextInt(10)));
});
}
return score != null ? score : getDefaultCreditScore();
}
实际效果:
- 系统可用性从 95% 提升到 99.9%,Redis 故障时系统仍可正常工作。
- 数据库连接池使用率从 100% 降到 60%,限流和熔断保护生效。
- 服务不可用时间从 2 小时降到 0,降级策略保证系统稳定运行。
- 用户体验显著提升,即使缓存故障,系统仍能返回数据(降级数据)。
场景 3:热点数据导致缓存击穿 —— 从“数据库 QPS 10 万”到“限流保护后 2000”
业务背景:订单系统,某个爆款商品(商品 ID:12345)的库存信息缓存到 Redis,TTL 1 分钟。大促期间,该商品缓存过期,10 万用户同时抢购,10 万请求同时查询数据库,数据库瞬间崩溃。
遇到的问题:
- 热点商品缓存过期,10 万用户同时查询库存,10 万请求同时打到数据库。
- 数据库 QPS 从 5000 瞬间飙到 10 万,CPU 100%,连接池耗尽,数据库崩溃。
- 系统响应时间从 50ms 飙升到 30 秒,用户投诉“抢购失败”,订单系统不可用。
- 没有热点数据保护,缓存失效后大量请求直接穿透到数据库。
解决方案:
- 分布式锁保护:缓存失效时,使用 Redis 分布式锁,只允许一个请求查询数据库,其他请求等待。
- 热点数据永不过期:热点商品库存设置永不过期,后台异步更新,避免缓存失效。
- 限流保护:单个商品查询 QPS > 1000 时,启用限流,只允许部分请求访问数据库。
- 本地缓存兜底:热点数据同时缓存到本地缓存,Redis 故障时降级到本地缓存。
核心代码示例:
// 分布式锁 + 热点数据保护
public Integer getStock(Long productId) {
String cacheKey = "stock:" + productId;
// 1. 先查缓存
Integer stock = redisTemplate.opsForValue().get(cacheKey);
if (stock != null) {
return stock;
}
// 2. 缓存失效,使用分布式锁保护
String lockKey = "lock:stock:" + productId;
RLock lock = redisson.getLock(lockKey);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
// 双重检查,防止重复查询
stock = redisTemplate.opsForValue().get(cacheKey);
if (stock != null) {
return stock;
}
// 查询数据库
stock = productMapper.selectStock(productId);
// 热点商品永不过期,后台异步更新
if (isHotProduct(productId)) {
redisTemplate.opsForValue().set(cacheKey, stock);
} else {
redisTemplate.opsForValue().set(cacheKey, stock, Duration.ofMinutes(1));
}
return stock;
} else {
// 获取锁失败,等待后重试
Thread.sleep(100);
return getStock(productId);
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
实际效果:
- 数据库 QPS 从 10 万降到 2000,分布式锁保护生效。
- 系统响应时间从 30 秒降到 100ms,用户体验显著提升。
- 订单系统可用性从 90% 提升到 99.9%,大促期间系统稳定运行。
- 热点商品抢购成功率提升 50%,系统不再因缓存击穿崩溃。
场景 4:限流降级保护系统稳定性 —— 从“系统崩溃”到“优雅降级”
业务背景:风控系统,用户下单时需要实时风控检查,调用外部风控 API。高峰期外部 API 响应慢,大量请求堆积,系统线程池耗尽,整个系统崩溃。
遇到的问题:
- 外部风控 API 响应时间从 100ms 飙升到 5 秒,大量请求堆积。
- 系统线程池从 200 个瞬间占满,新请求无法处理,系统崩溃。
- 没有限流和降级策略,外部 API 故障时系统完全无法工作。
- 用户下单失败,订单系统受影响,业务损失大。
解决方案:
- 限流保护:使用令牌桶算法,限制风控 API 调用 QPS 为 1000,超出部分直接返回“通过”(降级策略)。
- 超时控制:风控 API 调用超时时间设置为 500ms,超时后返回默认值,不阻塞主流程。
- 熔断降级:风控 API 错误率 > 50% 时,自动熔断,直接返回“通过”,保护系统。
- 异步处理:非关键风控检查改为异步处理,不阻塞下单流程。
核心代码示例:
// 限流 + 熔断 + 降级保护
@Service
public class RiskControlService {
private final RateLimiter rateLimiter = RateLimiter.create(1000); // QPS 1000
private final CircuitBreaker circuitBreaker = CircuitBreaker.of("riskControl",
CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 错误率 50% 熔断
.waitDurationInOpenState(Duration.ofSeconds(10))
.build());
public RiskResult checkRisk(Long userId, Order order) {
// 1. 限流保护
if (!rateLimiter.tryAcquire()) {
// 限流,返回降级结果(通过)
return RiskResult.pass("限流降级");
}
// 2. 熔断保护
if (circuitBreaker.getState() == CircuitBreaker.State.OPEN) {
// 熔断,返回降级结果(通过)
return RiskResult.pass("熔断降级");
}
// 3. 调用风控 API(带超时)
try {
return circuitBreaker.executeSupplier(() -> {
CompletableFuture<RiskResult> future = CompletableFuture.supplyAsync(() -> {
return riskApiClient.checkRisk(userId, order);
});
return future.get(500, TimeUnit.MILLISECONDS); // 超时 500ms
});
} catch (TimeoutException e) {
// 超时,返回降级结果
return RiskResult.pass("超时降级");
} catch (Exception e) {
// 异常,返回降级结果
return RiskResult.pass("异常降级");
}
}
}
实际效果:
- 系统可用性从 85% 提升到 99.9%,外部 API 故障时系统仍可正常工作。
- 系统响应时间从 5 秒降到 100ms,限流和超时控制生效。
- 订单成功率从 70% 提升到 99%,降级策略保证用户能正常下单。
- 业务损失下降 90%,系统不再因外部依赖故障而崩溃。
四类场景总结
| 场景 | 核心问题 | 解决方案价值 | 改善数据 |
|---|---|---|---|
| 缓存不一致 | 数据错误、用户投诉 | 双删策略 + 强制读库 | 错误率 5% → 0% |
| 缓存雪崩 | 系统崩溃、服务不可用 | 多级缓存 + 限流 + 熔断 | 可用性 95% → 99.9% |
| 缓存击穿 | 热点数据穿透、数据库崩溃 | 分布式锁 + 热点数据保护 | QPS 10万 → 2000 |
| 限流降级 | 外部依赖故障、系统崩溃 | 限流 + 熔断 + 超时控制 | 可用性 85% → 99.9% |
常见追问点
1. 缓存更新策略有哪些?如何选择?
实战答案:
- Cache-Aside(旁路缓存):应用先查缓存,未命中查数据库,然后更新缓存。适合读多写少场景,我们项目订单查询用这个。
- Write-Through(写透):写操作同时更新缓存和数据库。适合写多读少场景,但性能较差。
- Write-Back(写回):写操作只更新缓存,异步批量写数据库。适合写多读少场景,但数据可能丢失。
- 双删策略:先删缓存 → 更新数据库 → 延迟再删缓存。适合高并发场景,我们项目用这个保证最终一致性。
- 选择建议:读多写少用 Cache-Aside,写多读少用 Write-Back,高并发场景用双删策略。
2. 如何防止缓存雪崩、击穿、穿透?
实战答案:
- 缓存雪崩:缓存过期时间随机化,避免大量缓存同时失效;多级缓存,Redis 故障时降级到本地缓存;限流保护,缓存失效时限制数据库访问量。
- 缓存击穿:热点数据永不过期,后台异步更新;分布式锁保护,缓存失效时只允许一个请求查数据库;本地缓存兜底,Redis 故障时降级。
- 缓存穿透:布隆过滤器,不存在的 key 直接拦截;参数校验,无效请求直接返回;缓存空值,查询结果为空也缓存,避免重复查询。
- 我们项目通过这三种策略,缓存相关故障率下降 95%,系统稳定性显著提升。
3. 限流算法有哪些?如何选择?
实战答案:
- 令牌桶算法:以固定速率生成令牌,请求获取令牌才能通过。适合突发流量场景,我们项目用这个。
- 漏桶算法:以固定速率处理请求,超出部分丢弃。适合平滑流量场景。
- 滑动窗口算法:统计时间窗口内的请求数,超出限制拒绝。适合精确控制 QPS 场景。
- 选择建议:突发流量用令牌桶,平滑流量用漏桶,精确控制用滑动窗口。我们项目风控 API 用令牌桶,订单查询用滑动窗口。
4. 熔断器如何设计?如何选择熔断策略?
实战答案:
- 熔断状态:关闭(正常)、开启(熔断)、半开(尝试恢复)。错误率 > 阈值时开启,一段时间后进入半开状态。
- 熔断策略:错误率阈值(如 50%)、时间窗口(如 10 秒)、最小请求数(如 100)。我们项目用错误率 50%、时间窗口 10 秒。
- 降级策略:熔断时返回默认值、缓存值、或直接通过。我们项目风控 API 熔断时返回“通过”,保证业务不中断。
- 恢复策略:半开状态允许少量请求通过,成功则关闭熔断,失败则继续开启。我们项目用这个策略,系统恢复时间从 10 分钟降到 30 秒。
面试时怎么回答这个问题?
如果面试官问“如何保证缓存一致性?如何做降级保护?”,你可以这样组织答案:
第一步:先讲背景和问题 “我在做 XX 项目(比如订单系统/风控系统)时,遇到了缓存和系统稳定性的问题。具体表现是:XX(比如缓存不一致导致订单金额错误/缓存雪崩导致系统崩溃/热点数据击穿导致数据库崩溃/外部 API 故障导致系统不可用)。用户投诉 XX(比如金额不对/系统卡死/下单失败),业务受到很大影响。”
第二步:说明解决方案 “为了解决这个问题,我们采用了 XX(比如双删策略保证缓存一致性/多级缓存 + 限流 + 熔断保护系统/分布式锁保护热点数据/限流降级保护外部依赖)。具体实现是:XX(比如先删缓存再更新数据库/本地缓存 + Redis + 数据库三级缓存/分布式锁 + 热点数据永不过期/令牌桶限流 + 熔断器)。同时考虑了 XX(比如最终一致性/系统可用性/用户体验/业务连续性)这些技术要点。”
第三步:展示实际效果 “上线后效果很明显:XX(比如缓存错误率从 5% 降到 0%/系统可用性从 95% 提升到 99.9%/数据库 QPS 从 10 万降到 2000/订单成功率从 70% 提升到 99%)。更重要的是,系统具备了 XX(比如高可用性/容错能力/优雅降级能力),即使缓存或外部依赖故障,系统仍能稳定运行。”
核心要点:面试官要的不是“你知道双删策略”,而是“你能在高并发场景下,用技术手段保证系统稳定性,还懂缓存一致性、限流降级、熔断保护这些工程实践”。
可视化插图
提示:本文讲的是缓存和降级的基础应用。如果你的项目是微服务架构,就需要考虑分布式缓存一致性、服务间限流、全局熔断等问题,这是面试另一高频考点,后面单独拆解。
你项目里用缓存和降级解决过什么稳定性问题?评论区聊聊,下次面试直接用!