Redis三大缓存问题与分布式锁
文章目录一、缓存穿透查不存在的数据绕过缓存直连DB1. 什么是穿透2. 四种解决方案二、缓存击穿热点key过期并发打爆数据库1. 概念区分2. 主流解决办法三、缓存雪崩大批量key同时过期全量请求落库1. 雪崩vs击穿区别2. 完整解决方案四、分布式锁集群环境并发数据安全必备1. 为什么需要分布式锁2. Redis分布式锁踩坑全过程坑1只用SETNX上锁无过期坑2SETNX EXPIRE两条命令非原子坑3锁被其他线程误删坑4判断UUIDDEL删除非原子操作3. 合格Redis分布式锁四大硬性要求4. SpringBoot简易代码实现五、总结一、缓存穿透查不存在的数据绕过缓存直连DB1. 什么是穿透用户查询数据库、缓存都不存在的数据比如传-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上锁无过期setnx lock_user 1 del lock_user业务代码异常崩溃锁永远不释放直接死锁。坑2SETNX EXPIRE两条命令非原子setnx lock_user 1 expire lock_user 10上锁成功后Redis宕机过期命令没执行依旧死锁。解决一条命令原子上锁set lock_user uuid nx ex 10NX不存在才创建EX设置过期单命令原子执行。坑3锁被其他线程误删线程A业务卡顿锁自动过期线程B抢到锁执行业务A执行完手动删除B的锁。解决锁value存入UUID删锁前校验持有者身份只删自己的锁。坑4判断UUIDDEL删除非原子操作先GET对比UUID再DEL中间锁过期被抢走依然误删。终极方案Lua脚本保证解锁原子性-- KEYS[1]锁keyARGV[1]当前线程uuidifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(del,KEYS[1])elsereturn0end整个脚本一次性执行不存在中间间隙完美避免误删。3. 合格Redis分布式锁四大硬性要求互斥性同一时间只能一个客户端持有锁防死锁锁自带过期时间宕机自动释放身份校验谁加锁谁解锁不能删除别人的锁原子操作加锁、解锁全过程原子执行4. SpringBoot简易代码实现GetMapping(/stock/test)publicStringstockTest(){// 生成唯一标识StringuuidUUID.randomUUID().toString();StringlockKeystock_lock;// 原子上锁过期3秒BooleanlockredisTemplate.opsForValue().setIfAbsent(lockKey,uuid,3,TimeUnit.SECONDS);if(lock){try{// 扣库存业务StringstockStrredisTemplate.opsForValue().get(goods_stock);intstockInteger.parseInt(stockStr);if(stock0){redisTemplate.opsForValue().set(goods_stock,stock-1);return扣减成功剩余库存(stock-1);}return库存不足;}finally{// Lua脚本原子解锁StringluaScriptif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;DefaultRedisScriptLongscriptnewDefaultRedisScript();script.setScriptText(luaScript);script.setResultType(Long.class);redisTemplate.execute(script,Collections.singletonList(lockKey),uuid);}}else{// 抢锁失败重试try{Thread.sleep(100);returnstockTest();}catch(InterruptedExceptione){e.printStackTrace();return请求繁忙请重试;}}}五、总结缓存穿透查询不存在数据方案缓存空值、布隆过滤器、参数校验缓存击穿单个热点key过期并发打库方案永不过期、随机过期、互斥锁缓存雪崩批量key同时过期方案随机过期、多级缓存、定时刷新分布式锁分布式并发控制核心使用set key uuid nx ex原子上锁Lua脚本原子解锁满足互斥、防死锁、防误删、原子四大特性