Redisson分布式锁原理、实战与避坑指南:从秒杀到缓存击穿的完整解决方案

📅 2026/8/5 1:27:14
Redisson分布式锁原理、实战与避坑指南:从秒杀到缓存击穿的完整解决方案
1. 项目概述为什么分布式锁是微服务时代的“刚需”如果你正在开发一个微服务架构的系统或者一个用户量稍大的单体应用那么“库存超卖”、“优惠券重复发放”、“用户余额扣减出错”这些问题大概率已经或者即将成为你夜不能寐的根源。表面上看这些是业务逻辑Bug但往深了挖核心往往出在并发控制上——多个线程、多个进程甚至多个服务实例在同一时刻争抢着修改同一份数据。在单机时代我们靠synchronized或ReentrantLock这类JVM级别的锁就能高枕无忧。但一旦服务做了集群部署这些锁就瞬间失效了因为它们只能锁住自己JVM内的线程管不了其他机器上的进程。这时候分布式锁就登场了。它本质上是一个所有服务实例都能看见和认可的“全局标志位”。当一个实例想要执行一段临界区代码比如扣减库存时它必须先到这个公共区域通常是Redis、ZooKeeper等中间件去申请一把锁。只有申请成功的实例才能执行操作执行完毕后释放锁其他实例才能继续争抢。这样就把并发控制从单机扩展到了整个分布式系统。而在众多实现分布式锁的方案中Redisson提供的分布式锁可以说是Java开发者手中的“瑞士军刀”。它不仅仅是一个简单的setnx命令封装而是一个提供了可重入、公平锁、联锁、红锁等多种高级特性并且自动处理了锁续期、看门狗等复杂机制的成熟客户端。直接使用Redis命令手写分布式锁你可能会陷入锁超时设置、锁误释放、死锁等一大堆坑里。而Redisson帮你填平了这些坑让你能更专注于业务逻辑本身。所以“从头开始学Redisson分布式锁”不是一个简单的工具学习而是掌握在分布式环境下构建健壮、可靠系统的核心技能之一。2. 核心需求解析从业务场景看分布式锁的“用武之地”在动手写代码之前我们必须搞清楚到底哪些场景非用分布式锁不可理解了场景才能理解Redisson每个设计背后的用意。这里我结合几个最常见的“事故高发区”来聊聊。2.1 秒杀与库存扣减这是最经典的场景。假设商品库存只剩1件同时有10万个请求涌进来。如果没有分布式锁每个服务实例在查询数据库时都看到库存为1然后都执行了库存-1的操作最终导致库存变成-9这就是恐怖的“超卖”。分布式锁在这里的作用就是确保“查询库存”和“更新库存”这两个操作组成的临界区同一时间只有一个线程能执行。整个流程变成抢锁 - 查询库存 - 判断是否大于0 - 扣减库存 - 释放锁。这样无论多少请求都只能排队串行处理从根本上杜绝超卖。注意这里锁的粒度是关键。如果你把整个秒杀流程都锁住性能会急剧下降。更优的做法是以商品ID为维度进行加锁例如lock redisson.getLock(product_stock_lock: productId)。这样不同商品之间的秒杀就不会相互阻塞只有同一商品的请求才会串行化这在电商大促中至关重要。2.2 分布式环境下的重复任务调度假设你有一个定时任务每天凌晨统计前一天的报表。这个任务在单机部署时用Scheduled注解很简单。但当你部署了多个服务实例时可怕的事情发生了每个实例的定时任务都会在同一时间触发同一份报表被重复计算了多次。这时你需要用分布式锁让这个任务在集群中“幂等”地执行。任务开始时所有实例都尝试获取一个唯一的锁比如lock redisson.getLock(daily_report_job_lock)只有一个实例能成功执行任务其他实例获取锁失败直接跳过。这样就实现了集群环境下任务的精确调度。2.3 防止缓存击穿下的重复数据库查询缓存击穿是指一个热点Key突然过期此时有大量并发请求这个Key这些请求发现缓存不存在都会同时去查询数据库给数据库带来瞬间的巨大压力。一个常见的解决方案是使用分布式锁进行“互斥重建”。当第一个请求发现缓存失效时它成功获取锁然后去数据库查询数据并回填缓存。在这期间其他并发请求也尝试获取锁但都会失败于是它们可以选择短暂休眠后重试读取缓存或者直接返回默认值。这样就避免了大量请求穿透到数据库。public String getData(String key) { String data redisTemplate.opsForValue().get(key); if (data null) { // 缓存未命中尝试获取锁 RLock lock redisson.getLock(lock: key); try { if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 等待3秒锁持有10秒 // 再次检查缓存防止在等待锁期间已有其他线程更新了缓存 data redisTemplate.opsForValue().get(key); if (data null) { // 查询数据库 data queryFromDB(key); // 写入缓存 redisTemplate.opsForValue().set(key, data, 1, TimeUnit.HOURS); } } else { // 获取锁失败说明已有其他线程在重建缓存 // 方案1休眠后重试 Thread.sleep(100); return getData(key); // 方案2返回兜底数据 // return getDefaultData(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(获取锁中断, e); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } return data; }2.4 用户余额或积分等敏感数据的并发更新用户钱包扣款、积分增减这些操作必须保证绝对准确。并发下可能出现的经典问题是余额100元两个请求同时扣款60元。两个线程都读到余额为100都认为足够扣款然后分别执行100-60结果余额变成了40元而实际应该只扣一次60元余额应为40元这里用户被多扣了60元。通过分布式锁例如以用户ID为维度lock redisson.getLock(user_balance_lock: userId)可以强制这些更新操作串行执行避免数据错乱。3. Redisson分布式锁的核心原理深度拆解Redisson的锁之所以强大且省心是因为它在简单的Redis命令之上构建了一套完整的锁管理机制。理解这套机制是你正确使用和排查问题的前提。3.1 锁的存储结构不仅仅是String很多人以为分布式锁就是在Redis里设置一个Key。Redisson做得更精细。当你调用redisson.getLock(myLock)时它在Redis中主要使用的数据结构是Hash。锁Key例如myLock对应的不是一个简单的字符串值而是一个Hash表。这个Hash表里至少会包含以下字段UUID:threadId 这个字段的Name是客户端UUID:当前线程ID它的Value是一个数字代表重入次数。这是实现可重入锁的关键。例如8743c9c0-0795-4710-be57-8a3b4d17b8c7:1-1表示这个客户端UUID的这个线程ID为1持有该锁1次。还可能包含一些系统字段用于支持看门狗、锁模式等。使用Hash结构的好处是可重入性通过累加UUID:threadId对应的Value值轻松实现同一个线程多次加锁重入。锁归属清晰Value中包含了客户端UUID和线程ID在释放锁时可以精确判断当前释放锁的线程是否是锁的持有者防止误删其他客户端的锁。高效检查锁状态、重入计数、释放锁等操作都可以通过Redis的HINCRBY、HEXISTS等原子命令完成。3.2 看门狗Watchdog机制告别锁超时烦恼这是Redisson最核心的“黑科技”之一也是它比手动实现强大得多的地方。手动实现分布式锁时你必须在加锁时设置一个过期时间TTL以防止客户端崩溃导致锁永远无法释放死锁。但这引出了另一个问题业务逻辑的执行时间可能超过TTL。假设你设置锁超时时间为30秒但某个业务操作因为GC、网络延迟或复杂计算执行了35秒。在第30秒时Redis中的锁自动过期释放了。此时另一个客户端成功获取了该锁。一秒钟后最初的客户端终于执行完了并开始执行释放锁的逻辑——这会导致它错误地释放了第二个客户端刚刚创建的锁数据一致性被彻底破坏。Redisson的看门狗机制就是为了解决这个问题。当你使用lock.lock()不加超时参数或者使用lock.lock(10, TimeUnit.SECONDS)但未指定leaseTime参数时看门狗就会自动启用。它的工作原理如下客户端A成功加锁锁的默认过期时间设置为30秒可配置lockWatchdogTimeout。同时在客户端A内部会启动一个定时任务看门狗这个任务每隔过期时间的1/3即10秒执行一次。每次看门狗任务执行时它会检查客户端A是否还持有这把锁通过检查Redis中对应Hash的字段是否存在。如果还持有它就通过pexpire命令将锁的过期时间重新刷新为30秒。只要客户端A的业务线程没有执行完这个续期操作就会一直进行直到业务线程主动调用unlock()。当业务线程调用unlock()释放锁后看门狗任务也会被取消。这样一来只要持有锁的客户端还“活着”锁就不会因为超时而自动释放完美避免了上述的锁失效问题。只有当客户端进程崩溃看门狗线程也随之停止锁才会在最后一次续期后的30秒自动过期。实操心得看门狗机制虽好但不能滥用。如果你能明确知道业务逻辑的最大执行时间强烈建议使用带有明确leaseTime参数的加锁方法例如lock.lock(10, TimeUnit.SECONDS)。这会告诉Redisson不使用看门狗锁在10秒后自动释放。这能避免在客户端异常崩溃时锁因为看门狗的续期而长时间不释放尽管有超时但比预期长。对于定时任务、已知耗时的操作指定leaseTime是更优选择。3.3 可重入性Reentrancy实现可重入锁意味着同一个线程可以多次获取同一把锁而不会把自己阻塞死。在单机JVM中ReentrantLock通过一个计数器实现。在Redisson的分布式锁中这个计数器就存储在之前提到的Redis Hash结构里。加锁流程简化客户端A线程T1尝试获取锁“myLock”。Redisson执行Lua脚本原子性地检查Redis中“myLock”这个Hash是否存在。如果不存在则创建Hash设置字段UUID_A:threadId_T1的值为1首次获取并设置过期时间。加锁成功。如果存在且字段UUID_A:threadId_T1也存在则通过HINCRBY命令将其值加1重入次数1并重置过期时间。加锁成功。如果存在但字段不是UUID_A:threadId_T1说明锁被其他客户端或线程持有则加锁失败。解锁流程简化客户端A线程T1调用unlock()。Redisson执行Lua脚本原子性地获取字段UUID_A:threadId_T1的值重入次数。如果值减1后大于0说明还有重入层数则更新该值并重置过期时间。如果值减1后等于0说明这是最外层锁则直接删除这个Hash键彻底释放锁。整个流程通过Lua脚本保证原子性确保了在高并发下重入计数的绝对准确。3.4 公平锁与非公平锁Redisson也实现了公平锁RFairLock。公平锁的核心是排队。它内部使用Redis的List数据结构作为一个等待队列。非公平锁默认的RLock所有尝试加锁的客户端一起争抢谁抢到算谁的。可能产生“饥饿”现象即某些客户端永远抢不到锁。公平锁RFairLock客户端加锁时如果锁已被占用它不会立即返回失败而是将自己放入一个等待队列Redis List的末尾。当锁被释放时等待时间最长的客户端队首将获得锁。这保证了获取锁的顺序与申请锁的顺序一致。公平锁避免了饥饿但性能通常低于非公平锁因为它需要维护队列。在绝大多数业务场景中非公平锁的吞吐量更高是默认推荐的选择。只有在严格要求顺序性的特殊场景下才考虑使用公平锁。4. 从入门到精通Redisson分布式锁的完整实操指南理论讲透了我们进入实战环节。我会从环境搭建、基础用法一直讲到高级特性和生产级配置。4.1 环境准备与基础集成首先在你的Spring Boot项目中引入Redisson依赖。我推荐使用Spring Boot Starter它能很好地处理配置和Bean的注入。Maven依赖dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.0/version !-- 请使用最新稳定版本 -- /dependency基础配置application.ymlspring: redis: # 单节点模式 host: 127.0.0.1 port: 6379 password: yourpassword # 如果没有密码可省略 database: 0 # Redisson特定配置可选通常默认即可 redisson: config: | singleServerConfig: idleConnectionTimeout: 10000 connectTimeout: 10000 timeout: 3000 retryAttempts: 3 retryInterval: 1500 # 看门狗默认超时时间单位毫秒 lockWatchdogTimeout: 30000配置完成后Redisson客户端RedissonClient会自动注入到Spring容器中你可以直接Autowired使用。4.2 基础加锁与解锁的四种姿势姿势一最常用——阻塞式加锁带看门狗Autowired private RedissonClient redissonClient; public void doSomething() { RLock lock redissonClient.getLock(myBusinessLock); try { // 1. 阻塞等待直到获取锁。默认启用看门狗锁超时时间为30秒可配置 lock.lock(); // 2. 执行业务逻辑 businessHandle(); } finally { // 3. 必须在finally块中释放锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }这是最傻瓜式的用法但也是隐患最多的一种。如果businessHandle()方法执行时间过长或发生死循环看门狗会不断续期锁可能永远不会释放导致其他线程永远等待。姿势二阻塞式加锁指定自动释放时间public void doSomethingWithLeaseTime() { RLock lock redissonClient.getLock(myBusinessLock); try { // 等待获取锁最多等待10秒。获取后锁在5秒后自动释放不启用看门狗。 boolean isLocked lock.tryLock(10, 5, TimeUnit.SECONDS); if (isLocked) { businessHandle(); // 必须确保业务在5秒内完成 } else { log.warn(获取锁失败处理降级逻辑); handleFallback(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(锁等待被中断, e); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }这种方式明确指定了leaseTime5秒Redisson不会启用看门狗。适用于你能准确预估业务耗时的场景。务必确保业务执行时间小于leaseTime。姿势三非阻塞式加锁立即返回public void doSomethingTryLock() { RLock lock redissonClient.getLock(myBusinessLock); if (lock.tryLock()) { // 立即尝试获取不到直接返回false try { businessHandle(); } finally { lock.unlock(); } } else { // 获取锁失败快速失败或重试 log.info(系统繁忙请稍后再试); } }适用于不希望线程等待的场景比如高并发下的快速失败配合前端提示“系统繁忙”用户体验更好。姿势四异步加锁public void doSomethingAsync() { RLock lock redissonClient.getLock(myBusinessLock); RFutureBoolean future lock.tryLockAsync(10, 5, TimeUnit.SECONDS); future.onComplete((isLocked, throwable) - { if (throwable ! null) { log.error(异步加锁异常, throwable); return; } if (isLocked) { try { businessHandle(); } finally { lock.unlockAsync(); } } else { handleFallback(); } }); }异步API适用于反应式编程或不想阻塞当前线程的场景。注意异步操作的回调执行线程可能不是原线程需要处理好线程上下文如ThreadLocal。4.3 高级特性实战联锁、红锁与读写锁联锁MultiLock当你需要同时锁定多个资源并且要求要么全部锁定要么一个都不锁定时就需要联锁。例如转账操作需要同时锁定A账户和B账户。public void transfer(String fromAccount, String toAccount) { RLock lock1 redissonClient.getLock(account: fromAccount); RLock lock2 redissonClient.getLock(account: toAccount); RLock multiLock redissonClient.getMultiLock(lock1, lock2); multiLock.lock(); try { // 执行转账业务扣减fromAccount增加toAccount doTransfer(fromAccount, toAccount); } finally { multiLock.unlock(); } }Redisson的联锁会按顺序依次尝试获取所有传入的锁全部成功才算加锁成功。释放时也会按顺序释放。这能有效预防死锁因为锁的获取顺序是固定的。红锁RedLock这是一个基于Redis集群的分布式锁算法旨在解决在Redis单点故障或主从异步复制下可能出现的锁失效问题。其核心思想是向多个奇数个通常5个相互独立的Redis主节点申请锁当从超过半数的节点N/2 1成功获取锁时才算真正持有锁。重要提示Redis官方后来发表了一篇文章指出了RedLock算法在特定极端场景下如系统时钟跳跃依然存在争议并且实现复杂。对于绝大多数业务场景使用带有主从复制和故障转移的Redis Sentinel或Redis Cluster并结合合理的锁超时时间其可靠性已经足够。Redisson虽然提供了RedissonRedLock实现但除非你对一致性有极端要求且能接受其复杂性和性能损耗否则不建议轻易使用。读写锁ReadWriteLockRReadWriteLock实现了java.util.concurrent.locks.ReadWriteLock接口允许多个读锁同时持有但写锁是排他的。RReadWriteLock rwLock redissonClient.getReadWriteLock(myReadWriteLock); RLock readLock rwLock.readLock(); RLock writeLock rwLock.writeLock(); // 读操作 readLock.lock(); try { // 多个线程可以同时进入这里执行读操作 readData(); } finally { readLock.unlock(); } // 写操作 writeLock.lock(); try { // 只有一个线程可以进入这里执行写操作 writeData(); } finally { writeLock.unlock(); }这在“读多写少”的场景下能极大提升并发性能例如缓存数据的更新。4.4 生产级配置与最佳实践锁命名要有业务意义锁的Key是全局资源命名应包含业务前缀和资源标识避免冲突。如order:pay:lock:{orderId}product:stock:lock:{skuId}。务必在finally块中释放锁这是铁律确保任何情况下包括异常锁都能被释放。释放前检查锁持有者使用lock.isHeldByCurrentThread()判断防止误释放其他线程的锁在复杂线程池或异步回调中可能发生。合理设置等待时间和持有时间waitTime获取锁的最大等待时间根据系统容忍的排队时间设置。设置太短可能导致大量失败太长则线程堆积。leaseTime锁自动释放时间如果你能预估业务最大耗时就设置一个略大于该值的leaseTime并禁用看门狗。如果无法预估则使用看门狗不传leaseTime但要监控业务执行时间防止异常长耗时任务。避免在锁内执行耗时操作或远程调用锁的持有时间应尽可能短只包裹真正需要互斥的核心资源操作。将数据准备、日志记录等非竞争操作移到锁外。做好降级和熔断获取锁失败是常态不是异常。必须有相应的降级策略比如返回“系统繁忙请重试”的提示或者使用本地缓存返回旧数据保证系统整体可用性。监控与告警监控Redis中锁Key的数量、锁等待时间、获取锁失败率等指标。设置告警当锁平均持有时间异常增长或失败率飙升时及时介入排查。5. 避坑指南Redisson分布式锁的典型问题与排查实录即使理解了原理实践中依然会踩坑。下面是我和团队遇到过的一些典型问题及解决方案。5.1 锁超时与看门狗的那些事儿问题现象业务逻辑执行时间不确定有时会超过设置的leaseTime导致锁提前释放数据出错。根因分析使用了tryLock(waitTime, leaseTime, unit)并设置了较短的leaseTime且业务执行时间超过了它。解决方案方案A推荐如果业务耗时波动大不要设置leaseTime直接使用lock()或tryLock(waitTime, unit)依赖看门狗自动续期。同时你需要优化业务代码确保其平均耗时在可控范围内并设置监控告警。方案B如果必须设置leaseTime则需要将其设置为业务可能的最大耗时 安全余量。例如业务99.9%的情况在2秒内完成但极端情况可能到10秒那么leaseTime可以设为15-20秒。这需要你对业务有深入的了解。5.2 “我明明释放了锁为什么日志显示还在持有”问题现象在finally块中调用了unlock()但监控发现锁的TTL依然在刷新似乎没释放。排查步骤检查是否重入在锁内部又调用了另一个需要同一把锁的方法导致重入次数大于1。一次unlock()只减少一次计数锁并未完全释放。确保加锁和解锁次数匹配。检查线程一致性在异步回调或线程池场景下执行unlock()的线程可能不是当初lock()的线程。Redisson的锁是与线程ID绑定的。确保锁的获取和释放在同一线程上下文。可以使用lock.unlockAsync()配合回调或者在异步任务开始时传递锁引用并在同一异步线程中释放。检查异常unlock()方法本身也可能抛出异常如网络中断导致释放失败。可以在finally块中加入更健壮的释放逻辑finally { if (lock.isHeldByCurrentThread()) { try { lock.unlock(); } catch (Exception e) { log.error(释放锁异常锁Key: {}, lock.getName(), e); // 可以考虑发送告警这是一个危险信号 } } }5.3 Redis主从切换与锁失效问题现象在Redis Sentinel或Cluster模式下主节点宕机从节点升级为主节点但客户端A持有的锁信息可能因异步复制而未同步到新的主节点导致客户端B也能获取到同一把锁。根因分析这是Redis异步复制机制带来的固有风险。Redisson默认的锁在单节点或主从架构下无法完全规避此问题。解决方案与选择接受风险对于绝大多数业务主从切换是低概率事件且锁通常有TTL短暂的数据不一致在可接受范围内。这是成本与收益的权衡。使用RedLock如前所述通过多个独立Redis实例来降低风险但带来复杂性和性能下降。升级架构对于金融级强一致性场景考虑使用ZooKeeper、etcd等CP模型的协调服务来实现分布式锁它们通过ZAB或Raft协议保证强一致性但性能通常低于Redis。5.4 锁等待导致的线程池耗尽与链式雪崩问题现象某个热点资源如某个明星商品的库存锁竞争激烈大量线程阻塞在lock.tryLock(10, SECONDS)上。如果这些线程来自Web容器的公共线程池如Tomcat的HTTP线程池会导致线程池迅速被占满整个服务无法响应其他请求引发雪崩。排查与解决监控锁等待时间在tryLock前后记录时间统计平均等待时间和最大等待时间。如果等待时间持续增长说明竞争激烈。缩小锁粒度这是最有效的办法。检查锁的Key是否过于宽泛。能否从“商品锁”细化到“商品SKU锁”甚至结合库存分段。引入排队与限流在锁的外层使用更高效的限流器如Redisson的RRateLimiter或消息队列进行流量削峰控制进入锁竞争环节的请求数量。使用独立线程池对于特别耗时的锁竞争操作将其提交到专用的、有界线程池中执行避免影响主业务线程池。设置合理的等待超时不要设置过长的waitTime。对于非核心操作快速失败并返回用户“请稍后重试”好过让用户长时间等待。5.5 性能调优与监控指标为了让分布式锁稳定高效你需要关注以下指标监控指标说明异常排查方向锁获取成功率成功获取锁次数 / 尝试获取锁总次数成功率持续下降说明竞争激烈或Redis服务异常。平均锁持有时间从获取到释放锁的平均耗时时间过长说明业务逻辑在锁内执行太慢需要优化或拆分。平均锁等待时间尝试获取锁时平均需要等待多久等待时间过长说明资源成为瓶颈需要细化锁粒度或限流。Redis命令延迟执行hset,pexpire等锁相关命令的耗时延迟增高可能Redis负载过高或网络有问题。看门狗续期次数看门狗线程执行续期的频率异常增高可能业务执行时间不稳定频繁触及续期。可以在加锁/解锁的关键节点通过AOP或手动埋点将日志发送到监控系统如Prometheus Grafana进行可视化。Redisson自身也提供了丰富的监控指标可以通过JMX或Micrometer集成暴露出来。6. 超越基础分布式锁在复杂场景下的设计思考掌握了基本用法和避坑技巧后我们来看一些更复杂的场景思考如何用分布式锁及其变种来优雅地解决问题。6.1 分布式锁与数据库事务的协同这是一个常见陷阱在方法上加了Transactional同时在方法内部使用了分布式锁。Transactional public void business() { RLock lock redisson.getLock(lock); lock.lock(); try { // 1. 查询数据 // 2. 修改数据 // 3. 保存到数据库 } finally { lock.unlock(); } }问题如果事务提交失败会发生什么锁已经释放了但数据修改回滚了。其他线程拿到锁后读到的还是旧数据然后基于旧数据做修改导致数据错误。解决方案锁的范围必须包含整个事务。有两种方式编程式事务将事务控制放在锁的代码块内部。声明式事务调整将Transactional注解加在调用business()方法的上层方法上确保锁在事务范围内。更稳妥的做法是将“获取锁 - 执行业务 - 释放锁”这一整体放在一个没有事务的方法里然后在这个方法内部在锁的保护下再调用带有事务的Service方法。6.2 结合Spring Retry实现锁竞争的重试机制对于可重试的失败如获取锁失败可以使用Spring Retry框架实现优雅的重试而不是简单的while(true)循环。Service public class OrderService { Autowired private RedissonClient redissonClient; Autowired private RetryTemplate retryTemplate; public void createOrder(String orderId) { retryTemplate.execute(context - { RLock lock redissonClient.getLock(order_create: orderId); // 非阻塞尝试快速失败进入重试逻辑 if (lock.tryLock()) { try { // 核心下单逻辑 return doCreateOrder(orderId); } finally { lock.unlock(); } } else { // 获取锁失败抛出异常触发重试 throw new LockAcquisitionFailedException(获取订单创建锁失败 orderId: orderId); } }); } Bean public RetryTemplate retryTemplate() { RetryTemplate template new RetryTemplate(); // 指数退避策略第一次等待100ms第二次200ms第三次400ms... ExponentialBackOffPolicy backOff new ExponentialBackOffPolicy(); backOff.setInitialInterval(100); backOff.setMultiplier(2); backOff.setMaxInterval(3000); // 最大等待3秒 template.setBackOffPolicy(backOff); // 最多重试5次 SimpleRetryPolicy retryPolicy new SimpleRetryPolicy(5); template.setRetryPolicy(retryPolicy); return template; } }这样设计既避免了无限制重试又通过指数退避减轻了竞争压力。6.3 锁的细粒度化与分段锁实践对于超级热点资源如“全民疯抢的茅台酒库存”即使锁粒度到了SKU级别可能依然有数十万请求竞争一把锁。这时可以考虑分段锁Striped Lock的思路。假设茅台酒库存有10000瓶。我们可以创建10把锁每把锁管理1000瓶库存。public boolean seckill(Long productId, Integer quantity) { // 1. 基于用户ID或请求ID等决定使用哪一把分段锁 (0-9) int segment (int) (Thread.currentThread().getId() % 10); // 简单示例 RLock segmentLock redisson.getLock(product_stock_segment: productId : segment); segmentLock.lock(); try { // 2. 查询和扣减该分段对应的“虚拟库存” Integer segmentStock getSegmentStockFromRedis(productId, segment); if (segmentStock quantity) { decrSegmentStockInRedis(productId, segment, quantity); // 3. 异步或批量同步到数据库总库存 asyncUpdateTotalStockInDB(productId, quantity); return true; } return false; } finally { segmentLock.unlock(); } }这样就将竞争一把锁的流量分散到了10把锁上理论上可以将并发处理能力提升近10倍。当然这增加了系统的复杂性需要维护分段库存与总库存的一致性。这属于一种“空间换时间”和“最终一致性”的权衡适用于对极限性能有要求且能容忍短暂库存不一致如秒杀场景的业务。