我从本地缓存到Redis的迁移实战:解决3类一致性难题
上季度,我帮一家做SaaS进销存系统的客户重构了缓存层。当时系统跑了两年,日活不算高,但SKU数量膨胀到了50万+,原有的Guava Cache单机模式彻底扛不住了。最棘手的是,三个分支服务同时读写库存,经常出现“超卖”或者“库存幽灵”——A节点扣了库存,B节点还读着旧值。客户那边的运营团队每天要花两小时人工对账,这次项目目标很明确:一周内完成从本地内存缓存到 Redis 的平滑迁移,且不能停机超过10分钟。
为什么本地缓存撑不住:一致性陷阱
说实话,初期用本地缓存是为了榨取每一毫秒的延迟优势。Guava的 API 确实顺手,配置简单,命中率也能做到 99% 以上。但问题出在“多实例”上。我们的微服务集群有 4 个节点,每个节点都维护着自己的内存缓存副本。当Node-1更新了一条商品的价格,Node-2直到 TTL 过期前,依然返回旧价格。
当时我觉得加个广播通知就行,让所有节点监听消息,收到后失效本地缓存。试了一圈发现,这招在业务高峰时段根本不可靠。消息队列偶尔积压,或者某个节点网络抖动,就会导致部分节点缓存失效失败,数据依然是脏的。更坑的是,内存缓存是 JVM 托管的,一旦发生 Full GC,缓存加载的停顿期会把下游数据库打挂。
我们最终决定引入 Redis。这里的决策关键在于:是只缓存“热数据”,还是全量迁移?方案A是只把商品详情放 Redis,库存逻辑还在本地内存做原子操作;方案B是库存和详情全部下沉到 Redis,利用 Redis 原子性。我选了方案B。虽然读性能相比本地内存有 20% 左右的损耗,但换来了全局一致性。在电商场景下,比毫秒级延迟更重要的是“绝对正确”。
为了平滑迁移,我写了个双写适配器:
```java
public class CacheMigrator {
// 旧逻辑:写本地 Guava Cache
// 新逻辑:写 Redis Cluster
public void save(Product p) {
localCache.put(p);
// 异步写入 Redis,保证最终一致
redisTemplate.opsForValue().set(p.key(), p, 3600, TimeUnit.SECONDS);
}
}
```
踩坑实录:雪崩与穿透的混合双打
迁移到 Redis 后,前三个晚上我睡不着。因为遇到了典型的“缓存雪崩”与“穿透”混合攻击。
第一次事故发生在周二凌晨 3 点。我们的缓存 key 生成规则里有个 Bug,导致大量请求命中了同一个热点 Key 的空值。当这个 Key 过期时,几万个请求瞬间涌向 MySQL,直接拖垮了数据库连接池。监控告警响了 20 分钟我才反应过来。
排查过程很痛苦。一开始我以为是 Redis 挂了,Ping 命令正常。再看代码,发现我们为了防穿透,给“不存在的商品”也设置了缓存,TTL 只有 30 秒。这反而加剧了问题——这 30 秒后,所有请求又同时去查库。
修复方案分两步走。第一步,针对热点 Key,我引入了“本地缓存 + Redis”的两级策略。核心逻辑是:Redis 作为一级缓存,JVM 堆内保留一个极小的ConcurrentHashMap作为二级缓存,专门存 Top 100 的热点数据。这样即使 Redis 抖动,也有兜底。
第二步,针对穿透问题,我改用了BloomFilter拦截无效请求。
```java
// 使用 Redisson 实现布隆过滤器
RBloomFilter bloomFilter = redissonClient.getBloomFilter("product_filter");
bloomFilter.add(productId);
if (!bloomFilter.contains(productId)) {
return null; // 直接返回,不查库
}
```
有意思的是,BloomFilter 引入后,无效请求减少了 85%,但内存占用增加了 200MB。当时我纠结过是否值得,但考虑到数据库的 QPS 压力已经接近瓶颈,这点内存换稳定,划算。
迁移后的架构演进与反思
项目上线一周后,系统的平均响应时间从 45ms 降到了 12ms。虽然绝对值提升不算夸张,但 P99 延迟从 200ms 压到了 50ms 以内,稳定性显著提升。
这次实战让我意识到,缓存选型不是“哪个更快用哪个”,而是“哪里最脆弱补哪里”。本地缓存适合读多写极少、对一致性要求不高的场景,比如字典表;而分布式缓存是解决多节点一致性的唯一解,代价是网络开销。
现在回头看,如果当时能早点上 Redis Cluster 并配合好Pipeline批量操作,性能还能再压榨一点。但在工程实践中,先解决“正确性”,再追求“极致性能”,这个顺序不能反。
本文基于实际项目经验整理,欢迎在评论区交流技术问题。