Redis 和 Caffeine 的区别是什么?
结论:两者不是替代关系,而是不同层级的缓存。Caffeine 是进程内(本地)缓存,访问走 JVM 堆,纳秒级、零网络;Redis 是进程外(分布式)缓存,访问走网络,毫秒级、多实例共享。真实系统普遍是 Caffeine 做 L1、Redis 做 L2,难点不在"选哪个",而在"L1 怎么失效"。
一、定位差异对照
| 维度 | Caffeine(本地缓存) | Redis(分布式缓存) |
|---|---|---|
| 数据位置 | JVM 堆内,与业务同进程 | 独立进程 / 独立机器 |
| 访问延迟 | 百纳秒级 | 0.2~2 ms(同机房) |
| 容量上限 | 受堆大小与 GC 约束 | 受物理内存约束,可到数百 GB |
| 一致性 | 各实例各自一份,天然不一致 | 全局一份,天然一致 |
| 淘汰算法 | W-TinyLFU(窗口 LRU + 频率草图) | 近似 LRU / LFU / random / ttl |
| 持久化 | 无,重启即空 | RDB / AOF |
| 扩展方式 | 加堆、加实例(各自一份) | 主从、哨兵、Cluster |
| 适用数据 | 极热、可容忍短暂过期、体积小 | 全量热点数据、需跨实例共享 |
| 典型故障 | GC 压力、堆外泄漏 | 网络抖动、缓存击穿、集群切换 |
二、Caffeine 为什么快,以及快到什么程度
业务线程
│ get(key)
├─► Caffeine:ConcurrentHashMap 定位 → 命中直接返回对象引用(~100 ns)
│
└─► Redis:序列化 → TCP 往返 → 反序列化(~0.5 ms,跨机房 2~10 ms)
└─ 一次网络往返 ≈ 本地命中的 5000 倍
Caffeine 的核心不是"用 HashMap",而是三件事叠加:
- 读路径无锁:读操作只更新频率草图(Count-Min Sketch + 布隆过滤器),不阻塞;
- 写路径分段:按 key 哈希分到不同缓冲区,异步批量刷入淘汰结构,减少竞争;
- W-TinyLFU 淘汰:小窗口 LRU 吸收突发流量,主区域按频率准入,避免"扫表式"的一次性 key 冲掉热点。
// Caffeine:按大小 + 过期时间 + 异步刷新
Cache<String, User> local = Caffeine.newBuilder()
.maximumSize(100_000)
.expireAfterWrite(Duration.ofMinutes(5))
.refreshAfterWrite(Duration.ofMinutes(1))
.recordStats()
.build(key -> loadFromRedisOrDb(key)); // 未命中时同步加载,天然防击穿
三、两级缓存的读写与失效
读:L1 命中 → 返回
L1 未命中 → L2(Redis) 命中 → 回填 L1 → 返回
L2 未命中 → 查 DB → 写 L2 → 写 L1 → 返回
写:更新 DB → 删 L2 → 广播失效消息(Redis Pub/Sub 或 MQ)→ 各实例删 L1
// 失效广播:每个实例订阅自己的频道,收到后只删本地 key
public void evict(String key) {
redis.delete("user:" + key); // 先删 L2
redis.convertAndSend("cache:evict", key); // 再广播
}
@RedisListener(topic = "cache:evict")
public void onEvict(String key) {
local.invalidate(key); // 各实例删 L1
}
要点:先删 L2 再广播,顺序反了会出现"某个实例旧值回填 L2"的窗口;广播本身不可靠(Pub/Sub 不持久化),所以要给 L1 配一个较短的 expireAfterWrite 兜底,把最长不一致时间压到秒级。
四、常见追问
- 问:既然有 Redis,为什么还要 Caffeine?答:挡住超高 QPS 的热点 key,把网络与 Redis 的 CPU 省下来;单机 QPS 从几万抬到几十万,Redis 只承担未命中的那部分。
- 问:本地缓存会不会内存泄漏?答:会。用 maximumSize + weakValues 兜底,避免把大对象或线程上下文缓存进去;排查时看 GC 日志与堆 dump 里的 retained size。
- 问:L1 和 L2 的数据不一致窗口怎么量化?答:最长不一致 ≈ 广播延迟 + L1 的 expireAfterWrite。业务能接受 1~5 秒就用短 TTL 兜底,不能接受就不要用 L1,或改成"按版本号校验"。
- 问:Caffeine 的 W-TinyLFU 与 Redis 的 LRU 谁更准?答:在"突发扫描 + 稳定热点"混合负载下 TinyLFU 命中率更高;Redis 的 LRU 是采样近似,为省内存牺牲了精度。
- 问:多级缓存下缓存击穿怎么处理?答:Caffeine 的
build(loader)自带单 key 串行加载,Redis 层再用 SETNX 互斥,DB 层用单飞(singleflight)合并,三层各挡一次。
小结
- Caffeine 解决"快",Redis 解决"共享",二者是层级关系而不是竞品。
- 本地缓存必须回答三个问题:容量多大、最长不一致多久、失效怎么广播。
- 多级缓存的收益来自命中率分层,成本来自一致性复杂度;没有失效兜底的 L1 不要上生产。