3. 缓存失效策略:TTL过期、LRU淘汰、主动失效、被动失效机制详解

缓存失效策略是缓存一致性治理的核心操作面。选错策略会导致缓存与数据库长期不一致、缓存击穿、雪崩或内存溢出。本节从四种主流失效机制的原理、适用场景、性能影响和配置要点展开,帮助你在实际系统中做出正确选择。

3.1 四种失效策略总览

策略 触发方式 一致性保障 性能开销 典型场景
TTL过期 时间驱动(被动) 弱一致性,最终一致 极低(无实时开销) 会话、验证码、静态配置
LRU淘汰 内存压力驱动(被动) 不保证一致性 低(O(1)链表操作) 热点数据、用户画像、Feed流
主动失效 业务事件驱动(主动) 强一致性(配合同步) 中(依赖消息/回调) 订单状态、库存、账户余额
被动失效 查询时校验(延迟) 最终一致性 低(仅失效时额外查询) 读多写少、可容忍短暂不一致
选型原则: 对一致性要求高的数据(如支付状态)优先使用主动失效;对性能敏感且允许短暂不一致的数据(如商品详情)优先使用TTL过期 + 被动失效组合;LRU淘汰只用于内存管理,不能作为一致性手段。

3.2 TTL过期机制

TTL(Time-To-Live)是缓存系统最基础的失效方式。写入缓存时指定一个过期时间,到达时间后缓存自动失效。Redis、Memcached、Caffeine 等主流缓存均原生支持。

3.2.1 实现方式

  • 惰性删除: 访问时检查是否过期,过期则删除并返回null。Redis默认采用此方式,CPU开销低。
  • 定时扫描: 后台线程定期扫描部分key,删除过期key。Redis每秒扫描10次,每次随机取20个key,过期比例超过25%则重复扫描。
  • 定时器: 每个key绑定一个定时器,到期精确删除。Caffeine支持此模式,但内存开销较大。

3.2.2 性能调优要点

参数 推荐值 说明
TTL时长 业务容忍窗口 × 1.5~2 太短导致频繁回源,太长增加不一致窗口
随机偏移 ±10%~20% 防止大量key同时过期导致缓存雪崩
扫描频率 默认(10次/秒) 高并发场景可调至20次/秒,但CPU会增加
// Java示例:Caffeine TTL + 随机偏移
Cache<String, UserProfile> cache = Caffeine.newBuilder()
    .expireAfterWrite(Duration.ofMinutes(10 + ThreadLocalRandom.current().nextInt(-2, 3)))
    .maximumSize(10_000)
    .build();

// Redis TTL设置(Jedis)
jedis.setex("user:1001", 600, userJson);  // 600秒过期
避坑: 不要对热点key设置完全相同的TTL。例如秒杀商品缓存全部设置60秒过期,60秒后同时失效,所有请求穿透到数据库,极易引发缓存雪崩。务必加入随机偏移量。

3.3 LRU淘汰机制

LRU(Least Recently Used)是内存受限场景下的淘汰策略。当缓存容量达到上限时,淘汰最久未访问的数据。Redis的allkeys-lru和volatile-lru、Caffeine的maximumSize、Memcached的LRU均基于此原理。

3.3.1 实现原理

  • 双向链表 + HashMap: 访问时把节点移到链表头部,淘汰时移除链表尾部节点。时间复杂度O(1)。
  • 近似LRU: Redis为了节省内存,采用采样淘汰(默认采样5个key,淘汰其中最久未访问的),性能更高但淘汰精度略低。
  • W-TinyLFU: Caffeine使用的增强算法,结合频率和最近访问,能更好地抵抗扫描攻击。
LRU淘汰流程示意图 Head(最近访问) Key:A Key:B Key:C Tail(淘汰候选) ← 访问 Key:B,将其移到头部 Head Key:B(刚访问) Key:A Key:C Tail 淘汰触发条件 当缓存容量达到上限时,移除Tail节点(最久未访问的Key),新数据插入Head

3.3.2 性能调优要点

  • 合理设置最大内存: Redis建议设置为总内存的60%~70%,预留空间给操作系统和持久化。
  • 选择正确的淘汰策略:
    • allkeys-lru:所有key参与淘汰,适用于通用缓存。
    • volatile-lru:仅淘汰带TTL的key,适用于混合存储(持久key + 临时key)。
    • allkeys-random:随机淘汰,适用于均匀访问的场景。
  • 监控淘汰率: 淘汰率(evicted_keys / total_keys)超过5%说明内存不足,需要扩容或优化缓存粒度。
# Redis配置示例
maxmemory 4gb
maxmemory-policy allkeys-lru
# 监控淘汰量
redis-cli info stats | grep evicted_keys

3.4 主动失效机制

主动失效是保证缓存与数据库强一致性的关键手段。当数据发生变更(增、删、改)时,立即通知缓存删除或更新对应key。常见实现方式包括:

3.4.1 实现模式

