缓存分层架构设计:本地缓存 vs 分布式缓存、多级缓存架构(L1/L2/L3)、热点数据识别策略

缓存分层这件事,说白了就是给数据找几个不同级别的「临时仓库」。我刚开始做向量检索系统时,天真地以为一个 Redis 就能搞定一切。结果呢?线上压测时 QPS 刚过万,Redis 的带宽就打满了,CPU 也飙到 80%。后来我才明白——没有分层设计的缓存,在高并发场景下就是纸老虎。

本地缓存 vs 分布式缓存:选型背后的逻辑

先看一个最基础的问题:你的缓存该放在进程内,还是独立部署?

对比维度 本地缓存(L1) 分布式缓存(L2)
访问延迟 纳秒级(内存直接读) 毫秒级(网络 RTT + 序列化)
吞吐能力 单机百万 QPS 集群百万 QPS(受带宽限制)
数据一致性 各节点独立,容易不一致 中心化存储,一致性更好
容量上限 受限于 JVM 堆 / 物理内存 可水平扩展至 TB 级
典型实现 Caffeine、Guava Cache Redis、Memcached

我个人习惯这样选:热点极高、对一致性不敏感的场景,优先上本地缓存。比如向量检索中的「高频词向量」——用户搜「苹果」这个词,100 次里有 80 次命中同一个向量。这时候本地缓存能扛住千万级 QPS,Redis 反而成了瓶颈。

核心原则:本地缓存扛热数据,分布式缓存扛全量。两者不是替代关系,是互补关系。

多级缓存架构:L1 / L2 / L3 怎么搭?

我在项目中遇到过最典型的架构是三层:

  • L1(进程内缓存):Caffeine,存最热的 1% 数据。TTL 设 30 秒,过期后自动失效。
  • L2(分布式缓存):Redis Cluster,存 20% 的次热数据。TTL 设 5 分钟,配合淘汰策略。
  • L3(持久化存储):向量数据库(如 Milvus、Qdrant),存全量数据。作为兜底。

为什么这么分层?你想想看,一次向量查询的链路是这样的:

请求到达 → 查 L1(Caffeine) → 命中?返回。
                    ↓ 未命中
                查 L2(Redis) → 命中?返回,并回写 L1。
                    ↓ 未命中
                查 L3(向量库) → 返回,并回写 L2 和 L1。

嗯,这里要注意:回写策略不能乱来。我曾经踩过一个坑——L3 查完后同时回写 L1 和 L2,结果 L1 的 TTL 设得太长,导致数据更新后用户一直读到旧向量。后来我改成「L3 → L2 → L1」的逐级回写,并且 L1 的 TTL 不超过 30 秒。

避坑指南:L1 的容量一定要设上限。我见过有人把 Caffeine 的 maximumSize 设成 1000 万,结果 GC 把服务搞挂了。建议 L1 容量控制在 JVM 堆的 5% 以内。

热点数据识别策略:怎么知道哪些数据该进 L1?

分层架构搭好了,但「哪些数据放 L1」才是真正的难题。我常用的方法有三种:

1. 基于访问频率的滑动窗口

维护一个固定大小的窗口(比如 1 分钟),统计每个 key 的访问次数。超过阈值就标记为热点,自动提升到 L1。

// 伪代码示例
class HotKeyDetector {
    private final ConcurrentHashMap<String, AtomicInteger> counter = new ConcurrentHashMap<>();
    private final int threshold = 100; // 每分钟超过 100 次算热点

    public void recordAccess(String key) {
        counter.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet();
    }

    public boolean isHot(String key) {
        return counter.getOrDefault(key, new AtomicInteger(0)).get() > threshold;
    }
}

2. 基于 LFU 的近似统计

Caffeine 底层用的就是 TinyLFU 算法。它用 Count-Min Sketch 来估算访问频率,内存开销极小。我个人推荐直接用 Caffeine 的 .maximumSize() + .expireAfterWrite() 组合,它会自动把高频 key 留在缓存里。

3. 业务规则预判

有些热点是可以提前知道的。比如电商大促时的「爆款商品向量」、新闻推荐中的「头条文章向量」。我习惯在系统启动时,通过配置中心下发一份「预置热点列表」,直接预热到 L1。

注意:热点识别不能只靠算法。我曾经遇到过「突发流量导致误判」——某个 key 在 1 秒内被刷了 1000 次,被标记为热点,结果只是爬虫在扫数据。建议加上「持续时间」维度,比如连续 3 个窗口都超过阈值才算热点。

多级缓存架构总览

下面这张图是我在实际项目中用过的分层方案,你可以参考:

多级缓存架构(L1 / L2 / L3) L1 本地缓存 Caffeine / Guava L2 分布式缓存 Redis Cluster L3 持久化存储 向量数据库 热点识别引擎 滑动窗口计数器 (1 分钟窗口,阈值 100) TinyLFU 近似统计 (Count-Min Sketch) 业务规则预判 (配置中心下发热点列表) 输出:热点 key → 提升到 L1 回写策略:L3 → L2 → L1 L1 TTL ≤ 30s,L2 TTL ≤ 5min

分层架构的「坑」与「解」

最后分享几个实战中容易踩的坑:

  • 缓存穿透:L1 和 L2 都没有,请求直接打到 L3。我习惯在 L1 里缓存一个「空值标记」,TTL 设 10 秒,防止恶意 key 打穿。
  • 缓存雪崩:大量 key 同时过期。解决方案是给 TTL 加一个随机偏移量,比如 30 秒 ± 5 秒。
  • 缓存击穿:某个热点 key 过期,瞬间大量请求打到 L3。用互斥锁(如 Redis 的 SETNX)控制只有一个线程去查 L3,其他线程等待。

我的经验:多级缓存不是层数越多越好。三层已经能覆盖 99% 的场景。我见过有人搞了五层,结果维护成本爆炸,延迟反而增加了。记住——每多一层,就多一次网络开销和一致性风险。