分布式缓存与一致性
缓存是提升系统性能最常用也最易用错的手段。本文聚焦两大核心问题:缓存带来的三大难题(穿透、击穿、雪崩)以及缓存与数据库的数据一致性。掌握这两点,才能在生产环境安全地使用缓存。
一、缓存的价值与风险
1.1 为什么用缓存
- 数据库读多写少,热点数据命中缓存可显著降低数据库压力。
- Redis 等缓存读写性能(微秒级)远高于数据库(毫秒级)。
- 将热点数据、查询结果、会话、配置放入缓存,缩短响应时间。
1.2 缓存的代价
引入缓存意味着多了一份数据副本,随之而来的是:
- 一致性:缓存与数据库可能不一致。
- 三大难题:穿透、击穿、雪崩。
- 运维复杂度:缓存部署、容量、过期策略。
二、缓存三大难题
2.1 缓存穿透
现象:查询一个数据库中不存在的数据,缓存里也没有(因为没存),请求每次都打到数据库。恶意攻击者可构造大量不存在的 key 压垮数据库。
解决:
- 缓存空值:对不存在的 key 也缓存一个空值(设置较短过期时间,如 5 分钟),避免穿透。
- 布隆过滤器(Bloom Filter):在缓存前加一层布隆过滤器,对不存在的数据直接拦截(有极小误判率,但不会漏判存在的数据)。
- 参数校验:对明显非法/越界的参数直接拒绝。
2.2 缓存击穿
现象:某个热点 key 过期的瞬间,大量并发请求同时打到数据库,导致数据库压力瞬间飙升。
解决:
- 互斥锁(mutex):key 过期后,只有一个线程去查库并回填缓存,其余线程等待。
- 逻辑过期:缓存数据不过期(物理上设一个较长的 TTL),但数据里存一个逻辑过期时间;读取时若发现逻辑过期,则异步重建缓存并返回旧值,避免击穿。
- 热点数据不过期:对超高热点数据不设置过期时间,由后台定时任务主动更新。
2.3 缓存雪崩
现象:大量 key 在同一时间集中过期(或缓存节点宕机),请求全部打到数据库,导致数据库被压垮,进而引发系统雪崩。
解决:
- 过期时间加随机:给每个 key 的过期时间加一个随机值(如 1~5 分钟),避免同一时间集中过期。
- 多级缓存:本地缓存(Caffeine)+ Redis 二级缓存,降低对 Redis/DB 的直连压力。
- 缓存高可用:Redis 集群(主从 + 哨兵 / Cluster),避免单点宕机。
- 限流降级:数据库层面加限流、熔断,兜底保护。
三、缓存与数据库的一致性
3.1 为什么会产生不一致
缓存是数据库的副本。写操作如果处理不当,缓存与数据库就会不一致。不一致的根本原因是:缓存和数据库是两个独立的存储,无法在一个原子操作里同时更新。
3.2 缓存更新策略
策略一:Cache Aside(旁路缓存)——最常用
读流程:读缓存 → 命中则返回;未命中则查库、回填缓存、返回。 写流程:先更新数据库,再删除缓存(或更新缓存)。
读:get(key) -> 缓存命中? -> 是返回
-> 否: 查DB -> 写缓存 -> 返回
写:update(DB) -> 删除缓存(或更新缓存)为什么"更新数据库后删缓存"而不是"更新缓存":
- 若先更新数据库再更新缓存,两个操作非原子,中间失败会导致缓存是旧值。
- 删除缓存后,下次读会查库重建,天然最终一致。
- 更新缓存比删除缓存更容易出现并发覆盖旧值。
策略二:延迟双删(Delay Double Delete)
在高并发下,"先更新 DB 再删缓存"仍可能出现短暂不一致(删除缓存前有读请求把旧值写回缓存)。延迟双删通过"删除缓存 → 更新 DB → 延迟再次删除缓存"来消除:
1. 删除缓存 key
2. 更新数据库
3. 延迟 N ms(如 500ms)后再次删除缓存 key延迟的目的是等待可能读到的旧值(在删除和更新之间的间隙)过期,第二次删除彻底清掉旧值。
3.3 最终一致性的本质
缓存的一致性追求的是最终一致(缓存数据最终会与数据库一致),而非强一致。因为:
- 强一致需要在每次读写都加分布式锁/串行化,性能损失极大。
- 绝大多数场景(读多写少)下,短暂的缓存不一致(毫秒~秒级)是可接受的。
适用强一致的场景(资金、库存关键数据)应避免依赖缓存,或采用严格的缓存失效 + 双删 + 消息补偿。
3.4 消息队列最终一致性
在更复杂的场景(多个服务、多个库),可通过消息队列保证最终一致:写操作后发一条消息(携带最新数据),消费端异步更新缓存。配合消息重试 + 幂等,即使中间失败也能最终对齐。
四、最佳实践
- 缓存 key 设计:带业务前缀 + 版本号,避免不同业务/版本冲突。
- 过期时间:所有 key 都要有过期时间,并加随机抖动,防止雪崩。
- 热点保护:对热点 key 做本地缓存兜底、逻辑过期、互斥锁。
- 缓存不可用时降级:缓存读失败要降级到数据库,而不是抛异常导致整个请求失败。
- 监控:监控缓存命中率、穿透/击穿/雪崩指标,及时发现异常。
小结
- 缓存三大难题:穿透(查不存在的数据,用缓存空值/布隆过滤器)、击穿(热点 key 过期,用互斥锁/逻辑过期)、雪崩(大量 key 集中过期,用随机 TTL/多级缓存)。
- 一致性主流方案:Cache Aside(旁路缓存)+ 延迟双删,追求最终一致。
- 缓存与数据库无法原子更新,最终一致性是权衡性能后的正确选择。
版本差异(技术原理说明)
| 维度 | 说明 |
|---|---|
| 技术原理 | 分布式一致性/事务/锁/ID 生成等原理与具体版本无关,长期有效 |
| 落地选型 | 新项目建议优先使用 Nacos/Redis/Seata 等成熟组件(JDK 17+ 兼容) |
| Java 版本 | 示例代码基于 JDK 8 编写,JDK 17/21 下语法兼容 |
本文讲解的分布式系统核心问题与解决方案原理稳定,不随框架版本变化;落地时选用支持 JDK 17/21 的组件版本即可。