2. 缓存基础回顾:缓存的定义、缓存命中与未命中、缓存淘汰策略

在深入探讨 OpenAPI 接口缓存一致性之前,必须建立对缓存基础概念的共识。缓存并非简单的“存一下数据”,而是一种通过空间换时间、减少重复计算与 I/O 开销的系统性设计。本节将围绕缓存的核心定义、命中判定机制以及主流淘汰策略展开,为后续架构演进提供理论基石。

2.1 缓存的定义与核心价值

缓存(Cache)是位于原始数据源与请求方之间的临时存储层,用于保存频繁访问的数据副本。其核心价值在于:

  • 降低延迟:从内存或 SSD 读取数据比从数据库或远程服务获取快 1~3 个数量级。
  • 减轻后端压力:缓存层吸收大量读请求,保护数据库、下游微服务不被击穿。
  • 提升吞吐量:相同硬件资源下,缓存系统可支撑更高的 QPS。
关键指标:缓存系统的性能通常用 命中率(Hit Ratio) 衡量,计算公式为:命中率 = 缓存命中次数 / (缓存命中次数 + 缓存未命中次数)。企业级缓存系统通常要求命中率 ≥ 95%。

2.2 缓存命中与未命中

每一次缓存访问都会产生两种结果之一:

状态 定义 典型处理流程 对系统的影响
缓存命中(Cache Hit) 请求的 key 在缓存中存在且未过期 直接返回缓存数据,零后端调用 延迟极低(< 1ms),后端负载不变
缓存未命中(Cache Miss) 请求的 key 不存在或已过期 穿透到数据库/下游服务获取数据,回填缓存后返回 延迟增加(10~200ms),后端产生一次查询开销
缓存穿透(Cache Penetration) 请求的数据在缓存和数据库中都不存在 每次请求都直接打到数据库,缓存形同虚设 严重时导致数据库连接耗尽
避坑提示:缓存穿透是 OpenAPI 接口设计中极易被忽视的风险。对于查询结果为空的数据,建议在缓存中设置一个 空值占位(Null Object),并设置较短的 TTL(如 30~60 秒),避免恶意请求反复穿透。

2.3 缓存淘汰策略

当缓存空间满时,必须按照特定规则移除旧数据以容纳新数据。不同的淘汰策略直接影响命中率和系统行为。以下为三种最基础的策略:

策略 全称 淘汰依据 适用场景 典型实现
LRU Least Recently Used 最近最少被访问 热点数据集中、访问模式有局部性 双向链表 + 哈希表
LFU Least Frequently Used 访问频率最低 访问模式稳定、需要长期保留高频数据 最小堆 / 多级链表
FIFO First In First Out 进入缓存的时间最早 数据时效性要求高、缓存空间固定 队列

三种策略的对比特征:

  • LRU 对突发流量友好:一个数据被访问一次后会被移到链表头部,短时间内不会被淘汰。
  • LFU 对长期热点友好:高频数据即使长时间未被访问,只要频率高就不会被淘汰;但存在“缓存污染”问题——过去高频但当前不再需要的数据会长期占据空间。
  • FIFO 实现最简单,但完全不考虑访问频率和时效性,容易淘汰掉仍被频繁使用的数据。
工程建议:在 OpenAPI 网关或业务缓存层中,LRU 是默认首选。Redis 的 allkeys-lru 和 volatile-lru 策略均基于 LRU 近似算法。如果业务场景中某些数据需要长期保留(如用户配置),可结合 LFU 或手动设置永不过期。

2.4 缓存淘汰策略对比图

下图展示了三种淘汰策略在相同数据访问序列下的淘汰行为差异,帮助理解不同策略对缓存内容的影响:

缓存淘汰策略对比:LRU vs LFU vs FIFO 缓存淘汰策略对比:LRU vs LFU vs FIFO 缓存中保留的数据 被淘汰的数据 新插入的数据 访问序列:A → B → C → D → A → E → F 缓存容量:4 个槽位 LRU A B C D A B E C F LFU A B C D A D E C F FIFO A B C D A A E B F 淘汰行为差异说明: • LRU:淘汰最久未访问的 B(A 被重新访问后提升优先级) • LFU:淘汰频率最低的 D(A 被访问两次,频率最高) • FIFO:淘汰最早进入的 A(即使 A 刚被访问过,仍按入队顺序淘汰)

2.5 缓存淘汰策略选择矩阵

在实际的 OpenAPI 缓存设计中,选择淘汰策略需要结合数据访问特征和业务容忍度:

业务场景 推荐策略 原因
热点新闻 / 商品详情 LRU 访问模式随时间漂移,LRU 能快速响应热点变化
用户配置 / 权限数据 LFU 高频用户数据需要长期保留,低频数据可淘汰
验证码 / 临时 Token FIFO + TTL 数据有严格时效性,先进入的先失效
OpenAPI 响应缓存 LRU + 主动失效 接口响应具有时间局部性,配合业务主动失效保证一致性

2.6 缓存淘汰策略实现示例

以下为 LRU 策略的简化实现(基于双向链表 + 哈希表),展示核心淘汰逻辑:

class LRUCache {
    private int capacity;
    private Map<String, Node> map;
    private Node head, tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<>();
        this.head = new Node("", "");
        this.tail = new Node("", "");
        head.next = tail;
        tail.prev = head;
    }

    public String get(String key) {
        if (!map.containsKey(key)) return null;
        Node node = map.get(key);
        moveToHead(node);
        return node.value;
    }

    public void put(String key, String value) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.value = value;
            moveToHead(node);
        } else {
            if (map.size() == capacity) {
                Node removed = removeTail();
                map.remove(removed.key);
            }
            Node newNode = new Node(key, value);
            map.put(key, newNode);
            addToHead(newNode);
        }
    }

    private void moveToHead(Node node) {
        removeNode(node);
        addToHead(node);
    }

    private void addToHead(Node node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private Node removeTail() {
        Node node = tail.prev;
        removeNode(node);
        return node;
    }

    static class Node {
        String key, value;
        Node prev, next;
        Node(String k, String v) { this.key = k; this.value = v; }
    }
}
工程提示:生产环境不建议手写 LRU。Redis 的 maxmemory-policy allkeys-lru 和 Caffeine 的 CacheBuilder.newBuilder().maximumSize(10000).build() 均提供了经过工业验证的近似 LRU 实现,性能远优于手写版本。

2.7 缓存基础与 OpenAPI 接口的关联

理解缓存基础是设计 OpenAPI 缓存一致性的前提。在后续章节中,我们将看到:

  • 缓存命中率直接影响接口响应时间的 P99 分位值。
  • 淘汰策略决定了缓存数据在接口变更时的“惯性”——LRU 可能导致旧数据在热点切换时被快速淘汰,而 LFU 可能让过时数据长期滞留。
  • 缓存穿透是 OpenAPI 接口被恶意扫描时的典型攻击面,需要在网关层做布隆过滤器或空值缓存。