2. 缓存基础回顾:本地缓存与分布式缓存(Redis)原理、缓存穿透/击穿/雪崩、缓存更新策略

2.1 缓存体系概述

在微服务架构中,缓存是提升系统吞吐量、降低数据库压力的核心手段。根据数据存储位置与访问方式,缓存主要分为本地缓存与分布式缓存两类。理解两者的原理与适用场景,是排查缓存一致性故障的前提。

对比维度 本地缓存 分布式缓存(Redis)
存储位置 进程内堆内存(JVM / 进程内存) 独立缓存服务节点(跨进程)
访问延迟 纳秒级(无网络开销) 微秒~毫秒级(网络RTT)
容量上限 受限于单机堆内存(通常GB级) 可横向扩展至TB级
数据一致性 各实例独立,易出现不一致 集中存储,一致性由中间件保证
典型实现 Caffeine、Guava Cache、Ehcache Redis Cluster、Redis Sentinel
适用场景 配置信息、热点数据、单机只读缓存 全局共享数据、分布式锁、会话状态
故障排查提示: 本地缓存与Redis混用时,若未设置合理的过期时间或失效通知机制,极易出现“本地缓存与Redis数据不一致”的故障,这是熔断降级触发的重要诱因之一。

2.2 缓存三大经典问题:穿透、击穿、雪崩

这三个问题是缓存系统设计时必须防御的核心风险,也是触发熔断降级的常见根因。

2.2.1 缓存穿透

定义: 请求查询一个数据库中也不存在的数据,导致每次请求都穿透缓存直达数据库。

  • 危害: 大量无效请求压垮数据库,引发服务雪崩。
  • 解决方案:
    • 缓存空对象(设置短TTL,如30~60秒)
    • 布隆过滤器(Bloom Filter)前置拦截
    • 参数校验与非法请求拦截

2.2.2 缓存击穿

定义: 某个热点key在缓存失效的瞬间,大量并发请求同时涌入数据库重建缓存。

  • 危害: 单点热点key失效导致数据库瞬时高负载。
  • 解决方案:
    • 互斥锁(Mutex Lock)——只允许一个线程去数据库查询并重建缓存
    • 逻辑过期(不设置物理TTL,后台异步更新)
    • 热点数据永不过期 + 主动更新

2.2.3 缓存雪崩

定义: 大量缓存key在同一时间集中失效,或缓存节点宕机,导致所有请求直接打到数据库。

  • 危害: 数据库瞬间被击穿,引发系统级熔断。
  • 解决方案:
    • TTL添加随机偏移(如基础TTL + 随机1~5分钟)
    • 缓存集群高可用(Redis Sentinel / Cluster)
    • 本地缓存 + 分布式缓存二级缓存兜底
    • 限流降级(提前熔断保护数据库)
熔断关联: 缓存雪崩是触发微服务熔断降级最常见的场景之一。当数据库连接池被打满,上游服务调用超时率达到阈值(如50%),熔断器将自动打开,拒绝后续请求直到恢复。
缓存穿透、击穿、雪崩对比图 缓存三大经典问题对比 缓存穿透 数据不存在于缓存和数据库 每次请求都直达数据库 解决:空对象缓存 / 布隆过滤器 缓存击穿 热点key失效瞬间 大量并发请求重建缓存 解决:互斥锁 / 逻辑过期 缓存雪崩 大量key同时失效 或缓存节点宕机 解决:TTL随机化 / 高可用集群 数据库 数据库 数据库 熔断降级触发条件 数据库连接池打满 / 超时率达到阈值 说明:三种问题均可能导致数据库过载,进而触发熔断降级保护

2.3 缓存更新策略

缓存与数据库之间的数据一致性,取决于所采用的更新策略。微服务架构中常见的三种策略对比如下:

