从碎片化到一体化:Java分布式缓存的“三级跳”实战
文章目录
- 每日一句正能量
- 前言
- 一、痛点复盘:Redis-only的“三大裂缝”
- 二、架构设计:「三级缓存 + 一体化监控」
- 1. L1:本地缓存(Caffeine)
- 2. L2:Redis集群(分片 + 拆分)
- 3. L3:DB + 异步刷新
- 三、代码实战:三级缓存落地
- ① 布隆过滤器前置(Redisson)
- ② 本地缓存(Caffeine + Spring Cache)
- ③ 热Key拆分(随机后缀)
- 四、性能对比实测
- 五、一体化监控:让缓存“看得见、喊得应”
- 1. 指标层(Micrometer + Prometheus)
- 2. 追踪层(SkyWalking)
- 3. 告警层(AlertManager)
- 六、上线灰度与踩坑日记
- 1. 灰度策略
- 2. 坑 1:Caffeine 的 `refreshAfterWrite` 造成 Dog-Pile
- 3. 坑 2:布隆过滤器误杀率飙高
- 4. 坑 3:跨机房消息时延导致“缓存漂移”
- 七、可扩展演进路线
- 八、回顾与建议

每日一句正能量
人生,就是一个修炼的过程,何必用这一颗不平的心看待人和事,作践了自己,辜负了岁月。
前言
背景:2023年Q4,我们团队负责的汽车零部件MES系统日订单量突破12万,原有「Redis-only」架构出现缓存穿透、热Key失效、网络抖动三大痛点。经过6周重构,实现了「三级缓存 + 一体化监控」的新架构,CPU使用率下降45%,P99延迟从800ms降至180ms。本文将完整复盘这一实战过程,希望能为Java分布式缓存设计提供参考。
一、痛点复盘:Redis-only的“三大裂缝”
问题 | 现象 | 根因 |
---|---|---|
缓存穿透 | 1800 QPS打到DB,Redis命中率仅62% | 缺乏布隆过滤器,大量无效SKU |
热Key失效 | 个别SKU查询QPS 5万,Redis单核飙红 | 无热Key拆分,无本地缓存 |
网络抖动 | P99偶发800ms,Redis RTT突增 | 跨机房调用,无本地兜底 |
二、架构设计:「三级缓存 + 一体化监控」

