缓存一致性与降级保护实战稿:从系统崩溃到高可用架构

74 阅读14分钟

缓存一致性与降级保护实战稿:从系统崩溃到高可用架构

为什么大厂面试爱问“如何保证缓存一致性?如何做降级保护?”

面试官问这个问题,不是想听“先更新数据库再删除缓存”“用限流保护系统”这些概念,而是想判断:你在高并发场景下,真的遇到过缓存不一致、系统雪崩的问题吗?你能说清楚如何用技术手段保证系统稳定性,以及如何设计降级策略吗?

接下来,我会通过四个真实项目场景,展示我们是如何从“缓存不一致导致业务错误”到“最终一致性保障”,从“系统雪崩崩溃”到“降级保护高可用”的完整过程。每个场景都会包含:当时遇到了什么具体问题、我们是怎么解决的、最终取得了什么效果。


场景 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(比如高可用性/容错能力/优雅降级能力),即使缓存或外部依赖故障,系统仍能稳定运行。”

核心要点:面试官要的不是“你知道双删策略”,而是“你能在高并发场景下,用技术手段保证系统稳定性,还懂缓存一致性、限流降级、熔断保护这些工程实践”。


可视化插图

image.png


image.png 提示:本文讲的是缓存和降级的基础应用。如果你的项目是微服务架构,就需要考虑分布式缓存一致性、服务间限流、全局熔断等问题,这是面试另一高频考点,后面单独拆解。

你项目里用缓存和降级解决过什么稳定性问题?评论区聊聊,下次面试直接用!