策略 核心原理 一致性保证 适用场景 潜在风险
Cache Aside 读:先查缓存,miss则查DB并回填缓存
写:先更新DB,再删除缓存
最终一致性(存在短暂不一致窗口) 读多写少,对一致性要求不极端 并发写时可能出现脏读(删除缓存与更新DB时序问题)
Read Through 缓存层封装读逻辑,miss时由缓存组件自动加载DB数据 最终一致性 希望简化业务代码,统一缓存加载逻辑 缓存组件故障可能导致读请求失败
Write Behind 写操作只更新缓存,异步批量写入DB 弱一致性(可能丢失数据) 写吞吐量极高,可容忍数据丢失(如日志、计数) 宕机时缓存数据未持久化到DB,数据丢失风险
故障排查要点: 在微服务熔断降级场景中,Cache Aside 是最常用的策略。但若删除缓存失败(如Redis超时),会导致缓存与数据库长期不一致。此时应结合重试机制或监听Binlog异步补偿,避免因缓存不一致引发业务层熔断。

2.3.1 Cache Aside 策略执行流程

  1. 读请求: 查询缓存 → 命中则直接返回;未命中则查询数据库 → 回填缓存 → 返回结果。
  2. 写请求: 更新数据库 → 删除缓存(或更新缓存,但推荐删除以简化并发控制)。
  3. 并发问题: 线程A更新DB后删除缓存,线程B在删除前读取旧缓存,导致短暂不一致。
// Cache Aside 伪代码示例
public User getUser(String userId) {
    // 1. 查缓存
    User user = redis.get("user:" + userId);
    if (user != null) {
        return user;
    }
    // 2. 缓存未命中,查数据库
    user = userDao.selectById(userId);
    if (user != null) {
        // 3. 回填缓存,设置TTL
        redis.setex("user:" + userId, 3600, user);
    }
    return user;
}

public void updateUser(User user) {
    // 1. 更新数据库
    userDao.updateById(user);
    // 2. 删除缓存(下次读取时重建)
    redis.del("user:" + user.getId());
}

2.3.2 Write Behind 策略执行流程

  1. 写请求: 直接更新缓存(内存或Redis)→ 返回成功。
  2. 异步持久化: 后台线程或消息队列定期将缓存中的变更批量写入数据库。
  3. 风险点: 缓存节点宕机或进程崩溃,未持久化的数据永久丢失。
// Write Behind 伪代码示例(简化)
public void writeUser(User user) {
    // 1. 更新缓存
    cache.put("user:" + user.getId(), user);
    // 2. 将变更事件写入队列,异步批量刷DB
    writeBehindQueue.add(new UserUpdateEvent(user));
}

// 后台线程定期执行
public void flushToDatabase() {
    List<UserUpdateEvent> batch = writeBehindQueue.drain(100);
    for (UserUpdateEvent event : batch) {
        userDao.updateById(event.getUser());
    }
}
三种缓存更新策略对比 三种缓存更新策略对比 Cache Aside 读:缓存→DB→回填 写:更新DB→删除缓存 一致性:最终一致 Read Through 读:缓存组件自动加载 写:通常配合Cache Aside 一致性:最终一致 Write Behind 写:更新缓存→异步刷DB 读:直接从缓存读取 一致性:弱一致 数据库 数据库 数据库 缓存 缓存 缓存 异步 说明:Cache Aside 是微服务中最常用的策略,Write Behind 适用于高吞吐但可容忍数据丢失的场景 Read Through 通常作为 Cache Aside 的封装,由缓存中间件自动处理回填逻辑

2.4 策略选择与熔断降级的关联

不同的缓存更新策略,在熔断降级场景下的表现差异显著:

  • Cache Aside: 当Redis超时或删除缓存失败时,业务层应捕获异常并降级为直接查库,避免因缓存故障阻塞主流程。
  • Write Behind: 若异步刷DB队列积压或Redis宕机,缓存数据丢失将导致业务数据不一致,此时应触发熔断并暂停写入,等待人工修复。
  • Read Through: 缓存组件故障可能导致所有读请求失败,需配置本地缓存兜底或直接降级到数据库。
最佳实践: 在微服务中推荐以 Cache Aside 为主策略,配合 本地缓存(Caffeine) 作为二级缓存,并设置合理的熔断阈值(如Redis超时率 > 20% 时熔断缓存操作)。同时,对缓存删除操作增加重试机制和监控告警,避免因缓存删除失败导致的数据不一致问题。