只要做Java后端开发,线上服务基本都离不开Redis缓存。缓存确实能大幅提升接口QPS、降低数据库压力,但很多人只用到了基础的缓存查询、存储功能,完全忽略了缓存穿透、缓存击穿、缓存雪崩这三大经典问题。
我之前参与过电商项目线上压测,发现很多接口看似有缓存加持,一旦遇到恶意请求、热点Key过期、大批量缓存同时失效,数据库瞬间被打满,CPU飙升、接口大面积超时,严重时直接引发服务雪崩。
这三种缓存问题看着相似,但成因和解决方案完全不同。网上很多教程讲得太笼统,落地性很差。今天我结合生产真实落地经验,逐一拆解三种问题的成因、风险,配套可直接上线的代码+配置方案,帮大家彻底根治Redis缓存线上隐患。
一、快速区分三大缓存问题(通俗易懂)
很多开发工作多年,还是容易混淆三种场景,我用最直白的话总结:
1. 缓存穿透:查询不存在的数据,缓存查不到、数据库频繁查询,恶意空请求打垮DB。
2. 缓存击穿:热点Key过期瞬间,大量并发请求直接打到数据库,瞬间压垮DB。
3. 缓存雪崩:大批量缓存同时过期/Redis宕机,所有请求全部落库,整体服务瘫痪。
搞清楚场景差异,才能针对性优化,避免盲目堆砌解决方案。
二、缓存穿透:解决方案与实战代码
缓存穿透一般是用户传非法ID、爬虫恶意刷接口导致的。比如查询id=-1、随机乱数,Redis和数据库都没有数据,每次请求都会穿透到DB,毫无防护能力。
生产最优方案:空值缓存 + 布隆过滤器。小项目用空值缓存足够,高并发电商项目叠加布隆过滤器拦截无效Key。
落地代码(空值缓存)
@Service public class GoodsServiceImpl implements GoodsService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private GoodsMapper goodsMapper; @Override public Goods getGoodsById(Long id) { String key = "goods:info:" + id; // 1.先查缓存 Object cacheObj = redisTemplate.opsForValue().get(key); if (cacheObj != null) { // 判断是否是空值缓存 if ("null".equals(cacheObj)) { return null; } return (Goods) cacheObj; } // 2.缓存未命中,查询数据库 Goods goods = goodsMapper.selectById(id); if (goods == null) { // 空值缓存,过期时间短,避免占用内存 redisTemplate.opsForValue().set(key, "null", 60, TimeUnit.SECONDS); } else { redisTemplate.opsForValue().set(key, goods, 30, TimeUnit.MINUTES); } return goods; } }
空值缓存可以拦截大量重复无效请求,成本极低、落地零难度,是中小型项目首选方案。
三、缓存击穿:热点Key失效并发解决方案
秒杀商品、首页热门数据这类热点Key,访问量极大。一旦缓存过期,瞬间海量并发请求直接穿透到数据库,极易导致DB瞬间卡死。
生产最优方案:互斥锁 + 热点Key永不过期,这里推荐Redisson分布式锁方案,性能和稳定性最优。
分布式锁防击穿核心代码
@Override public Goods getHotGoods(Long id) { String key = "goods:hot:" + id; String lockKey = "lock:goods:" + id; Goods goods = (Goods) redisTemplate.opsForValue().get(key); // 缓存存在直接返回 if (goods != null) { return goods; } // 缓存不存在,加分布式锁,只放行一个请求查库 RLock lock = redissonClient.getLock(lockKey); try { lock.lock(); // 二次查询缓存,防止重复查库 goods = (Goods) redisTemplate.opsForValue().get(key); if (goods != null) { return goods; } // 查询数据库回填缓存 goods = goodsMapper.selectById(id); if (goods != null) { // 热点数据可设置较长过期时间,接近永不过期 redisTemplate.opsForValue().set(key, goods, 60, TimeUnit.MINUTES); } } finally { lock.unlock(); } return goods; }
通过分布式锁控制,保证同一时刻只有一个请求查库回填缓存,彻底杜绝热点Key击穿问题。
四、缓存雪崩:批量失效终极解决配置
缓存雪崩的核心原因:大量Key设置了相同过期时间,整点批量过期,瞬间所有缓存失效,请求全部打满数据库。
最实用的解决方式:过期时间随机偏移,打散缓存过期时间,避免集中失效。同时搭配Redis高可用集群,防止单机宕机引发雪崩。
过期时间打散代码
// 基础过期时间30分钟 long baseTime = 30 * 60; // 随机偏移 0~600秒,打散批量过期 long randomTime = new Random().nextInt(600); redisTemplate.opsForValue().set(key, goods, baseTime + randomTime, TimeUnit.SECONDS);
仅仅这几行代码,就能彻底解决绝大多数缓存雪崩场景,改造量极小、收益极高。
五、生产环境完整兜底策略
除了针对性解决方案,线上项目建议统一配置兜底机制,层层防护:
1. Redis高可用:线上必须部署主从+哨兵/集群,杜绝单机故障;
2. 服务熔断降级:配合Sentinel、Resilience4j,Redis异常时直接返回默认数据,不打DB;
3. 禁止固定过期时间:所有业务缓存必须加随机偏移;
4. 热点Key监控:实时监控大Key、热Key,提前人工兜底。
六、总结
很多人把三种缓存问题混为一谈,导致优化方案堆砌、代码冗余却没解决根本问题。简单复盘核心逻辑:穿透防空查、击穿防热点、雪崩防批量失效。
本文提供的代码和配置都是经过线上验证的生产方案,轻量化、无冗余,适配90%以上的中小型后端项目。只要严格按照这套规范落地,基本可以彻底杜绝Redis缓存引发的线上故障,大幅提升服务稳定性。

