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 缓存淘汰策略对比图
下图展示了三种淘汰策略在相同数据访问序列下的淘汰行为差异,帮助理解不同策略对缓存内容的影响:
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 接口被恶意扫描时的典型攻击面,需要在网关层做布隆过滤器或空值缓存。