{T}

分布式缓存与一致性

缓存是提升系统性能最常用也最易用错的手段。本文聚焦两大核心问题:缓存带来的三大难题(穿透、击穿、雪崩)以及缓存与数据库的数据一致性。掌握这两点,才能在生产环境安全地使用缓存。

一、缓存的价值与风险

1.1 为什么用缓存

  • 数据库读多写少,热点数据命中缓存可显著降低数据库压力。
  • Redis 等缓存读写性能(微秒级)远高于数据库(毫秒级)。
  • 将热点数据、查询结果、会话、配置放入缓存,缩短响应时间。

1.2 缓存的代价

引入缓存意味着多了一份数据副本,随之而来的是:

  • 一致性:缓存与数据库可能不一致。
  • 三大难题:穿透、击穿、雪崩。
  • 运维复杂度:缓存部署、容量、过期策略。

二、缓存三大难题

2.1 缓存穿透

现象:查询一个数据库中不存在的数据,缓存里也没有(因为没存),请求每次都打到数据库。恶意攻击者可构造大量不存在的 key 压垮数据库。

解决

  1. 缓存空值:对不存在的 key 也缓存一个空值(设置较短过期时间,如 5 分钟),避免穿透。
  2. 布隆过滤器(Bloom Filter):在缓存前加一层布隆过滤器,对不存在的数据直接拦截(有极小误判率,但不会漏判存在的数据)。
  3. 参数校验:对明显非法/越界的参数直接拒绝。

2.2 缓存击穿

现象:某个热点 key 过期的瞬间,大量并发请求同时打到数据库,导致数据库压力瞬间飙升。

解决

  1. 互斥锁(mutex):key 过期后,只有一个线程去查库并回填缓存,其余线程等待。
  2. 逻辑过期:缓存数据不过期(物理上设一个较长的 TTL),但数据里存一个逻辑过期时间;读取时若发现逻辑过期,则异步重建缓存并返回旧值,避免击穿。
  3. 热点数据不过期:对超高热点数据不设置过期时间,由后台定时任务主动更新。

2.3 缓存雪崩

现象大量 key 在同一时间集中过期(或缓存节点宕机),请求全部打到数据库,导致数据库被压垮,进而引发系统雪崩。

解决

  1. 过期时间加随机:给每个 key 的过期时间加一个随机值(如 1~5 分钟),避免同一时间集中过期。
  2. 多级缓存:本地缓存(Caffeine)+ Redis 二级缓存,降低对 Redis/DB 的直连压力。
  3. 缓存高可用:Redis 集群(主从 + 哨兵 / Cluster),避免单点宕机。
  4. 限流降级:数据库层面加限流、熔断,兜底保护。

三、缓存与数据库的一致性

3.1 为什么会产生不一致

缓存是数据库的副本。写操作如果处理不当,缓存与数据库就会不一致。不一致的根本原因是:缓存和数据库是两个独立的存储,无法在一个原子操作里同时更新

3.2 缓存更新策略

策略一:Cache Aside(旁路缓存)——最常用

读流程:读缓存 → 命中则返回;未命中则查库、回填缓存、返回。 写流程:先更新数据库,再删除缓存(或更新缓存)。

text
读:get(key) -> 缓存命中? -> 是返回
                 -> 否: 查DB -> 写缓存 -> 返回
写:update(DB) -> 删除缓存(或更新缓存)

为什么"更新数据库后删缓存"而不是"更新缓存"

  • 若先更新数据库再更新缓存,两个操作非原子,中间失败会导致缓存是旧值。
  • 删除缓存后,下次读会查库重建,天然最终一致。
  • 更新缓存比删除缓存更容易出现并发覆盖旧值。

策略二:延迟双删(Delay Double Delete)

在高并发下,"先更新 DB 再删缓存"仍可能出现短暂不一致(删除缓存前有读请求把旧值写回缓存)。延迟双删通过"删除缓存 → 更新 DB → 延迟再次删除缓存"来消除:

text
1. 删除缓存 key
2. 更新数据库
3. 延迟 N ms(如 500ms)后再次删除缓存 key

延迟的目的是等待可能读到的旧值(在删除和更新之间的间隙)过期,第二次删除彻底清掉旧值。

3.3 最终一致性的本质

缓存的一致性追求的是最终一致(缓存数据最终会与数据库一致),而非强一致。因为:

  • 强一致需要在每次读写都加分布式锁/串行化,性能损失极大。
  • 绝大多数场景(读多写少)下,短暂的缓存不一致(毫秒~秒级)是可接受的。

适用强一致的场景(资金、库存关键数据)应避免依赖缓存,或采用严格的缓存失效 + 双删 + 消息补偿。

3.4 消息队列最终一致性

在更复杂的场景(多个服务、多个库),可通过消息队列保证最终一致:写操作后发一条消息(携带最新数据),消费端异步更新缓存。配合消息重试 + 幂等,即使中间失败也能最终对齐。

四、最佳实践

  1. 缓存 key 设计:带业务前缀 + 版本号,避免不同业务/版本冲突。
  2. 过期时间:所有 key 都要有过期时间,并加随机抖动,防止雪崩。
  3. 热点保护:对热点 key 做本地缓存兜底、逻辑过期、互斥锁。
  4. 缓存不可用时降级:缓存读失败要降级到数据库,而不是抛异常导致整个请求失败。
  5. 监控:监控缓存命中率、穿透/击穿/雪崩指标,及时发现异常。

小结

  • 缓存三大难题:穿透(查不存在的数据,用缓存空值/布隆过滤器)、击穿(热点 key 过期,用互斥锁/逻辑过期)、雪崩(大量 key 集中过期,用随机 TTL/多级缓存)。
  • 一致性主流方案:Cache Aside(旁路缓存)+ 延迟双删,追求最终一致
  • 缓存与数据库无法原子更新,最终一致性是权衡性能后的正确选择。

版本差异(技术原理说明)

维度说明
技术原理分布式一致性/事务/锁/ID 生成等原理与具体版本无关,长期有效
落地选型新项目建议优先使用 Nacos/Redis/Seata 等成熟组件(JDK 17+ 兼容)
Java 版本示例代码基于 JDK 8 编写,JDK 17/21 下语法兼容

本文讲解的分布式系统核心问题与解决方案原理稳定,不随框架版本变化;落地时选用支持 JDK 17/21 的组件版本即可。