当前位置: 首页 > news >正文

Redis篇(五):分布式锁、缓存一致性与延迟队列

一、缓存雪崩、击穿、穿透:问题本质与解决方案

在高并发场景下,Redis 缓存失效可能导致数据库被瞬间压垮。这三个问题虽然名字相似,但成因和解决方案完全不同。

1.1 缓存雪崩(Cache Avalanche)

问题本质:大量 key 在同一时间过期或 Redis 故障宕机,所有请求同时打到数据库,导致数据库瞬间被压垮。

触发条件

  • 批量设置缓存时使用了相同的 TTL
  • Redis 集群宕机或重启
  • 缓存服务故障

解决方案

// 1. 随机过期时间:基础 TTL + 随机偏移intbaseTtl=3600;intrandomOffset=ThreadLocalRandom.current().nextInt(0,600);redisTemplate.opsForValue().set(key,data,Duration.ofSeconds(baseTtl+randomOffset));// 2. 多级缓存:本地缓存 + Redis + DB@Cacheable(value="local",cacheManager="caffeineCacheManager")@Cacheable(value="redis",cacheManager="redisCacheManager")publicStringgetData(Stringkey){returndb.query(key);}// 3. 熔断降级:数据库压力过大时返回默认值@SentinelResource(value="getData",fallback="getDataFallback")publicStringgetData(Stringkey){returnredisTemplate.opsForValue().get(key);}

1.2 缓存击穿(Cache Breakdown)

问题本质:某个热点 key 恰好过期,高并发请求瞬间穿透到数据库。

触发条件

  • 秒杀商品详情页缓存过期
  • 热点新闻缓存失效

解决方案

方案一:互斥锁(Mutex Lock)