模式 实现方式 延迟 可靠性
同步失效 写数据库后立即删除缓存(Cache-Aside模式) 毫秒级 高(同事务)
异步消息失效 写数据库后发送MQ消息,消费者删除缓存 秒级 中(依赖MQ可靠性)
Binlog监听失效 监听MySQL binlog变更,解析后删除缓存 秒级 高(解耦、无侵入)
分布式锁失效 更新时加锁,失效后释放,其他线程回源 取决于锁粒度 高(防并发击穿)

3.4.2 性能调优要点

  • 避免频繁失效: 高频更新的数据(如计数器)不适合每次变更都失效,可合并为批量失效或使用TTL兜底。
  • 延迟双删: 先删缓存 → 写数据库 → 休眠(如500ms)→ 再删缓存。解决并发读写导致的脏数据问题。
  • 异步削峰: 秒杀场景下,使用MQ异步失效,避免写操作阻塞。
// Java示例:延迟双删
public void updateUser(User user) {
    // 1. 先删缓存
    cache.delete("user:" + user.getId());
    // 2. 写数据库
    userDao.update(user);
    // 3. 延迟再删(异步)
    executor.schedule(() -> cache.delete("user:" + user.getId()), 500, TimeUnit.MILLISECONDS);
}
实战案例: 某电商订单系统使用同步失效 + 延迟双删,将缓存与数据库不一致窗口从平均3秒降至50毫秒以内。关键优化点是将延迟时间从固定1秒改为根据数据库主从延迟动态调整。

3.5 被动失效机制

被动失效也称为“懒加载”或“查询时校验”。缓存不主动删除数据,而是在读取时检查数据是否仍然有效。常见实现方式:

  • 版本号校验: 缓存中存储数据版本号,查询时与数据库版本号对比,不一致则回源。
  • 时间戳校验: 缓存中存储最后更新时间,查询时判断是否超过容忍窗口。
  • 布隆过滤器: 用于快速判断key是否可能存在,避免无效回源。
// 版本号校验示例
public User getUserWithVersion(long userId) {
    // 从缓存获取数据和版本号
    CacheEntry entry = cache.get("user:" + userId);
    if (entry != null) {
        long cachedVersion = entry.getVersion();
        long dbVersion = versionDao.getVersion(userId);
        if (cachedVersion == dbVersion) {
            return entry.getData();  // 缓存有效
        }
    }
    // 回源数据库
    User user = userDao.getById(userId);
    cache.put("user:" + userId, new CacheEntry(user, user.getVersion()));
    return user;
}

3.5.1 性能调优要点

  • 版本号存储: 使用Redis Hash结构,将数据和版本号放在同一个key的不同field,减少网络开销。
  • 批量校验: 列表页场景下,使用pipeline批量校验版本号,避免多次网络往返。
  • 降级策略: 当数据库压力过大时,暂时跳过版本校验,直接使用缓存数据(牺牲一致性换取可用性)。

3.6 策略组合与选型矩阵

实际生产环境中,单一策略往往无法满足所有需求。推荐组合使用:

缓存失效策略组合决策树 数据变更频率? 高频(秒级变更) TTL + 被动失效 (版本号校验) 中频(分钟级变更) 主动失效 + TTL兜底 (MQ异步失效) 低频(小时级变更) 主动失效 + 长TTL (同步删除)

3.6.1 推荐组合方案

业务场景 推荐策略组合 一致性等级 性能表现
订单状态、支付结果 主动失效(同步删除)+ 延迟双删 强一致 中等(增加一次网络开销)
商品详情、文章内容 TTL过期 + 被动失效(版本号) 最终一致(秒级) 高(极少回源)
用户会话、验证码 TTL过期 + LRU淘汰 弱一致(可接受) 极高(无额外开销)
库存、秒杀计数 主动失效(MQ异步)+ 分布式锁 最终一致(毫秒级) 高(异步削峰)
配置信息、字典表 主动失效(Binlog监听)+ 长TTL 最终一致(秒级) 极高(几乎不回源)
关键提醒: 不要试图用LRU淘汰来保证数据一致性。LRU只解决内存容量问题,不关心数据是否过期。如果数据更新后未被访问,LRU可能一直保留旧数据,导致长时间不一致。一致性必须由TTL或主动/被动失效来保障。

3.7 本章小结

  • TTL过期是最基础的失效手段,适合容忍短暂不一致的场景,务必加入随机偏移防雪崩。
  • LRU淘汰只用于内存管理,不能替代一致性策略,选择正确的淘汰策略(allkeys-lru vs volatile-lru)至关重要。
  • 主动失效是强一致性的核心,延迟双删和MQ异步失效是两种主流实现,根据业务对延迟的容忍度选择。
  • 被动失效通过版本号或时间戳校验,在性能和一致性之间取得平衡,适合读多写少场景。
  • 组合策略是生产环境的常态,根据数据变更频率和一致性要求选择最优组合。