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使用的增强算法,结合频率和最近访问,能更好地抵抗扫描攻击。
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 策略组合与选型矩阵
实际生产环境中,单一策略往往无法满足所有需求。推荐组合使用:
3.6.1 推荐组合方案
| 业务场景 | 推荐策略组合 | 一致性等级 | 性能表现 |
|---|---|---|---|
| 订单状态、支付结果 | 主动失效(同步删除)+ 延迟双删 | 强一致 | 中等(增加一次网络开销) |
| 商品详情、文章内容 | TTL过期 + 被动失效(版本号) | 最终一致(秒级) | 高(极少回源) |
| 用户会话、验证码 | TTL过期 + LRU淘汰 | 弱一致(可接受) | 极高(无额外开销) |
| 库存、秒杀计数 | 主动失效(MQ异步)+ 分布式锁 | 最终一致(毫秒级) | 高(异步削峰) |
| 配置信息、字典表 | 主动失效(Binlog监听)+ 长TTL | 最终一致(秒级) | 极高(几乎不回源) |
关键提醒: 不要试图用LRU淘汰来保证数据一致性。LRU只解决内存容量问题,不关心数据是否过期。如果数据更新后未被访问,LRU可能一直保留旧数据,导致长时间不一致。一致性必须由TTL或主动/被动失效来保障。
3.7 本章小结
- TTL过期是最基础的失效手段,适合容忍短暂不一致的场景,务必加入随机偏移防雪崩。
- LRU淘汰只用于内存管理,不能替代一致性策略,选择正确的淘汰策略(allkeys-lru vs volatile-lru)至关重要。
- 主动失效是强一致性的核心,延迟双删和MQ异步失效是两种主流实现,根据业务对延迟的容忍度选择。
- 被动失效通过版本号或时间戳校验,在性能和一致性之间取得平衡,适合读多写少场景。
- 组合策略是生产环境的常态,根据数据变更频率和一致性要求选择最优组合。