4. 缓存失效与更新策略:TTL、LRU、LFU、主动失效、被动失效对一致性的影响
缓存一致性治理的核心在于“何时失效”与“如何更新”。失效策略决定了缓存数据的生命周期,更新策略决定了数据变更后缓存如何同步。本章深入剖析 TTL、LRU、LFU、主动失效与被动失效五种机制,并评估它们对缓存一致性的具体影响。
4.1 缓存失效策略全景
缓存失效策略可分为时间驱动、容量驱动和事件驱动三大类。下表对比了五种核心策略的定义与适用场景:
| 策略 | 类型 | 触发条件 | 一致性影响 | 典型场景 |
|---|---|---|---|---|
| TTL(生存时间) | 时间驱动 | 写入后固定时长到期 | 弱一致性,存在过期窗口 | 会话、验证码、静态资源 |
| LRU(最近最少使用) | 容量驱动 | 缓存满时淘汰最久未访问项 | 不影响数据新鲜度,仅影响命中率 | 热点数据、用户画像 |
| LFU(最不经常使用) | 容量驱动 | 缓存满时淘汰访问频率最低项 | 不影响数据新鲜度,但可能保留冷数据 | 推荐系统、广告检索 |
| 主动失效 | 事件驱动 | 数据源变更时主动删除/更新缓存 | 强一致性(配合事务) | 订单状态、库存、配置 |
| 被动失效 | 事件驱动 | 读取时发现数据过期或不存在 | 最终一致性,依赖 TTL 兜底 | 读多写少、可容忍短暂不一致 |
4.2 TTL:时间驱动的过期机制
TTL(Time-To-Live)是最基础的失效策略。缓存项在写入时设置一个绝对或相对过期时间,到期后自动失效。
- 绝对 TTL:指定具体时间点失效,如“2025-06-01 00:00:00”。
- 相对 TTL:从写入或最后一次访问开始计时,如“写入后 300 秒失效”。
- 滑动 TTL:每次访问刷新过期时间,适用于活跃会话。
一致性影响: TTL 必然引入“过期窗口”——在数据源已更新但缓存尚未失效的时间段内,客户端读到的是脏数据。窗口长度等于 TTL 值。
// 设置缓存项,TTL 为 60 秒
cache.put("user:1001", userData, 60, TimeUnit.SECONDS);
// 数据源更新后,缓存仍有效 60 秒
// 客户端在这 60 秒内读到旧数据
4.3 LRU 与 LFU:容量驱动的淘汰策略
LRU(Least Recently Used)和 LFU(Least Frequently Used)解决的是缓存空间有限时的淘汰问题,它们不直接决定数据是否过期,但会影响缓存中数据的分布,间接影响一致性。
- LRU:淘汰最近最少访问的条目。实现简单,适合访问模式有明显时间局部性的场景。
- LFU:淘汰访问频率最低的条目。适合访问模式有频率倾斜的场景,但可能保留“曾经热门、现已过时”的数据。
一致性影响: LRU/LFU 本身不保证数据新鲜度。如果缓存中保留的是旧数据,即使它被频繁访问,也不会自动更新。因此,LRU/LFU 必须与 TTL 或主动失效配合使用。
allkeys-lru 或 volatile-lru 淘汰策略,同时为每个 key 设置合理的 TTL,实现“容量 + 时间”双重保障。
4.4 主动失效:事件驱动的强一致性保障
主动失效是指在数据源发生变更时,立即通知缓存层删除或更新对应的缓存项。这是实现强一致性或读写一致性的核心手段。
常见的实现模式:
- Cache-Aside 模式下的主动失效:应用在更新数据库后,主动删除缓存中的对应 key。
- Write-Through 模式:应用同时写入数据库和缓存,保证两者同步。
- 消息队列驱动失效:数据库变更后发送消息,缓存消费者监听并执行失效操作。
// 更新数据库后,主动删除缓存
public void updateUser(User user) {
database.update(user);
cache.delete("user:" + user.getId()); // 主动失效
}
// 下次读取时,缓存未命中,从数据库加载最新数据
public User getUser(Long id) {
User user = cache.get("user:" + id);
if (user == null) {
user = database.get(id);
cache.put("user:" + id, user, 300);
}
return user;
}
4.5 被动失效:最终一致性的兜底方案
被动失效是指缓存项在读取时被检查是否过期或不存在,如果过期则从数据源重新加载。它通常与 TTL 配合使用,作为主动失效的补充。
- Lazy Loading(懒加载):读取时发现缓存不存在,加载并写入缓存。
- TTL 过期检查:读取时检查 TTL,过期则重新加载。
一致性影响: 被动失效只能保证最终一致性。在 TTL 过期之前,如果数据源已更新,客户端仍会读到旧数据。被动失效无法消除过期窗口,但可以保证窗口结束后数据自动恢复一致。
4.6 策略组合与一致性等级
单一策略难以满足所有一致性需求,实际架构中需要组合使用。下表展示了常见组合及其达成的一致性等级:
| 组合策略 | 一致性等级 | 适用场景 | 复杂度 |
|---|---|---|---|
| TTL + 被动失效 | 最终一致性 | 读多写少、可容忍短暂不一致 | 低 |
| 主动失效 + TTL | 强一致性(窗口极小) | 订单、库存、支付 | 中 |
| 主动失效 + LRU/LFU | 强一致性 + 高命中率 | 热点数据、用户会话 | 中高 |
| Write-Through + TTL | 强一致性 | 配置中心、元数据 | 高 |
4.7 失效策略对一致性的影响总结
下图展示了不同失效策略在一致性、性能和复杂度三个维度上的权衡关系:
- 强一致性场景(订单、支付、库存):优先采用 主动失效 + TTL 组合,必要时引入分布式事务或消息队列保证失效操作的原子性。
- 最终一致性场景(用户资料、文章列表、推荐):采用 TTL + 被动失效 组合,配合 LRU 淘汰策略,在一致性与性能之间取得平衡。
- 容量管理:LRU/LFU 是缓存系统的“内存管家”,必须与时间或事件驱动的失效策略配合,不能单独依赖。