2. 缓存基础与常见模式:穿透、击穿、雪崩与三大读写模式
缓存是后端架构中提升读性能、降低数据库压力的核心手段,但若使用不当,会引入数据不一致、缓存失效风暴等风险。本章从两类问题出发:一是缓存失效引发的三类经典故障(穿透、击穿、雪崩),二是缓存与数据库之间的三种主流同步模式(旁路缓存、读写穿透、异步刷写)。理解这些基础,是构建安全合规缓存体系的前提。
2.1 缓存穿透、击穿、雪崩:定义与区别
这三类问题均与缓存未命中或缓存失效有关,但触发场景、影响范围和应对策略截然不同。下表从核心定义、典型场景、影响范围三个维度进行对比。
| 故障类型 | 核心定义 | 典型场景 | 影响范围 |
|---|---|---|---|
| 缓存穿透 | 请求查询一个缓存和数据库中都不存在的数据,导致每次请求都穿透缓存直达数据库。 | 恶意攻击者构造大量不存在的 ID(如 -1、999999)发起查询。 | 数据库连接数飙升,严重时导致数据库宕机。 |
| 缓存击穿 | 一个热点 key 在缓存失效的瞬间,大量并发请求同时回源查询数据库。 | 秒杀商品详情页的 key 刚好过期,数千请求同时打到数据库。 | 单点数据库压力激增,可能引发该热点数据相关的服务雪崩。 |
| 缓存雪崩 | 大量 key 在同一时间或短时间内集中过期,导致大量请求同时回源数据库。 | 缓存服务重启、大量 key 设置了相同的过期时间(如统一凌晨 0 点过期)。 | 数据库瞬间被海量请求打满,整体服务不可用。 |
合规视角提示: 缓存穿透常被用于数据爬取或撞库攻击,属于安全合规审计中的“异常数据访问模式”。建议在网关层或缓存层对非法参数(如负数 ID、超长字符串)进行前置拦截,并记录审计日志。
2.1.1 三类故障的应对策略对比
- 缓存穿透:
- 缓存空对象:将数据库查询为 null 的结果也缓存(设置较短的 TTL,如 30 秒)。
- 布隆过滤器:在缓存前置一个布隆过滤器,快速判断 key 是否可能存在。
- 参数校验:接口层对请求参数进行合法性校验,拒绝明显非法的 key。
- 缓存击穿:
- 互斥锁(Mutex):回源查询数据库时加锁,只允许一个线程去加载数据,其他线程等待。
- 逻辑过期:不设置物理 TTL,而是使用逻辑过期时间,由后台线程异步刷新。
- 热点 key 永不过期:结合后台定时更新策略。
- 缓存雪崩:
- 过期时间打散:在基础 TTL 上增加随机偏移量(如 ±10%)。
- 多级缓存:本地缓存(如 Caffeine)+ 分布式缓存(如 Redis)分层防护。
- 缓存高可用:使用 Redis 哨兵或集群模式,避免缓存服务单点故障。
关键区别记忆点: 穿透是“查不到”,击穿是“一个热点扛不住”,雪崩是“大量热点一起倒”。
2.2 缓存与数据库一致性三大模式
缓存与数据库之间的数据同步模式决定了系统的最终一致性、实时性和实现复杂度。业界主流有三种模式:旁路缓存(Cache-Aside)、读写穿透(Read/Write-Through)和异步刷写(Write-Behind)。
2.2.1 模式对比总览
| 模式 | 读流程 | 写流程 | 一致性 | 复杂度 | 典型应用 |
|---|---|---|---|---|---|
| 旁路缓存 | 先读缓存,miss 则回源 DB 并回填缓存 | 先更新 DB,再删除/更新缓存 | 最终一致(有窗口期) | 低 | Redis + MySQL 手动管理 |
| 读写穿透 | 缓存层代理读请求,自动回源 | 缓存层代理写请求,同步写 DB 和缓存 | 强一致(同步写) | 中 | Redis Enterprise、Oracle Coherence |
| 异步刷写 | 读缓存(可能脏读) | 只写缓存,异步批量刷入 DB | 弱一致(有丢失风险) | 高 | 写密集型日志、计数器 |
2.2.2 旁路缓存(Cache-Aside)
这是最常用的模式,应用程序直接管理缓存和数据库的读写。读时先查缓存,未命中则查 DB 并回填;写时先更新 DB,再删除缓存(或更新缓存)。
常见陷阱: 写操作如果先删除缓存再更新 DB,会导致并发读请求在 DB 更新前回填旧数据,造成数据不一致。推荐采用“先更新 DB,后删除缓存”的策略,并配合延迟双删或消息队列补偿。
// 旁路缓存读流程伪代码
public User getUser(String userId) {
// 1. 先查缓存
User user = cache.get("user:" + userId);
if (user != null) {
return user;
}
// 2. 缓存 miss,查数据库
user = db.query("SELECT * FROM user WHERE id = ?", userId);
if (user != null) {
// 3. 回填缓存,设置过期时间
cache.set("user:" + userId, user, 300);
}
return user;
}
// 旁路缓存写流程伪代码
public void updateUser(User user) {
// 1. 先更新数据库
db.execute("UPDATE user SET name = ? WHERE id = ?", user.getName(), user.getId());
// 2. 再删除缓存(或更新缓存)
cache.del("user:" + user.getId());
}
2.2.3 读写穿透(Read/Write-Through)
缓存层作为数据访问的唯一入口,应用程序只与缓存交互,缓存组件内部负责与数据库同步。读穿透由缓存组件在 miss 时自动回源;写穿透由缓存组件同步写入数据库和缓存。
- 优点: 应用程序逻辑简化,一致性由缓存层保证。
- 缺点: 缓存组件复杂度高,写操作延迟增加(需等待 DB 写入完成)。
- 适用场景: 对一致性要求高、写并发不极端的业务,如用户账户信息。
2.2.4 异步刷写(Write-Behind)
写操作只更新缓存,立即返回成功,然后由后台线程异步批量写入数据库。读操作直接读取缓存,可能读到尚未持久化的数据。
- 优点: 写吞吐量极高,适合日志、计数、埋点等写密集型场景。
- 风险: 缓存宕机可能导致数据丢失;异步刷写失败需要重试或补偿机制。
- 合规要求: 涉及资金、订单等关键数据时,必须开启 WAL(Write-Ahead Log)或使用可靠消息队列保证最终一致性。
2.3 模式选择与合规考量
在实际架构中,选择哪种模式需要结合业务对一致性、性能和合规的要求:
- 金融/交易类: 优先使用读写穿透或旁路缓存 + 强一致性补偿(如分布式锁、版本号),确保缓存与数据库的最终一致性可审计。
- 内容/社交类: 旁路缓存即可满足需求,配合缓存穿透防护和过期时间打散。
- 日志/监控类: 异步刷写模式可大幅提升写入性能,但需配置 WAL 或消息队列防止数据丢失。
合规红线: 无论采用哪种模式,所有缓存回源操作、写操作、异步刷写失败记录都必须纳入审计日志。对于涉及个人隐私或资金的数据,缓存层不得存储明文敏感信息,必须脱敏或加密。