1. L1:本地缓存(Caffeine)
- 作用:热Key兜底,P99 < 5ms
- 容量:100万Key,TTL 5分钟
- 策略:LRU + TTL混写
2. L2:Redis集群(分片 + 拆分)
- 作用:海量数据,横向扩展
- 策略:
- 布隆过滤器前置(Redisson)
- 热Key拆分(随机后缀)
- 分片算法:CRC16(slot) + 随机后缀
3. L3:DB + 异步刷新
- 作用:最终兜底,避免穿透
- 策略:
- 布隆过滤器拦截
- 异步刷新(Spring Async)
三、代码实战:三级缓存落地
① 布隆过滤器前置(Redisson)
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("sku:bf");
bloomFilter.tryInit(1000000L, 0.01);
// 写入
bloomFilter.add(sku);
// 查询
if (!bloomFilter.contains(sku)) {return Optional.empty();
}
② 本地缓存(Caffeine + Spring Cache)
@Configuration
@EnableCaching
public class CacheConfig {@Beanpublic CacheManager cacheManager() {CaffeineCacheManager manager = new CaffeineCacheManager();manager.setCaffeine(Caffeine.newBuilder().maximumSize(1000000).expireAfterWrite(5, TimeUnit.MINUTES).recordStats());return manager;}
}
使用:
@Cacheable(value = "sku", key = "#sku")
public SkuDTO querySku(String sku) {return redisTemplate.opsForValue().get("sku:" + sku);
}
③ 热Key拆分(随机后缀)
String suffix = String.valueOf(ThreadLocalRandom.current().nextInt(0, 100));
String key = "sku:" + sku + ":" + suffix;
return redisTemplate.opsForValue().get(key);
四、性能对比实测
指标 | 重构前 | 重构后 | 提升倍数 |
---|---|---|---|
缓存命中率 | 62 % | 96 % | 1.55 × |
P99 延迟 | 800 ms | 180 ms | 4.4 × |
平均 CPU | 78 % | 43 % | 1.8 × |
DB 峰值 QPS | 1 800 | 120 | 15 × |
热 Key 单核负载 | 100 % | 18 % | 5.6 × |
跨机房 RTT 抖动 | 60 ms~800 ms | 0 ms(L1 命中) | ∞ |
注:压测条件 12 万订单 / 分钟,SKU 池 300 万,热点 SKU 占比 0.2 %,8 核 16 G 容器 × 40 实例。
五、一体化监控:让缓存“看得见、喊得应”
1. 指标层(Micrometer + Prometheus)
management:metrics:export:prometheus:enabled: truetags:application: mes-cachetier: l1/l2/l3
关键指标:
cache_get_total{ tier="L1" }
cache_miss_total{ tier="L2", reason="bloom_filter" }
redis_hot_key_qps{ key_suffix="~split" }
2. 追踪层(SkyWalking)
- 给每一次
CacheInterceptor
生成独立 Span,自动打 Tag:cache.type
、cache.key
、hit/miss
- 通过 Trace ID 可直接关联到下游 SQL,定位“缓存穿透 → DB 抖动”全链路。
3. 告警层(AlertManager)
规则 | 阈值 | 动作 |
---|---|---|
L1 命中率 < 90 % | 持续 2 min | 飞书 @缓存值班 |
单 Key QPS > 3 万 | 1 min | 自动触发“热 Key 拆分”脚本 |
DB QPS > 300 | 1 min | 降级开关 + 熔断线程池 |
热 Key 拆分脚本(Redis Lua)
-- 参数:KEYS[1]=原始热Key,ARGV[1]=分片数
local shard = math.random(0, tonumber(ARGV[1])-1)
redis.call('RENAMENX', KEYS[1], KEYS[1]..':s'..shard)
return shard
通过 Kubernetes Job 定时巡检,30 s 内完成“识别-拆分-通知”闭环。
六、上线灰度与踩坑日记
1. 灰度策略
- 按 订单号尾号 灰度,initial 5 % → 30 % → 100 %,每阶段观察 24 h。
- 双写标记:在消息头加上
x-cache-version:v2
,兼容回滚。
2. 坑 1:Caffeine 的 refreshAfterWrite
造成 Dog-Pile
现象:本地缓存过期瞬间,40 实例并发回源 Redis,QPS 毛刺 21 万。
根因:refreshAfterWrite
不会加全局锁,仅阻塞当前线程。
解法:
- 采用
AsyncCache
+LoadingCache#refresh(key)
单线程池预热; - 引入 Redisson 分布式信号量,限制并发回源 ≤ 8。
3. 坑 2:布隆过滤器误杀率飙高
现象:新品 SKU 上架后,L2 命中率骤降 8 %。
根因:tryInit
仅系统启动时执行一次,后续新增 SKU 不再同步。
解法:
- 监听 Canal 的
INSERT
事件,异步bloomFilter.add()
; - 每晚离线 Job 全量重建,防止 Hash 冲突累积。
4. 坑 3:跨机房消息时延导致“缓存漂移”
现象:用户修改 SKU 图片后,L1 与 L2 数据不一致最长 1.5 min。
根因:MQ 消息先到 B 机房,缓存清除晚于 A 机房查询。
解法:
- 所有缓存更新操作带
timestamp
字段,采用 Last-Write-Wins; - 引入 Redis Stream 做全局事件总线,各机房消费本地副本,延迟 < 200 ms。
七、可扩展演进路线
阶段 | 目标 | 关键技术 |
---|---|---|
2024 Q2 | 多云单元化 | Redis-Cluster + Global-Cache-Proxy,基于 CRDT 同步 |
2024 Q4 | 计算缓存融合 | 将库存校验逻辑下沉至 Redis Functions,减少 1 次 RTT |
2025 Q2 | Serverless 缓存 | 采用 AWS ElastiCache Serverless,自动弹性,按请求计费 |
八、回顾与建议
- 先监控,再优化:没有 Trace 的缓存重构就是“蒙眼跳崖”。
- 灰度是生命线:哪怕再小的改动,也要具备 5 分钟回滚能力。
- 热 Key 是永恒的敌人:随机拆分只是“缓兵之计”,最终要让业务 Key 设计 去热点化。
- 本地缓存不是银弹:CPU 节省的代价是 内存 和 一致性,务必做好容量评估与版本漂移控制。
最后,把我们在 Wiki 首页的标语送给大家:
“像呵护 CPU 一样呵护缓存,像监控现金一样监控延迟。”
祝各位在分布式缓存的路上,跳得稳、跳得远!
转载自:https://blog.csdn.net/u014727709/article/details/151209841
欢迎 👍点赞✍评论⭐收藏,欢迎指正