3. 热点Key的本地缓存策略:Caffeine与Redis两级缓存架构
3.1 两级缓存架构设计背景
在热点Key场景下,大量请求集中访问少数Key,即使Redis单机QPS可达10万+,网络IO和序列化开销仍会成为瓶颈。引入本地缓存(如Caffeine)作为L1,Redis作为L2,形成两级缓存架构,可显著降低Redis负载和响应延迟。
核心收益: 热点Key的读取延迟从毫秒级降至微秒级(本地内存访问约50-100ns),Redis QPS下降60%-80%。
3.2 两级缓存架构图
3.3 Caffeine本地缓存配置
Caffeine是Java 8+高性能本地缓存库,提供基于W-TinyLFU的淘汰策略,相比Guava Cache命中率更高、内存控制更精准。
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| maximumSize | 10,000 | 最大缓存条目数,防止内存溢出 |
| expireAfterWrite | 60s | 写入后过期,保证数据新鲜度 |
| expireAfterAccess | 30s | 访问后过期,淘汰冷数据 |
| refreshAfterWrite | 30s | 写入后自动刷新,避免缓存击穿 |
| recordStats | true | 开启统计,用于监控命中率 |
最佳实践: expireAfterWrite 应小于 Redis 中对应 Key 的 TTL,确保本地缓存先过期,优先从 Redis 获取最新数据。
3.4 本地缓存过期与更新机制
两级缓存的核心挑战是数据一致性。本地缓存过期后,需要从Redis拉取最新数据并回填;同时需要处理Redis数据变更时主动失效本地缓存。
3.4.1 被动过期 + 主动刷新
- 被动过期: Caffeine 基于 expireAfterWrite 自动淘汰过期条目,下次访问触发回填。
- 主动刷新: 使用 refreshAfterWrite 在过期前异步刷新,避免单个热点Key过期瞬间大量请求穿透到Redis。
// Caffeine 配置示例
Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(60, TimeUnit.SECONDS)
.refreshAfterWrite(30, TimeUnit.SECONDS)
.recordStats()
.build(key -> {
// 回填逻辑:从Redis获取数据
return redisClient.get(key);
});
3.4.2 主动失效(缓存淘汰通知)
当Redis中的数据发生写操作(更新/删除)时,需要同步失效所有应用节点的本地缓存。常用方案:
| 方案 | 实现方式 | 延迟 | 复杂度 |
|---|---|---|---|
| Redis Keyspace Notification | 订阅 __keyevent@0__:expired/del 频道 | 秒级 | 低 |
| 消息队列广播 | 写操作后发送MQ广播,各节点消费后失效本地缓存 | 毫秒级 | 中 |
| 版本号对比 | 本地缓存携带版本号,每次对比Redis版本号决定是否失效 | 微秒级 | 高 |
注意: Keyspace Notification 在 Redis 高负载时可能丢失消息,生产环境建议使用消息队列广播方案保证可靠性。
3.5 两级缓存读写流程
3.6 性能基准对比
在8核16G服务器、1000并发线程、热点Key占比20%的测试条件下,两级缓存与单层Redis的对比结果:
| 指标 | 单层Redis | 两级缓存(Caffeine + Redis) | 提升幅度 |
|---|---|---|---|
| 平均延迟(P50) | 1.2ms | 0.08ms | 15x |
| P99延迟 | 5.8ms | 0.45ms | 12.9x |
| Redis QPS | 85,000 | 22,000 | 下降74% |
| 系统吞吐量 | 42,000 req/s | 68,000 req/s | 1.6x |
| 本地缓存命中率 | — | 87.3% | — |
关键结论: 两级缓存架构在热点Key场景下,P99延迟降低至原来的1/13,Redis负载降低74%,系统吞吐量提升60%。本地缓存命中率87.3%意味着仅12.7%的请求需要穿透到Redis,极大缓解了Redis压力。
3.7 避坑指南
- 缓存雪崩: 避免所有本地缓存同时过期,设置随机过期时间偏移(±10%)。
- 内存泄漏: 定期监控Caffeine的evictionCount和estimatedSize,确保内存回收正常。
- 序列化开销: 本地缓存存储对象引用,避免重复序列化;Redis侧使用Protobuf或Kryo压缩。
- 热点Key动态识别: 结合Redis的hotkey分析工具(如redis-cli --hotkeys)动态调整本地缓存策略。