3. 失效策略模型:TTL、LRU、LFU、FIFO策略的原理与适用场景对比

缓存失效策略是缓存一致性治理的核心决策点。不同的失效策略决定了缓存何时、以何种方式淘汰数据,直接影响缓存命中率、系统吞吐量和数据一致性。本章深入剖析四种经典失效策略的原理、数学模型与适用场景,帮助你在OpenAPI契约设计中做出正确的策略选择。

3.1 失效策略全景图

缓存失效策略可分为两大类:时间驱动型和空间驱动型。时间驱动型以TTL为代表,关注数据的新鲜度;空间驱动型包括LRU、LFU、FIFO,关注缓存容量的有限性。实际系统中,两者通常组合使用。

缓存失效策略分类全景 缓存失效策略 时间驱动型 空间驱动型 TTL(生存时间) LRU LFU FIFO 核心观点: • 时间驱动型(TTL)保证数据新鲜度,适合对一致性要求高的场景。 • 空间驱动型(LRU/LFU/FIFO)保证缓存容量可控,适合内存有限的场景。 • 生产环境通常组合使用:TTL + LRU 是最常见的混合策略。

3.2 TTL(生存时间)策略

3.2.1 原理

TTL(Time To Live)为每个缓存条目设置一个固定的生存时间。当条目被写入缓存时,启动计时器;计时器到期后,条目自动失效并被移除或标记为过期。TTL策略不关心访问频率或顺序,只关心数据的新鲜度。

3.2.2 数学模型

// TTL失效判定
if (current_time - entry.create_time >= entry.ttl) {
    // 条目过期,触发失效
    evict(entry);
}

// 常见TTL设置模式
- 固定TTL:所有条目使用相同的过期时间(如 300秒)
- 差异化TTL:不同数据类别使用不同的过期时间
- 自适应TTL:根据访问模式动态调整过期时间

3.2.3 适用场景

场景类型 推荐TTL 原因
会话数据(Session) 15-30分钟 用户登录状态需要定期刷新
配置数据 5-10分钟 配置变更频率低,但需要及时生效
热点新闻/排行榜 1-5分钟 内容更新频繁,用户对新鲜度敏感
静态资源(CSS/JS) 7-30天 资源极少变更,长TTL减少回源
数据库查询结果 1-60秒 数据一致性要求高,短TTL降低脏读风险
实践建议:在OpenAPI契约中,TTL应作为缓存策略的必选参数。建议使用 x-cache-ttl 扩展字段声明默认TTL,并允许客户端通过请求头 Cache-Control: max-age= 覆盖。

3.3 LRU(最近最少使用)策略

3.3.1 原理

LRU(Least Recently Used)基于“最近访问时间”淘汰数据。当缓存空间不足时,淘汰最长时间未被访问的条目。LRU利用时间局部性原理:最近被访问的数据,未来被访问的概率更高。

3.3.2 实现方式

// 基于双向链表 + 哈希表的LRU实现(O(1)时间复杂度)
class LRUCache {
    private int capacity;
    private Map<K, Node> map;
    private DoublyLinkedList list;
    
    public V get(K key) {
        if (!map.containsKey(key)) return null;
        Node node = map.get(key);
        list.moveToHead(node);  // 移动到链表头部(最近使用)
        return node.value;
    }
    
    public void put(K key, V value) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.value = value;
            list.moveToHead(node);
        } else {
            if (map.size() >= capacity) {
                Node tail = list.removeTail();  // 淘汰最久未使用的
                map.remove(tail.key);
            }
            Node newNode = new Node(key, value);
            list.addToHead(newNode);
            map.put(key, newNode);
        }
    }
}

3.3.3 适用场景

  • 用户会话缓存:活跃用户的会话频繁访问,不活跃用户的会话自然淘汰。
  • 数据库查询缓存:热点查询结果保留,冷查询结果自动淘汰。
  • CDN边缘缓存:最近被请求的资源保留在边缘节点。
  • API响应缓存:频繁调用的API响应保留,低频调用自动淘汰。
注意:LRU存在“扫描污染”问题。当大量一次性数据被访问时,会挤占热点数据的缓存空间。解决方案包括:使用LRU-K(记录最近K次访问)或结合TTL使用。

3.4 LFU(最不经常使用)策略

3.4.1 原理

LFU(Least Frequently Used)基于“访问频率”淘汰数据。每个缓存条目维护一个访问计数器,当缓存空间不足时,淘汰访问频率最低的条目。LFU利用频率局部性原理:访问频率高的数据,未来被访问的概率更高。

3.4.2 数学模型

// LFU访问频率衰减模型
// 方案一:简单计数
frequency = count_of_accesses

