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%),熔断器将自动打开,拒绝后续请求直到恢复。
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 策略执行流程
- 读请求: 查询缓存 → 命中则直接返回;未命中则查询数据库 → 回填缓存 → 返回结果。
- 写请求: 更新数据库 → 删除缓存(或更新缓存,但推荐删除以简化并发控制)。
- 并发问题: 线程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 策略执行流程
- 写请求: 直接更新缓存(内存或Redis)→ 返回成功。
- 异步持久化: 后台线程或消息队列定期将缓存中的变更批量写入数据库。
- 风险点: 缓存节点宕机或进程崩溃,未持久化的数据永久丢失。
// 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());
}
}
2.4 策略选择与熔断降级的关联
不同的缓存更新策略,在熔断降级场景下的表现差异显著:
- Cache Aside: 当Redis超时或删除缓存失败时,业务层应捕获异常并降级为直接查库,避免因缓存故障阻塞主流程。
- Write Behind: 若异步刷DB队列积压或Redis宕机,缓存数据丢失将导致业务数据不一致,此时应触发熔断并暂停写入,等待人工修复。
- Read Through: 缓存组件故障可能导致所有读请求失败,需配置本地缓存兜底或直接降级到数据库。
最佳实践: 在微服务中推荐以 Cache Aside 为主策略,配合 本地缓存(Caffeine) 作为二级缓存,并设置合理的熔断阈值(如Redis超时率 > 20% 时熔断缓存操作)。同时,对缓存删除操作增加重试机制和监控告警,避免因缓存删除失败导致的数据不一致问题。