1. 缓存穿透
现象
查询缓存、数据库都不存在的数据,大量恶意请求直接打向数据库,压垮 DB。
例:传入不存在的商铺 id,Redis 查不到,每次都去访问数据库。
解决方案
- 缓存空值(缓存 null)查询数据库不存在时,把
null存入 Redis 并设置短过期时间;后续相同请求直接返回空,不再访问数据库。
- 优点:简单直接;缺点:会占用少量 Redis 内存。
- 布隆过滤器把所有合法数据 ID 预先存入布隆过滤器;请求过来先过过滤器,不合法直接拦截,不去查 Redis 与 DB。适合海量数据场景。
- 参数校验:做 ID 格式、范围校验,拦截非法参数;避免 id 被猜测遍历。
业务流程(缓存空值方案)
- 根据 id 查询 Redis 缓存
- 缓存命中:判断是否为空值,空值直接返回;非空返回业务数据
- 缓存未命中:查询数据库
- 数据库存在:写入真实数据到 Redis,返回数据
- 数据库不存在:写入 null 空值到 Redis,结束
2. 缓存击穿(热点 Key 问题)
现象
某一个热点高并发的 key 过期失效,大量并发请求同时打到数据库,瞬间冲击 DB。
区别雪崩:击穿是单个热点 key 失效;雪崩是大量 key 集体失效。
两种解决方案对比
表格
| 方案 | 优点 | 缺点 |
|---|---|---|
| 互斥锁(分布式锁) | 无额外内存;数据一致性好;实现简单 | 线程等待,性能下降;存在死锁风险 |
| 逻辑过期 | 线程不用等待,并发性能高 | 不保证强一致性;消耗内存;业务实现复杂 |
①互斥锁流程
- 请求查询 Redis 缓存,命中直接返回
- 缓存未命中,尝试获取分布式互斥锁
- 获取锁失败:休眠重试,再次查询缓存
- 获取锁成功:查询数据库,重建缓存写入 Redis,释放锁,返回数据
核心:同一时刻只允许一个线程去数据库重建缓存,其他线程等待重试。
②逻辑过期
不给 key 设置真实 TTL 过期,在 value 内部存一个逻辑过期时间;线程发现逻辑时间过期,开启异步线程更新缓存,旧数据直接返回给用户,不阻塞请求。
@Override public Result queryById(Long id) { //缓存穿透 //解决击穿 Shop shop = queryWithMutex(id); //返回 if (shop == null) { return Result.fail("店铺不存在"); } return Result.ok(shop); } public Shop queryWithMutex(Long id) { String LockKey = null; Shop byId = null; try { //查ID String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id); //存在返回,不存在查数据库 if(StrUtil.isNotBlank(shopJson)) { Shop shop = JSONUtil.toBean(shopJson, Shop.class); return shop; } if(shopJson != null) { return null; } //实现缓存重建 LockKey = "lock:shop" + id; boolean isLock = tryLock(LockKey); if (!isLock) { Thread.sleep(50); return queryWithMutex(id); } byId = getById(id); //数据库结果 if (byId == null) { //将空值写入redis stringRedisTemplate.opsForValue().set("cache:shop" + id,"",10, TimeUnit.MINUTES); return null; } stringRedisTemplate.opsForValue().set("cache:shop" + id,JSONUtil.toJsonStr(byId),30, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { unlock(LockKey); } return byId; } public Shop queryWithPassThrough(Long id) { //查ID String shopJson = stringRedisTemplate.opsForValue().get("cache:shop" + id); //存在返回,不存在查数据库 if(StrUtil.isNotBlank(shopJson)) { Shop shop = JSONUtil.toBean(shopJson, Shop.class); return shop; } if(shopJson != null) { return null; } Shop byId = getById(id); //数据库结果 if (byId == null) { //将空值写入redis stringRedisTemplate.opsForValue().set("cache:shop" + id,"",10, TimeUnit.MINUTES); return null; } stringRedisTemplate.opsForValue().set("cache:shop" + id,JSONUtil.toJsonStr(byId),30, TimeUnit.MINUTES); return byId; } private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1",10,TimeUnit.MINUTES); return BooleanUtil.isTrue(flag); } private void unlock(String key) { stringRedisTemplate.delete(key); }3. 缓存雪崩
现象
大量 key 同一时间集体过期,或者 Redis 整体宕机,大量请求绕过缓存全部涌向数据库,数据库压力雪崩。
解决方案
- 过期时间添加随机值:TTL 加上随机偏移,打散 key 失效时间,避免集体过期。
- Redis 集群高可用:主从、哨兵、集群,避免 Redis 宕机。
- 限流 & 降级:网关层限流,服务降级,保护数据库。
- 多级缓存:本地 Caffeine 缓存 + Redis 多级缓存,Redis 故障可以兜底。
三者对比总结(面试速记)
表格
| 问题 | 诱因 | 核心特点 |
|---|---|---|
| 缓存穿透 | 查询根本不存在的数据 | 缓存、数据库都没有该数据 |
| 缓存击穿 | 单个热点 key 过期 | 一个热点 key 失效,并发打 DB |
| 缓存雪崩 | 大量 key 同时过期 / Redis 宕机 | 大批量 key 失效,大规模压垮数据库 |
面试常考区分点:
- 穿透:查不存在的数据;
- 击穿:一个热点 key 过期;
- 雪崩:大批量 key 失效 /redis 挂掉。