publicStringgetHotData(Stringkey){Stringdata=redisTemplate.opsForValue().get(key);if(data!=null)returndata;// 获取互斥锁StringlockKey="lock:"+key;booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofSeconds(10));if(locked){try{// 双重检查data=redisTemplate.opsForValue().get(key);if(data==null){data=db.query(key);redisTemplate.opsForValue().set(key,data,Duration.ofHours(1));}}finally{// Lua 脚本释放锁(保证原子性)Stringscript="if redis.call('get', KEYS[1]) == ARGV[1] then "+"return redis.call('del', KEYS[1]) else return 0 end";redisTemplate.execute(newDefaultRedisScript<>(script,Long.class),Collections.singletonList(lockKey),"1");}}else{// 未获取到锁,短暂休眠后重试Thread.sleep(100);returngetHotData(key);}returndata;}

方案二:逻辑永不过期

// 缓存不设物理 TTL,通过逻辑时间判断publicStringgetDataWithLogicExpiry(Stringkey){Stringjson=redisTemplate.opsForValue().get(key);if(json==null)returnnull;CacheDatacacheData=JSON.parseObject(json,CacheData.class);// 逻辑未过期,直接返回if(cacheData.getExpireTime()>System.currentTimeMillis()){returncacheData.getData();}// 逻辑已过期,获取锁后开启独立线程重建StringlockKey="lock:"+key;booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofSeconds(10));if(locked){CACHE_REBUILD_EXECUTOR.submit(()->{try{StringnewData=db.query(key);CacheDatanewCache=newCacheData(newData,System.currentTimeMillis()+Duration.ofHours(1).toMillis());redisTemplate.opsForValue().set(key,JSON.toJSONString(newCache));}finally{redisTemplate.delete(lockKey);}});}// 返回旧数据(逻辑过期但物理未删除)returncacheData.getData();}

1.3 缓存穿透(Cache Penetration)

问题本质:查询一个不存在的数据,由于缓存中没有,请求直接打到数据库。攻击者大量构造不存在的 key 进行查询,数据库将承受巨大压力。

解决方案

方案一:缓存空值

Stringdata=redisTemplate.opsForValue().get(key);if(data==null){data=db.query(key);if(data==null){// 缓存空值,防止重复查询 DB(设置较短 TTL)redisTemplate.opsForValue().set(key,"",Duration.ofMinutes(5));}else{redisTemplate.opsForValue().set(key,data,Duration.ofHours(1));}}

方案二:布隆过滤器(推荐)

# 使用 RedisBloom 模块BF.ADDusersuser:1001 BF.ADDusersuser:1002# 查询BF.EXISTSusersuser:1003# 返回 0 → 一定不存在,直接返回# 返回 1 → 可能存在,继续查缓存/DB

二、布隆过滤器:空间换时间的概率型数据结构

布隆过滤器是一种高效的概率型数据结构,用于快速判断一个元素是否可能存在于集合中。核心特点是空间效率极高,但存在一定的误判率。

2.1 核心组成

  • 位数组(Bit Array):长度为 m 的二进制数组,初始所有位为 0
  • 多个哈希函数(Hash Functions):k 个独立的哈希函数,将输入映射到位数组的某个位置

2.2 工作流程

添加元素

元素 x → h1(x)=3, h2(x)=5, h3(x)=8 → bit[3]=1, bit[5]=1, bit[8]=1

查询元素

元素 y → h1(y)=3, h2(y)=5, h3(y)=2 → bit[3]=1 ✓, bit[5]=1 ✓, bit[2]=0 ✗ → 存在 bit=0,一定不存在

2.3 关键特性

特性说明
一定不存在某个 bit 为 0 → 该 key 绝对不在集合中
可能存在所有 bit 都为 1 → 该 key 可能在集合中(误判)
不支持删除删除一个 key 会影响其他 key 的判断
空间效率1% 误判率仅需 9.6 bits / 元素

2.4 Redis 实现

# 安装 RedisBloom 模块后BF.RESERVE myfilter0.011000000# 误判率 1%,预计 100 万元素BF.ADD myfilter user:1001 BF.ADD myfilter user:1002 BF.EXISTS myfilter user:1003# 返回 0 或 1

三、缓存一致性策略:五种方案对比

3.1 Cache-Aside 旁路缓存(最常用)

读流程:先读缓存 → 未命中读 DB → 回填缓存
写流程:先更新 DB → 再删除缓存

publicStringread(Stringkey){Stringdata=redisTemplate.opsForValue().get(key);if(data==null){data=db.query(key);redisTemplate.opsForValue().set(key,data,Duration.ofHours(1));}returndata;}publicvoidwrite(Stringkey,Stringdata){db.update(key,data);redisTemplate.delete(key);// 删除缓存,非更新}

特点:适合读多写少场景,存在短暂不一致窗口。

3.2 Double Delete 双删策略(高并发优化)

写流程:先删缓存 → 更新 DB → 延迟再删缓存

publicvoidwriteWithDoubleDelete(Stringkey,Stringdata){redisTemplate.delete(key);// 第一次删除db.update(key,data);// 更新数据库// 延迟第二次删除(通过消息队列或延迟队列)delayedQueue.add(()->redisTemplate.delete(key),500);// 延迟 500ms}

特点:通过二次删除解决并发期间写不一致,延迟时间需大于业务 RT。

3.3 Read/Write-Through 穿透读写

写流程:缓存代理更新,同步更新 DB
读流程:缓存代理查询,未命中自动加载

特点:数据强一致,但写入性能较低,适合金融交易等一致性要求强的系统。

3.4 Write-Behind 异步写(高性能)

写流程:只更新缓存,异步批量写 DB

特点:高性能但存在数据丢失风险,适合秒杀库存、点赞等可容忍丢失的场景。

3.5 Binlog 同步(最终一致)

MySQL binlog → Canal/Maxwell → 解析日志 → 更新缓存

特点:保证最终一致,延迟约 100ms~1s,业务代码无入侵,适合多级缓存同步。

3.6 生产推荐组合

Cache-Aside + 延迟双删 + Binlog 补偿

写操作:先删缓存 → 更新 DB → 延迟双删 ↓ Canal 监听 binlog → 异步补偿删除缓存 ↓ 最终一致性保障

四、如何保证删除缓存操作一定能成功?

4.1 消息队列重试机制

// 删除缓存失败,放入消息队列重试publicvoiddeleteCacheWithRetry(Stringkey){try{redisTemplate.delete(key);}catch(Exceptione){// 放入消息队列,消费者重试删除mqProducer.send(newCacheDeleteMessage(key));}}

4.2 订阅 Binlog 补偿

// Canal 监听 binlog,异步删除缓存@CanalListener(destination="mydb")publicvoidonBinlog(CanalEntry.Entryentry){if(entry.getHeader().getEventType()==EventType.UPDATE){Stringkey=buildCacheKey(entry);redisTemplate.delete(key);}}

五、Redis 实现分布式锁

5.1 什么是分布式锁?

分布式锁是用于协调分布式系统中多个节点对共享资源进行互斥访问的同步机制。在单机环境中可通过synchronizedReentrantLock控制并发,但在分布式环境下需跨节点协同。

典型应用场景

  • 库存扣减(防止超卖)
  • 分布式任务调度(避免重复执行)
  • 配置中心原子更新
  • 分布式会话管理

5.2 分布式锁的演进

V1.0:SETNX + EXPIRE(存在死锁风险)

SETNX lock:order:10011# 加锁EXPIRE lock:order:100110# 设置过期# 问题:非原子操作,如果 SETNX 后崩溃,锁永远无法释放

V2.0:SET … NX PX(原子加锁 + 过期)

SET lock:order:1001 request_id NX PX10000# 问题:业务执行时间超过锁过期时间,导致锁提前释放

V3.0:Redisson 看门狗(原子加锁 + 自动续期)

RLocklock=redisson.getLock("order:1001");try{lock.lock();// 执行业务逻辑}finally{lock.unlock();}

5.3 Redisson 看门狗机制

业务线程获取锁 ↓ 看门狗线程启动(delay = lockWatchdogTimeout / 3,默认 10s/3 ≈ 3.3s) ↓ 每 3.3s 检查锁是否仍被持有 ↓ 若是 → 续期至 30s ↓ 业务完成 → unlock() → 看门狗停止 ↓ 异常崩溃 → 锁自动过期释放(避免死锁)

5.4 保证加锁和解锁的原子性

// 加锁:SET key value NX PX 10000(原子操作)// 解锁:Lua 脚本保证原子性StringunlockScript="if redis.call('get', KEYS[1]) == ARGV[1] then "+"return redis.call('del', KEYS[1]) "+"else return 0 end";redisTemplate.execute(newDefaultRedisScript<>(unlockScript,Long.class),Collections.singletonList("lock:order:1001"),requestId);

5.5 分布式锁的优缺点

优点缺点
性能高效超时时间不好设置
实现方便主从复制异步导致锁不可靠
避免单点故障(RedLock)需要额外组件(Redisson)

5.6 合理的超时时间设置

基于续约的方式

// Redisson 自动处理:默认 30s 过期,看门狗每 10s 续期Configconfig=newConfig();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClientredisson=Redisson.create(config);RLocklock=redisson.getLock("myLock");lock.lock();// 看门狗自动续期// 业务逻辑lock.unlock();

六、Redis 延迟队列实现

延迟队列是一种特殊的消息队列,消息在发送后不会立即被消费,而是延迟指定时间后才能被处理。

6.1 基于 ZSet 的实现

@ServicepublicclassRedisDelayQueue{@AutowiredprivateStringRedisTemplateredisTemplate;// 添加延迟任务publicvoidaddTask(StringtaskId,longdelaySeconds){longexecuteTime=System.currentTimeMillis()+delaySeconds*1000;redisTemplate.opsForZSet().add("delayed_queue",taskId,executeTime);}// 轮询消费到期任务@Scheduled(fixedRate=1000)publicvoidconsume(){longnow=System.currentTimeMillis();// 获取 score ≤ currentTime 的任务Set<String>tasks=redisTemplate.opsForZSet().rangeByScore("delayed_queue",0,now,0,1);for(StringtaskId:tasks){// 原子移除,防止多消费者重复消费Longremoved=redisTemplate.opsForZSet().remove("delayed_queue",taskId);if(removed!=null&&removed>0){// 执行业务逻辑executeTask(taskId);}}}}

6.2 适用场景

场景说明
订单超时关闭订单创建后 30 分钟未支付自动关闭
定时提醒预约成功后 1 小时发送提醒
任务重试失败任务延迟 5 分钟后重试
优惠券过期优惠券到期前 1 天发送提醒

6.3 Redis 延迟队列 vs 专业消息队列

特性Redis ZSetRabbitMQ DLXRocketMQ
实现复杂度简单中等复杂
ACK 机制
消费组不支持支持支持
数据可靠性可能丢失高可靠高可靠
适用场景简单延迟任务复杂消息流大规模分布式

七、热点数据动态缓存策略

通过数据最新访问时间做排名,过滤掉不常访问的数据,只保留经常访问的数据。

// 记录商品访问时间publicvoidrecordAccess(StringskuId){redisTemplate.opsForZSet().add("hot:products",skuId,System.currentTimeMillis());}// 获取 Top N 热点商品publicSet<String>getHotProducts(intn){returnredisTemplate.opsForZSet().reverseRange("hot:products",0,n-1);}// 定期清理冷数据@Scheduled(cron="0 0 * * * *")publicvoidcleanColdData(){// 移除 7 天前访问的商品longweekAgo=System.currentTimeMillis()-7*24*3600*1000;redisTemplate.opsForZSet().removeRangeByScore("hot:products",0,weekAgo);}

如果本文对你有帮助,欢迎点赞 👍 + 收藏 ⭐ + 关注 🔖,你的支持是我持续创作的动力!

http://www.rkmt.cn/news/1522472.html

相关文章:

  • 2026年石嘴山市本地人常去黄金回收门店前五整理:黄金回收铂金回收白银回收彩金回收靠谱门店TOP5实力排行榜推荐及联系方式汇总 - 亦辰小黄鸭
  • 张量积样条:解决GAM中变量交互建模的刚需工具
  • PotPlayer字幕翻译插件终极指南:3步实现外语视频无障碍观看
  • 2026年阳江市黄金回收白银回收铂金回收彩金回收测评+本地人气靠前五家靠谱门店介绍推荐及联系方式 - 前途无量YY
  • 2026年唐山市黄金回收白银回收铂金回收彩金回收测评+本地人气靠前五家靠谱门店介绍推荐及联系方式 - 前途无量YY
  • 终极QQ音乐解密指南:5分钟解锁你的加密音频库
  • 从FB到DRM:一个嵌入式Linux工程师的显示框架踩坑与选型心路历程
  • 117.DDPM核心原理精讲|前向加噪、反向去噪与ELBO损失函数完整推导
  • 解锁游戏无限可能:BepInEx插件框架全面指南
  • 2026年宁德市本地人常去黄金回收门店前五整理:黄金回收铂金回收白银回收彩金回收靠谱门店TOP5实力排行榜推荐及联系方式汇总 - 亦辰小黄鸭
  • 2026法考资料pdf|电子版|资料已整理
  • 2026年六盘水市黄金回收白银回收铂金回收彩金回收测评+本地人气靠前五家靠谱门店介绍推荐及联系方式 - 前途无量YY
  • 2026年攀枝花市本地人常去黄金回收门店前五整理:黄金回收铂金回收白银回收彩金回收靠谱门店TOP5实力排行榜推荐及联系方式汇总 - 亦辰小黄鸭
  • 从GRBL到Ruida:一文讲透LightBurn支持的三大激光控制器(附实物图识别)
  • 告别理论!用C++和OpenGL亲手实现一个简易3D建模视图:从glOrtho投影到模型交互
  • 2026年惠州市本地人常去黄金回收门店前五整理:黄金回收铂金回收白银回收彩金回收靠谱门店TOP5实力排行榜推荐及联系方式汇总 - 亦辰小黄鸭
  • RK3588项目选型指南:LT6911UXC、IT6616、RK628D,三款HDMI转MIPI芯片怎么选?
  • 别再傻傻分不清!服务器网卡选HHHL还是FHHL?一张图看懂PCIe卡尺寸怎么选
  • 2026年十堰市黄金回收白银回收铂金回收彩金回收测评+本地人气靠前五家靠谱门店介绍推荐及联系方式 - 前途无量YY
  • Linux Perf Swevent软件事件计数与Hrtimer触发
  • 继承关系的实验
  • 动态李代数在量子计算中的核心作用与应用解析
  • 2026年随州市本地人常去黄金回收门店前五整理:黄金回收铂金回收白银回收彩金回收靠谱门店TOP5实力排行榜推荐及联系方式汇总 - 亦辰小黄鸭
  • STM32的PB3引脚还能这么用?深入聊聊JTAG/SWD复用与异步跟踪功能那点事
  • 2026年石家庄市黄金回收白银回收铂金回收彩金回收测评+本地人气靠前五家靠谱门店介绍推荐及联系方式 - 前途无量YY
  • BLDC方波驱动 vs PMSM正弦波驱动:你的项目到底该选哪个?(从原理到选型指南)
  • 从glTF到3D Tiles:手把手教你为Cesium项目选择合适的3D模型格式
  • 别再纠结了!2024年新项目选pnpm、yarn还是npm?我帮你从实战角度盘一盘
  • Downkyi哔哩下载姬:3步解锁B站8K超高清视频的专业下载方案
  • 从MC1496到三极管:手把手教你用频谱分析仪对比两种混频器的真实性能