动态调整过期时间:让缓存自己”学习”最佳 TTL

动态调整过期时间:让缓存自己”学习”最佳 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
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容