动态调整过期时间:让缓存自己”学习”最佳 TTL
什么是动态调整过期时间
动态调整过期时间(Adaptive TTL / Dynamic TTL)是指不采用固定 TTL,而是根据数据的实际访问模式、热度变化、系统负载等因素,实时或准实时地调整每条缓存数据的过期时间。
简单的逻辑:“数据访问得越频繁,让它活得更久;被冷落的,早点淘汰。”
固定 TTL 的局限性
- “一刀切”:把所有缓存设成相同的 TTL,热点数据和低频数据的生命周期相同
- 低效:有的数据 5 分钟被访问 1000 次后被过期移除,而有的数据 1 小时内没人看却还占着内存
- 不灵活:业务模式变化(如突然成为爆款)时无法自动调整
常见的动态 TTL 策略
1. 访问频率驱动的动态 TTL
记录每个 key 的访问频率,高频率的 key 自动延长 TTL:
public class AdaptiveTtlManager {
private final Map<String, AtomicInteger> frequency = new ConcurrentHashMap<>();
// 每次访问记录
public void recordAccess(String key) {
frequency.computeIfAbsent(key, k -> new AtomicInteger()).incrementAndGet();
}
// 计算该 key 的动态 TTL(秒)
public int getTtl(String key) {
int freq = frequency.getOrDefault(key, new AtomicInteger()).get();
if (freq > 10_000) return 7200; // 超高频:2小时
if (freq > 1_000) return 3600; // 高频:1小时
if (freq > 100) return 600; // 中频:10分钟
return 60; // 低频:1分钟
}
// 定时清理频率记录,避免内存泄漏
@Scheduled(fixedRate = 60_000)
public void cleanup() {
frequency.clear(); // 简化实现,实际应使用时间窗口
}
}
2. 滑动窗口动态 TTL
每次访问 key 时,如果距离过期时间小于阈值,则将过期时间向后滑动:
public void accessAndSlide(String key, int baseTtl) {
String value = redis.opsForValue().get(key);
if (value == null) {
value = loadFromDB(key);
redis.opsForValue().set(key, value, baseTtl, TimeUnit.SECONDS);
return;
}
// 检查剩余 TTL,小于阈值则续期
Long remaining = redis.getExpire(key);
if (remaining != null && remaining < baseTtl / 2) {
redis.expire(key, baseTtl, TimeUnit.SECONDS);
}
}
3. 命中率驱动的动态 TTL
监控缓存层的整体命中率,动态调节全局的基础 TTL:
public class HitRateDrivenTtl {
private final AtomicLong hits = new AtomicLong();
private final AtomicLong misses = new AtomicLong();
public void record(boolean isHit) {
if (isHit) hits.incrementAndGet();
else misses.incrementAndGet();
}
public double getHitRate() {
long total = hits.get() + misses.get();
return total == 0 ? 1.0 : (double) hits.get() / total;
}
public int calculateBaseTtl() {
double hitRate = getHitRate();
if (hitRate > 0.95) return 3600; // 命中率高,延长 TTL
if (hitRate > 0.80) return 1800; // 正常
if (hitRate > 0.60) return 600; // 命中率低,缩短 TTL
return 300; // 命中率很低,加长也没用
}
}
原理:命中率很高 → 表示当前 TTL 够用甚至可延长 → DB 压力小
命中率很低 → 当前 TTL 不合理(可能数据已不是热点)→ 缩短 TTL 释放空间
4. 业务特征驱动的动态 TTL
不同时间段切换不同的 TTL 策略:
public int getTtlByTime() {
LocalTime now = LocalTime.now();
if (now.isAfter(LocalTime.of(10, 0)) && now.isBefore(LocalTime.of(14, 0))) {
return 1800; // 午间高峰期,TTL 翻倍保命中率
}
if (now.isAfter(LocalTime.of(22, 0)) || now.isBefore(LocalTime.of(6, 0))) {
return 300; // 夜间低谷,TTL 缩短释放内存
}
return 900;
}
实现注意事项
避免内存泄漏
频率统计需要定期清理,否则会无限增长:
// 使用 Guava Cache 做频率统计,自带过期
LoadingCache<String, AtomicInteger> freqCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(100_000)
.build(key -> new AtomicInteger());
频率衰减
访问频率应该有一个衰减机制,避免”历史热度”一直存在:
// 定时衰减:每分钟衰减一半
if (timeElapsed > 60_000) {
previousFreq.updateAndGet(v -> v / 2);
}
面试要点
- 动态 TTL 的核心思想:“热数据多活,冷数据快走”
- 最简单的实现就是用滑动窗口(每次访问刷新过期时间)
- 高阶方案:结合访问频率、命中率、时间段多方因素
- 不是所有场景都需要动态 TTL——如果你的缓存访问模式稳定,固定 TTL 足够了
- 记住动态 TTL 引入的额外开销:频率统计需要内存和计算资源
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容