文章目录
- 一、缓存穿透:查不存在的数据,绕过缓存直连DB
-
- [1. 什么是穿透](#1. 什么是穿透)
- [2. 四种解决方案](#2. 四种解决方案)
- 二、缓存击穿:热点key过期,并发打爆数据库
-
- [1. 概念区分](#1. 概念区分)
- [2. 主流解决办法](#2. 主流解决办法)
- 三、缓存雪崩:大批量key同时过期,全量请求落库
-
- [1. 雪崩vs击穿区别](#1. 雪崩vs击穿区别)
- [2. 完整解决方案](#2. 完整解决方案)
- 四、分布式锁:集群环境并发数据安全必备
-
- [1. 为什么需要分布式锁](#1. 为什么需要分布式锁)
- [2. Redis分布式锁踩坑全过程](#2. Redis分布式锁踩坑全过程)
-
- 坑1:只用SETNX上锁,无过期
- [坑2:SETNX + EXPIRE两条命令,非原子](#坑2:SETNX + EXPIRE两条命令,非原子)
- 坑3:锁被其他线程误删
- 坑4:判断UUID+DEL删除,非原子操作
- [3. 合格Redis分布式锁四大硬性要求](#3. 合格Redis分布式锁四大硬性要求)
- [4. SpringBoot简易代码实现](#4. SpringBoot简易代码实现)
- 五、总结
一、缓存穿透:查不存在的数据,绕过缓存直连DB
1. 什么是穿透
用户查询数据库、缓存都不存在 的数据,比如传-1、负数用户ID、随机垃圾参数。
缓存查不到数据不会写入空记录,请求每次都会落到MySQL。黑客恶意批量请求不存在key,会直接压垮数据库。
2. 四种解决方案
- 缓存空值(最简单)
查询DB返回null时,往Redis存入空字符串,设置短期过期(3~5分钟)。相同请求下次直接拦截,不用查库。
缺点:会产生大量无效空key,占用少量内存。 - 参数白名单校验
提前规定合法ID范围,前端/接口层直接拦截非法参数,不进入缓存查询逻辑。适合ID自增、固定枚举场景。 - 布隆过滤器(高并发最优)
把所有合法数据ID存入布隆过滤器,请求先过过滤器,不存在直接拦截。
优点:内存占用极小;缺点:存在微小误判概率,不支持删除数据。 - 实时监控预警
监控Redis命中率,命中率骤降立刻排查异常IP,配置黑名单限流。
二、缓存击穿:热点key过期,并发打爆数据库
1. 概念区分
和穿透完全不同:数据真实存在,只是缓存key刚好过期。秒杀商品、首页热点资讯这类高并发key过期瞬间,成千上万个请求同时去查数据库,瞬间压垮DB。
2. 主流解决办法
- 热点key永不过期
提前在项目启动/流量低谷时把热门数据写入Redis,代码不设置expire,后台定时主动更新缓存,杜绝过期场景。 - 过期时间随机偏移
给同类key统一过期时间基础上加随机秒数(1~30分钟随机),避免大批量热点同时失效。 - Redis互斥锁(SETNX/SET NX EX)
缓存失效时,只放行一个线程查询DB更新缓存,其他线程等待重试,避免并发查库。
核心命令:set hot_goods_lock 1 nx ex 5,原子加锁,避免死锁。
三、缓存雪崩:大批量key同时过期,全量请求落库
1. 雪崩vs击穿区别
- 击穿:单个热点key过期,并发打库
- 雪崩:大批量key集中同一时间过期,大量请求同时访问数据库,直接宕机
2. 完整解决方案
- 过期时间打散(最常用)
统一过期时间追加随机值,消除集中失效的时间点。 - 多级缓存架构
Nginx本地缓存 + Redis分布式缓存 + 本地Caffeine缓存,多层兜底,Redis崩了还有上层拦截。 - 加队列/限流削峰
过期更新请求放入队列,限制同时访问DB的线程数量,控制数据库并发压力。 - 后台定时主动刷新
不用等key过期再更新,定时任务提前刷新缓存,从根源杜绝集体过期。
四、分布式锁:集群环境并发数据安全必备
1. 为什么需要分布式锁
单机项目synchronized、Lock锁只作用单个JVM。分布式多台服务器部署时,本地锁失效,多服务同时操作共享数据(库存、计数器)会出现超卖、数据错乱,需要跨服务的分布式锁。
主流实现对比:
- 数据库锁:性能差,并发高容易堵库
- Zookeeper锁:可靠性高,性能弱
- Redis锁:性能最优,互联网项目首选
2. Redis分布式锁踩坑全过程
坑1:只用SETNX上锁,无过期
redis
setnx lock_user 1
del lock_user
业务代码异常崩溃,锁永远不释放,直接死锁。
坑2:SETNX + EXPIRE两条命令,非原子
redis
setnx lock_user 1
expire lock_user 10
上锁成功后Redis宕机,过期命令没执行,依旧死锁。
解决:一条命令原子上锁
redis
set lock_user uuid nx ex 10
NX不存在才创建,EX设置过期,单命令原子执行。
坑3:锁被其他线程误删
线程A业务卡顿,锁自动过期;线程B抢到锁执行业务,A执行完手动删除B的锁。
解决:锁value存入UUID,删锁前校验持有者身份,只删自己的锁。
坑4:判断UUID+DEL删除,非原子操作
先GET对比UUID再DEL,中间锁过期被抢走,依然误删。
终极方案:Lua脚本保证解锁原子性
lua
-- KEYS[1]锁key,ARGV[1]当前线程uuid
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
整个脚本一次性执行,不存在中间间隙,完美避免误删。
3. 合格Redis分布式锁四大硬性要求
- 互斥性:同一时间只能一个客户端持有锁
- 防死锁:锁自带过期时间,宕机自动释放
- 身份校验:谁加锁谁解锁,不能删除别人的锁
- 原子操作:加锁、解锁全过程原子执行
4. SpringBoot简易代码实现
java
@GetMapping("/stock/test")
public String stockTest() {
// 生成唯一标识
String uuid = UUID.randomUUID().toString();
String lockKey = "stock_lock";
// 原子上锁,过期3秒
Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 3, TimeUnit.SECONDS);
if (lock) {
try {
// 扣库存业务
String stockStr = redisTemplate.opsForValue().get("goods_stock");
int stock = Integer.parseInt(stockStr);
if (stock > 0) {
redisTemplate.opsForValue().set("goods_stock", stock - 1);
return "扣减成功,剩余库存:" + (stock - 1);
}
return "库存不足";
} finally {
// Lua脚本原子解锁
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
redisTemplate.execute(script, Collections.singletonList(lockKey), uuid);
}
} else {
// 抢锁失败,重试
try {
Thread.sleep(100);
return stockTest();
} catch (InterruptedException e) {
e.printStackTrace();
return "请求繁忙,请重试";
}
}
}
五、总结
- 缓存穿透:查询不存在数据,方案:缓存空值、布隆过滤器、参数校验
- 缓存击穿:单个热点key过期,并发打库,方案:永不过期、随机过期、互斥锁
- 缓存雪崩:批量key同时过期,方案:随机过期、多级缓存、定时刷新
- 分布式锁 :分布式并发控制,核心使用
set key uuid nx ex原子上锁+Lua脚本原子解锁,满足互斥、防死锁、防误删、原子四大特性