// 方案二:时间衰减计数(避免历史频率累积)
frequency = α * old_frequency + (1 - α) * recent_accesses
// α为衰减因子,0 < α < 1,通常取0.9

// 方案三:滑动窗口计数
frequency = count_of_accesses_in_last_N_minutes

3.4.3 适用场景

场景 优势 劣势
热门商品/内容推荐 持续热门的条目不会被淘汰 新热点需要时间积累频率
CDN热门资源 全局热门资源稳定缓存 突发流量可能导致缓存抖动
API限流计数器 高频调用方被准确识别 内存开销较大(需维护计数器)
机器学习特征缓存 高频特征保留,低频特征淘汰 需要配合频率衰减避免陈旧
LFU vs LRU 选择指南:
  • 数据访问模式稳定、热点集中 → 优先选择LFU
  • 数据访问模式动态变化、热点漂移 → 优先选择LRU
  • 混合场景 → 使用W-TinyLFU(窗口TinyLFU)或ARC(自适应替换缓存)

3.5 FIFO(先进先出)策略

3.5.1 原理

FIFO(First In First Out)基于“进入时间”淘汰数据。缓存维护一个队列,新数据从队尾加入,淘汰时从队首移除。FIFO不考虑访问频率或最近访问时间,只考虑数据在缓存中的停留时长。

3.5.2 实现方式

// FIFO缓存实现(基于队列)
class FIFOCache {
    private int capacity;
    private Queue<K> queue;      // 维护进入顺序
    private Map<K, V> map;       // 存储数据
    
    public V get(K key) {
        return map.get(key);     // 不改变队列顺序
    }
    
    public void put(K key, V value) {
        if (map.containsKey(key)) {
            map.put(key, value); // 更新值,不改变顺序
        } else {
            if (map.size() >= capacity) {
                K oldest = queue.poll();  // 移除最早进入的
                map.remove(oldest);
            }
            queue.offer(key);    // 新数据加入队尾
            map.put(key, value);
        }
    }
}

3.5.3 适用场景

  • 消息队列缓存:保证消息按顺序处理,先进入的消息先被消费。
  • 日志缓存:日志按时间顺序写入和淘汰,保证日志的时序性。
  • 数据流处理:滑动窗口计算中,先进入窗口的数据先被淘汰。
  • 简单场景:对缓存命中率要求不高,但实现必须简单的场景。
FIFO的局限性:FIFO不考虑访问模式,可能导致“缓存抖动”——频繁访问的数据被新进入的冷数据淘汰。在大多数业务场景中,FIFO的命中率低于LRU和LFU。仅在数据顺序性要求高于命中率的场景中使用。

3.6 四种策略对比总结

维度 TTL LRU LFU FIFO
淘汰依据 生存时间 最近访问时间 访问频率 进入时间
时间复杂度 O(1) O(1) O(log n) ~ O(1) O(1)
空间复杂度 O(n) O(n) O(n) + 计数器 O(n)
命中率 中等(依赖TTL设置) 高(时间局部性好) 高(频率局部性好) 低(不考虑访问模式)
扫描污染 不适用 易受影响 不易受影响 不适用
实现复杂度 低 中 高 低
典型应用 Redis EXPIRE Redis allkeys-lru Redis allkeys-lfu 消息队列

3.7 混合策略与OpenAPI契约设计

生产环境中,单一策略往往无法满足所有需求。常见的混合策略包括:

  • TTL + LRU:最常用的组合。TTL保证数据新鲜度,LRU保证热点数据不被冷数据挤占。
  • TTL + LFU:适合热点稳定的场景。TTL防止数据永久缓存,LFU保证高频数据优先保留。
  • 分层缓存:L1使用LRU(低延迟),L2使用LFU(高命中率),结合TTL做全局过期。
OpenAPI契约中的失效策略声明:
# 在OpenAPI 3.0中声明缓存失效策略
paths:
  /api/users/{id}:
    get:
      x-cache-policy:
        strategy: "lru"           # 空间淘汰策略
        ttl: 300                   # 时间过期策略(秒)
        max-size: 10000            # 最大条目数
        eviction-priority: "hot"   # 淘汰优先级标记
      responses:
        '200':
          headers:
            Cache-Control:
              schema:
                type: string
                example: "private, max-age=300, stale-while-revalidate=60"
策略选择决策树:
  1. 数据一致性要求高? → 优先使用TTL,设置较短的过期时间
  2. 缓存空间有限且热点集中? → 优先使用LFU
  3. 缓存空间有限且热点动态变化? → 优先使用LRU
  4. 数据顺序性要求高于命中率? → 使用FIFO
  5. 以上都不确定? → 使用TTL + LRU混合策略