一、Redis 应用全景Redis 不仅是缓存中间件更是分布式系统中不可或缺的组件。本文聚焦三大核心应用场景分布式锁、缓存一致性、延迟队列。二、分布式锁从 SETNX 到 Redisson2.1 为什么需要分布式锁在分布式系统中多个服务实例可能同时操作共享资源如库存、订单、配置需要一种跨进程的互斥机制来保证数据一致性。典型应用场景库存扣减防止超卖分布式任务调度避免重复执行配置中心原子更新分布式会话管理2.2 分布式锁的演进V1.0SETNX EXPIRE存在死锁风险SETNX lock:order:10011# 加锁EXPIRE lock:order:100110# 设置过期# 问题非原子操作如果 SETNX 后崩溃锁永远无法释放V2.0SET … NX PX原子加锁 过期SET lock:order:1001 request_id NX PX10000# 问题业务执行时间超过锁过期时间导致锁提前释放V3.0Redisson 看门狗原子加锁 自动续期RLocklockredisson.getLock(order:1001);try{lock.lock();// 执行业务逻辑}finally{lock.unlock();}2.3 保证加锁和解锁的原子性加锁原子性SET key value NX PX ttl是单条命令Redis 单线程执行天然原子。解锁原子性使用 Lua 脚本保证判断 删除的原子性。// Lua 脚本释放锁StringunlockScriptif 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);2.4 Redisson 看门狗机制业务线程获取锁 ↓ 看门狗线程启动delay lockWatchdogTimeout / 3默认 10s/3 ≈ 3.3s ↓ 每 3.3s 检查锁是否仍被持有 ↓ 若是 → 续期至 30s ↓ 业务完成 → unlock() → 看门狗停止 ↓ 异常崩溃 → 锁自动过期释放避免死锁2.5 分布式锁的优缺点优点缺点性能高效超时时间不好设置实现方便主从复制异步导致锁不可靠避免单点故障RedLock需要额外组件Redisson2.6 合理的超时时间设置// Redisson 自动处理默认 30s 过期看门狗每 10s 续期ConfigconfignewConfig();config.useSingleServer().setAddress(redis://127.0.0.1:6379);RedissonClientredissonRedisson.create(config);RLocklockredisson.getLock(myLock);lock.lock();// 看门狗自动续期// 业务逻辑lock.unlock();三、缓存一致性策略3.1 五种策略对比策略读流程写流程一致性适用场景Cache-Aside先读缓存未命中读 DB先更新 DB再删缓存最终一致读多写少延迟双删同 Cache-Aside先删缓存→更新 DB→延迟再删最终一致高并发写Read/Write-Through缓存代理查询缓存代理更新同步写 DB强一致金融交易Write-Behind只读缓存只更新缓存异步批量写 DB最终一致秒杀库存Binlog 同步先读缓存更新 DBCanal 监听异步删缓存最终一致多级缓存3.2 Cache-Aside 旁路缓存最常用publicStringread(Stringkey){StringdataredisTemplate.opsForValue().get(key);if(datanull){datadb.query(key);redisTemplate.opsForValue().set(key,data,Duration.ofHours(1));}returndata;}publicvoidwrite(Stringkey,Stringdata){db.update(key,data);redisTemplate.delete(key);// 删除缓存非更新}为什么删除缓存而不是更新缓存更新缓存需要计算新值可能涉及复杂逻辑删除缓存更简单下次读取时自动回填最新值避免并发更新导致缓存数据不一致3.3 延迟双删策略高并发优化publicvoidwriteWithDoubleDelete(Stringkey,Stringdata){redisTemplate.delete(key);// 第一次删除db.update(key,data);// 更新数据库// 延迟第二次删除通过消息队列或延迟队列delayedQueue.add(()-redisTemplate.delete(key),500);// 延迟 500ms}原理第一次删除后在数据库更新完成前可能有读请求将旧数据回填到缓存。延迟第二次删除确保这些旧数据被清除。3.4 如何保证删除缓存操作一定能成功方案一消息队列重试publicvoiddeleteCacheWithRetry(Stringkey){try{redisTemplate.delete(key);}catch(Exceptione){mqProducer.send(newCacheDeleteMessage(key));}}方案二订阅 Binlog 补偿CanalListener(destinationmydb)publicvoidonBinlog(CanalEntry.Entryentry){if(entry.getHeader().getEventType()EventType.UPDATE){StringkeybuildCacheKey(entry);redisTemplate.delete(key);}}3.5 生产推荐组合Cache-Aside 延迟双删 Binlog 补偿写操作先删缓存 → 更新 DB → 延迟双删 ↓ Canal 监听 binlog → 异步补偿删除缓存 ↓ 最终一致性保障四、缓存三大问题雪崩、击穿、穿透4.1 缓存雪崩问题大量 key 在同一时间过期或 Redis 故障所有请求同时打到数据库。解决方案// 1. 随机过期时间intbaseTtl3600;intrandomOffsetThreadLocalRandom.current().nextInt(0,600);redisTemplate.opsForValue().set(key,data,Duration.ofSeconds(baseTtlrandomOffset));// 2. 多级缓存Cacheable(valuelocal,cacheManagercaffeineCacheManager)Cacheable(valueredis,cacheManagerredisCacheManager)// 3. 熔断降级SentinelResource(valuegetData,fallbackgetDataFallback)4.2 缓存击穿问题热点 key 恰好过期高并发请求瞬间穿透到数据库。解决方案// 互斥锁publicStringgetHotData(Stringkey){StringdataredisTemplate.opsForValue().get(key);if(data!null)returndata;StringlockKeylock:key;booleanlockedredisTemplate.opsForValue().setIfAbsent(lockKey,1,Duration.ofSeconds(10));if(locked){try{dataredisTemplate.opsForValue().get(key);if(datanull){datadb.query(key);redisTemplate.opsForValue().set(key,data,Duration.ofHours(1));}}finally{redisTemplate.delete(lockKey);}}else{Thread.sleep(100);returngetHotData(key);}returndata;}4.3 缓存穿透问题查询不存在的数据请求直接打到数据库。解决方案// 布隆过滤器BF.ADDusers user:1001BF.EXISTSusers user:1003// 返回 0 → 一定不存在五、Redis 延迟队列实现延迟队列是一种特殊的消息队列消息在发送后不会立即被消费而是延迟指定时间后才能被处理。5.1 基于 ZSet 的实现ServicepublicclassRedisDelayQueue{AutowiredprivateStringRedisTemplateredisTemplate;// 添加延迟任务publicvoidaddTask(StringtaskId,longdelaySeconds){longexecuteTimeSystem.currentTimeMillis()delaySeconds*1000;redisTemplate.opsForZSet().add(delayed_queue,taskId,executeTime);}// 轮询消费到期任务Scheduled(fixedRate1000)publicvoidconsume(){longnowSystem.currentTimeMillis();SetStringtasksredisTemplate.opsForZSet().rangeByScore(delayed_queue,0,now,0,1);for(StringtaskId:tasks){LongremovedredisTemplate.opsForZSet().remove(delayed_queue,taskId);if(removed!nullremoved0){executeTask(taskId);}}}}5.2 适用场景场景说明订单超时关闭订单创建后 30 分钟未支付自动关闭定时提醒预约成功后 1 小时发送提醒任务重试失败任务延迟 5 分钟后重试优惠券过期优惠券到期前 1 天发送提醒六、Pipeline 与事务的区别6.1 Pipeline 的核心特点客户端功能批量打包命令减少网络 RTT非原子性命令可被其他客户端插入单条失败继续某条命令失败不影响后续执行适用场景批量数据操作、高吞吐场景Pipelinepipelinejedis.pipelined();for(inti0;i10000;i){pipeline.set(key:i,valuei);}ListObjectresultspipeline.syncAndReturnAll();6.2 事务的核心特点服务端功能MULTI/EXEC 保证命令原子执行原子性EXEC 后所有命令按顺序执行不可插入语法错误全失败入队时发现语法错误EXEC 全部放弃运行时错误不回滚执行时某条失败后续继续执行适用场景原子性操作、乐观锁并发控制MULTI SET balance:10011000DECRBY balance:1001100EXEC6.3 核心差异对比维度Pipeline事务原子性✗ 非原子✓ 原子执行隔离性✗ 可被插入✓ 天然隔离错误处理单条失败继续语法错误全失败网络开销批量减少 RTT正常 RTT性能更高较低6.4 为什么优先使用 Lua 脚本-- Lua 脚本原子性 灵活性 高性能localstockredis.call(GET,KEYS[1])iftonumber(stock)tonumber(ARGV[1])thenredis.call(DECRBY,KEYS[1],ARGV[1])return1elsereturn0endLua 脚本的优势原子性整个脚本作为一个命令执行不会被其他命令插入灵活性支持复杂逻辑判断和计算高性能一次网络往返减少 RTT七、Redis 事务 vs MySQL 事务7.1 核心差异维度Redis 事务MySQL 事务原子性部分原子语法错误全失败运行时错误不回滚完全原子全部成功或全部回滚隔离性天然串行化单线程多级别隔离RU/RC/RR/S持久性依赖 RDB/AOF 配置Redo Log 保证提交不丢失回滚机制无回滚支持 Rollback锁机制乐观锁WATCH悲观锁表锁/行锁复杂度简单命令批量执行复杂支持嵌套/保存点适用场景高并发简单操作强一致业务场景7.2 为什么 Redis 事务不支持回滚错误类型Redis 事务错误通常是编程错误命令拼写、参数类型开发环境可发现设计哲学追求简单和快速回滚增加复杂度替代方案可用 DISCARD 主动放弃或用 Lua 脚本实现复杂逻辑八、综合对比表机制核心问题最佳方案关键细节分布式锁跨节点互斥访问Redisson 看门狗原子加锁 自动续期缓存一致DB 与缓存不一致Cache-Aside 延迟双删先更新 DB 再删缓存缓存雪崩大量 key 同时失效随机 TTL 多级缓存分散过期时间缓存击穿热点 key 过期互斥锁 逻辑不过期控制并发重建缓存穿透查询不存在数据布隆过滤器 空值缓存拦截非法请求延迟队列定时触发任务ZSet 轮询消费score 存储执行时间Pipeline批量命令优化客户端打包减少网络 RTT事务原子性操作MULTI/EXEC/WATCH单线程天然隔离如果本文对你有帮助欢迎点赞 收藏 ⭐ 关注 你的支持是我持续创作的动力