4. 缓存失效与更新策略:TTL、LRU、LFU、主动失效、被动失效对一致性的影响

缓存一致性治理的核心在于“何时失效”与“如何更新”。失效策略决定了缓存数据的生命周期,更新策略决定了数据变更后缓存如何同步。本章深入剖析 TTL、LRU、LFU、主动失效与被动失效五种机制,并评估它们对缓存一致性的具体影响。

4.1 缓存失效策略全景

缓存失效策略可分为时间驱动、容量驱动和事件驱动三大类。下表对比了五种核心策略的定义与适用场景:

策略 类型 触发条件 一致性影响 典型场景
TTL(生存时间) 时间驱动 写入后固定时长到期 弱一致性,存在过期窗口 会话、验证码、静态资源
LRU(最近最少使用) 容量驱动 缓存满时淘汰最久未访问项 不影响数据新鲜度,仅影响命中率 热点数据、用户画像
LFU(最不经常使用) 容量驱动 缓存满时淘汰访问频率最低项 不影响数据新鲜度,但可能保留冷数据 推荐系统、广告检索
主动失效 事件驱动 数据源变更时主动删除/更新缓存 强一致性(配合事务) 订单状态、库存、配置
被动失效 事件驱动 读取时发现数据过期或不存在 最终一致性,依赖 TTL 兜底 读多写少、可容忍短暂不一致
架构决策要点: 在分布式系统中,主动失效是保证强一致性的首选,但会增加系统耦合度;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 秒内读到旧数据
避坑指南: 不要将 TTL 设置过长(如 24 小时),否则过期窗口内的一致性问题会被放大。建议 TTL 不超过业务可容忍的最大不一致时间。

4.3 LRU 与 LFU:容量驱动的淘汰策略

LRU(Least Recently Used)和 LFU(Least Frequently Used)解决的是缓存空间有限时的淘汰问题,它们不直接决定数据是否过期,但会影响缓存中数据的分布,间接影响一致性。

  • LRU:淘汰最近最少访问的条目。实现简单,适合访问模式有明显时间局部性的场景。
  • LFU:淘汰访问频率最低的条目。适合访问模式有频率倾斜的场景,但可能保留“曾经热门、现已过时”的数据。

一致性影响: LRU/LFU 本身不保证数据新鲜度。如果缓存中保留的是旧数据,即使它被频繁访问,也不会自动更新。因此,LRU/LFU 必须与 TTL 或主动失效配合使用。

最佳实践: 在 Redis 中,使用 allkeys-lru 或 volatile-lru 淘汰策略,同时为每个 key 设置合理的 TTL,实现“容量 + 时间”双重保障。

4.4 主动失效:事件驱动的强一致性保障

主动失效是指在数据源发生变更时,立即通知缓存层删除或更新对应的缓存项。这是实现强一致性或读写一致性的核心手段。

常见的实现模式:

  1. Cache-Aside 模式下的主动失效:应用在更新数据库后,主动删除缓存中的对应 key。
  2. Write-Through 模式:应用同时写入数据库和缓存,保证两者同步。
  3. 消息队列驱动失效:数据库变更后发送消息,缓存消费者监听并执行失效操作。
// 更新数据库后,主动删除缓存
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;
}
架构建议: 主动失效配合 Cache-Aside 模式,是分布式系统中平衡一致性与性能的黄金组合。注意:删除缓存而非更新缓存,可以避免并发写导致的脏数据问题。

4.5 被动失效:最终一致性的兜底方案

被动失效是指缓存项在读取时被检查是否过期或不存在,如果过期则从数据源重新加载。它通常与 TTL 配合使用,作为主动失效的补充。

  • Lazy Loading(懒加载):读取时发现缓存不存在,加载并写入缓存。
  • TTL 过期检查:读取时检查 TTL,过期则重新加载。

一致性影响: 被动失效只能保证最终一致性。在 TTL 过期之前,如果数据源已更新,客户端仍会读到旧数据。被动失效无法消除过期窗口,但可以保证窗口结束后数据自动恢复一致。

风险提示: 被动失效在高并发场景下可能引发 缓存雪崩(大量缓存同时过期)或 缓存击穿(热点 key 过期后大量请求穿透到数据库)。建议使用互斥锁或分布式锁控制回源行为。

4.6 策略组合与一致性等级

单一策略难以满足所有一致性需求,实际架构中需要组合使用。下表展示了常见组合及其达成的一致性等级:

组合策略 一致性等级 适用场景 复杂度
TTL + 被动失效 最终一致性 读多写少、可容忍短暂不一致 低
主动失效 + TTL 强一致性(窗口极小) 订单、库存、支付 中
主动失效 + LRU/LFU 强一致性 + 高命中率 热点数据、用户会话 中高
Write-Through + TTL 强一致性 配置中心、元数据 高

4.7 失效策略对一致性的影响总结

下图展示了不同失效策略在一致性、性能和复杂度三个维度上的权衡关系:

缓存失效策略权衡矩阵 一致性强度 → 复杂度 ↑ TTL + 被动失效 最终一致性 · 低复杂度 主动失效 + TTL 强一致性 · 中复杂度 主动失效 + LRU/LFU 强一致性 · 中高复杂度 Write-Through 强一致性 · 高复杂度 权衡:一致性越强,系统复杂度与耦合度越高;最终一致性方案更适合高可用、可扩展的分布式架构。
架构总结:
  • 强一致性场景(订单、支付、库存):优先采用 主动失效 + TTL 组合,必要时引入分布式事务或消息队列保证失效操作的原子性。
  • 最终一致性场景(用户资料、文章列表、推荐):采用 TTL + 被动失效 组合,配合 LRU 淘汰策略,在一致性与性能之间取得平衡。
  • 容量管理:LRU/LFU 是缓存系统的“内存管家”,必须与时间或事件驱动的失效策略配合,不能单独依赖。