Spring Boot整合Redisson分布式锁实战:配置、踩坑与调优

发布时间:2026/10/14 10:47:31
Spring Boot整合Redisson分布式锁实战:配置、踩坑与调优
线上业务出过一次很典型的事故秒杀活动刚开始某个热门券码的接口超时率直接飙到 30%数据库连接池被打满运维半夜把我叫起来排查。日志拉出来一看同一个券码的库存余量被几十个请求同时读到每个请求都以为还有最后一张于是全部放行。那一刻我特别清醒——服务已经拆成多实例了单机时代的synchronized和本地Lock彻底失效分布式锁不是要不要上的问题而是必须马上上的问题。那次事故之后我在 Spring Boot 项目里正式整合了 Redisson 来落地分布式锁。本文就把整个整合过程、核心 API 的用法、我在生产环境踩过的坑、以及压测和调优经验完整写出来。适合正在做多实例部署、需要在库存扣减、幂等控制、定时任务防重等场景里加分布式锁的 Java 开发同学也适合那些在手写 SETNX 锁和引入 Redisson之间犹豫不决的团队做选型参考。1. 为什么我放弃了 SETNX 裸方案转向 Redisson1.1 分布式锁要解决的三个核心问题先想清楚一件事分布式锁到底在锁什么多实例部署之后一个请求可能被负载均衡打到任意一台机器上本地锁只能拦住本机内的并发拦不住跨机器的竞争。分布式锁的实质是让多台机器抢同一个资源的操作权抢到的人执行没抢到的人等待或者放弃。一个合格的分布式锁至少要满足三个条件互斥性——同一时刻只有一个客户端能持有锁安全性——不会因为持有者宕机导致死锁可用性——加锁和解锁的过程本身不能成为性能瓶颈或单点故障。很多团队一开始会手写 Redis 锁也就是常见的SET key value NX EX。但真正上线之后就会发现裸方案只是看起来能用离生产可用还差得很远。1.2 原生 SETNX 方案的三宗罪第一宗罪是锁误删。A 线程加锁后业务执行超时锁自动过期了B 线程此时成功加锁。A 线程业务终于跑完执行DEL key直接把 B 线程的锁删掉了。更糟的是如果删除前没有校验 value 是否是自己的这种误删几乎是百分百会发生。第二宗罪是锁续期缺失。设置EX 10但业务可能跑 30 秒锁到点就没了后面的请求照样冲进来。有人会把过期时间设得很长比如 5 分钟但这又带来新问题如果持有者真的宕机了锁要等 5 分钟才能释放这期间整个资源相当于被钉死了。第三宗罪是可重入性。一个方法内部又调用了另一个加锁方法同一个线程第二次加锁时会被自己挡住。手写方案要解决可重入就得在 Redis 里存持有者信息和计数代码复杂度立刻上来了。我贴一段当时手写的简化版代码大家可以直观感受一下边界条件有多难处理// 这是简化演示真实场景还有更多边角情况 public boolean tryLock(String key, String requestId, long expireSeconds) { // 1. 加锁 Boolean success redisTemplate.opsForValue() .setIfAbsent(key, requestId, expireSeconds, TimeUnit.SECONDS); if (!Boolean.TRUE.equals(success)) { return false; } // 2. 记录当前线程持有便于重入判断 localOwner.set(requestId); return true; } public void unlock(String key, String requestId) { // 必须用 Lua 保证判断 value 再删除的原子性 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; // 这里已经是在手写 Lua 脚本了后续还要处理续期、重入计数... }问题很明显每多考虑一个边界代码就膨胀一截。而且这些逻辑一旦写错都是在并发最高的时候才暴露定位成本极高。1.3 Redisson 凭什么比手写方案稳Redisson 是一个基于 Redis 的 Java 客户端但它不只是客户端更像一个 Java 分布式工具集。它把上面说的锁误删、续期、可重入这些问题全部内置解决了。最核心的是它的看门狗机制。拿到锁之后只要没指定锁的过期时间Redisson 会启动一个后台任务默认每 10 秒检查一次锁是否还持有如果业务没跑完就把锁的过期时间重置为 30 秒。这就完美解决了业务执行时间超过锁过期时间的问题。它内部用 Lua 脚本保证了加锁和解锁的原子性锁的 value 里记录了持有线程的 ID解锁时先校验再删除不会误删别人的锁。可重入也天然支持同一个线程可以多次加同一把锁内部维护一个加锁计数释放一次减一次减到零才真正删除。我自己做选型时的判断很简单分布式锁这种基础组件稳定性比少引入一个依赖重要得多。手写方案省掉的成本会在线上事故里加倍还回来。2. Spring Boot 整合 Redisson 的配置细节与连接参数2.1 Maven 依赖引入与版本匹配的坑Spring Boot 项目引入 Redisson 最常见的方式是用官方提供的 starter。版本匹配是关键我见过太多人栽在这里Spring Boot 2.x 建议搭配redisson-spring-boot-starter3.17.x 或 3.20.x 系列Spring Boot 3.x 需要redisson-spring-boot-starter3.23.x 以上的版本因为 3.x 基于 Jakarta EE 做了变更dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.20.1/version /dependency如果不确定版本兼容性最简单的办法是去 Redisson 的 GitHub 仓库看 release notes里面明确写了每个版本对应支持的 Spring Boot 版本。千万不要随便拿个最新版就往上冲starter 版本和 Spring Boot 主版本不匹配时RedissonAutoConfiguration 可能会因为类加载问题直接起不来。2.2 YAML 配置与单机、主从、集群模式的选择引入依赖后在application.yml里配置连接信息。Redisson 支持单机、哨兵、主从、集群、云托管等多种部署模式配置格式不太一样。单机模式最省事spring: data: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 redis: redisson: config: | singleServerConfig: idleConnectionTimeout: 10000 connectTimeout: 10000 timeout: 3000 retryAttempts: 3 retryInterval: 1500 password: yourpassword subscriptionsPerConnection: 5 clientName: null database: 0 address: redis://127.0.0.1:6379 threads: 16 nettyThreads: 32 transportMode: NIO主从模式和哨兵模式类似区别在于地址配置写的是主节点信息或哨兵节点列表spring: redis: redisson: config: | masterSlaveServersConfig: masterAddress: redis://10.0.0.1:6379 slaveAddresses: - redis://10.0.0.2:6379 - redis://10.0.0.3:6379 database: 0集群模式稍微复杂一点需要把所有节点地址都列出来同时要关注scanInterval参数它控制 Redisson 更新集群拓扑的频率默认 1000 毫秒一般保持默认就行。2.3 RedissonClient 的初始化与自定义配置如果你不想全用 YAML也可以在代码里手动构建RedissonClient。这种方式的灵活性更高比如你需要在启动时根据环境动态切换配置或者做一些自定义的序列化配置Configuration public class RedissonConfig { Bean(destroyMethod shutdown) public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer() .setAddress(redis:// host : port) .setPassword(password) .setDatabase(database) .setConnectionMinimumIdleSize(10) .setConnectionPoolSize(50) .setIdleConnectionTimeout(10000) .setConnectTimeout(10000) .setTimeout(3000); // 设置全局默认看门狗超时时间单位毫秒 config.setLockWatchdogTimeout(30000); return Redisson.create(config); } }destroyMethod shutdown这个细节很关键它保证 Spring 容器关闭时 Redisson 的连接池和线程池能被正确释放否则在应用停机时会出现线程泄漏的告警。我实际使用中还有个习惯把RedissonClient单独封装成一个DistributedLockTemplate而不是在业务代码里到处直接注入RedissonClient。这样后续如果要替换锁实现或者加统一的监控埋点只需要改一个类就够了。3. RLock 核心 API 的完整用法从加锁到看门狗3.1 上锁代码的三种写法与选择RedissonClient拿到手之后最常用的锁对象是RLock。核心方法有三个lock()、tryLock()、lock(long leaseTime, TimeUnit unit)它们的语义差别很大。第一种是简单粗暴的lock()拿不到锁就阻塞等待直到拿到为止。适合必须串行执行的场景但要注意如果锁永远被别的线程持有这里会一直阻塞下去所以一般不会裸用。第二种是带等待时间的tryLock(waitTime, leaseTime, unit)这是生产环境最常用的RLock lock redissonClient.getLock(order:pay: orderId); boolean locked false; try { locked lock.tryLock(5, 30, TimeUnit.SECONDS); if (!locked) { // 拿不到锁直接返回或走降级逻辑 return Result.busy(); } // 业务逻辑 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (locked) { lock.unlock(); } }这个写法里有几个细节很多人会忽视。tryLock(5, 30, TimeUnit.SECONDS)里的 5 秒是等待获取锁的时间30 秒是锁的过期时间。如果 30 秒内业务没跑完锁会自动释放除非看门狗在续期——而这里一旦指定了 leaseTime看门狗就不会启动。这是一个非常容易踩的坑我在下一节详细说。还有catch (InterruptedException)里必须恢复中断标志位。很多同学忽略这一点导致线程池里的线程中断状态被吃掉后续任务表现诡异。3.2 看门狗机制锁续期到底是怎么回事看门狗Watchdog是 Redisson 分布式锁最亮眼的特性也是误解最多的部分。当你调用lock()或者tryLock(waitTime, unit)注意没有传 leaseTime 的那一版Redisson 会给锁设置一个默认的超时时间默认值 30 秒然后启动一个后台定时任务每隔lockWatchdogTimeout / 3也就是大约 10 秒去 Redis 检查这个锁是否还被当前线程持有。如果持有就把过期时间重新刷回 30 秒。这个机制带来的好处是锁的有效期跟业务执行时间绑定了业务没结束锁就不会提前失效。但关键在于只要你显式传了 leaseTime看门狗就立刻失效。原因是 Redisson 的设计逻辑是你自己指定了锁的持有时间说明你很清楚业务最长执行多久框架就不再多管闲事。我在项目里遇到过一个线上问题某同事在tryLock里传了 leaseTime 为 10 秒但一个批量处理任务在极端数据量下跑了 40 秒。锁在第 10 秒就过期了另一个请求趁虚而入直接造成了重复处理。排查到最后发现不是什么玄学就是看门狗没启动。所以说如果业务执行时间无法预估建议用不传 leaseTime 的tryLock(waitTime, unit)如果业务执行时间非常稳定比如一次 Redis 操作最多几十毫秒那传一个合理的 leaseTime 反而更保险省得看门狗线程白白空转。3.3 解锁的规范姿势与 finally 陷阱解锁只有一条铁律必须在 finally 里解锁并且只有加锁成功才解锁。这个只有加锁成功才解锁很多人会漏掉。如果tryLock返回 false没拿到锁代码直接走进了 finally 去执行lock.unlock()Redisson 此时会抛IllegalMonitorStateException因为当前线程压根不持有这个锁。虽然业务可能看起来没出问题但异常日志会被刷爆并且掩盖真正的业务异常。还有一点是关于解锁时机的。不要为了尽早释放而在非 finally 位置调用unlock()因为业务代码可能在解锁之后抛出异常锁就无法释放了。正确姿势就是上面示例里那样用一个局部变量locked标记加锁结果finally 里统一处理。另外Redisson 的RLock是支持可重入的。同一个线程可以在持有锁的情况下再次getLock同一个 key 并加锁内部计数加一对应的 unlock 次数也要匹配。使用时要注意加锁几次就要解锁几次否则计数永远减不到零锁会一直被认为持有中最终由看门狗反复续期造成隐性的死锁。4. 库存扣减与幂等场景分布式锁落地到真实业务4.1 库存超卖复现与分布式锁解法分布式锁最常见的落地场景就是库存扣减。模拟一个超卖场景商品库存 10 件下单接口被并发打到多台机器上。如果没有锁两个请求同时读到库存为 1各自判断库存大于 0然后都去扣减最终库存变成 -1这就是超卖。用分布式锁改造后逻辑变成先抢锁抢到锁的请求才允许读取库存和扣减库存执行完毕释放锁后面的请求再进来读到的就是扣减后的新值。这里我特意强调锁内要重新查库存——有的同学把库存数量放在方法参数里缓存了锁内直接用旧值判断锁了等于没锁。给出一个简化的实现思路public boolean deductStock(Long skuId, int count) { RLock lock redissonClient.getLock(stock:deduct: skuId); boolean locked false; try { locked lock.tryLock(3, TimeUnit.SECONDS); if (!locked) { return false; } // 锁内重新查询库存 Stock stock stockMapper.selectBySkuId(skuId); if (stock.getAvailable() count) { return false; } // 乐观扣减UPDATE stock SET available available - ? WHERE sku_id ? int rows stockMapper.deductStock(skuId, count); return rows 0; } finally { if (locked) { lock.unlock(); } } }这里还隐藏着一个更深的问题即使加锁了库存更新本身尽量用数据库的条件更新来自带原子性。分布式锁解决的是多实例并发进入临界区的问题数据库层面再叠加一层乐观扣减相当于双保险。不要把锁当作唯一防线锁只是把并发问题缩小到了可控范围。4.2 锁粒度设计别把所有商品锁在一把锁上锁的粒度直接决定并发能力这是我在实际项目中反复调整才想明白的。一开始图省事把 key 设置成stock:deduct:all意图是所有扣库存操作串行化。结果大促时所有商品的扣库存请求都在抢一把锁吞吐量直接变成原来的十分之一。后来改成按商品维度 key也就是stock:deduct: skuId不同商品之间不再互相阻塞并发能力大幅提升。再往后发现即使是同一个 SKU操作类型还能再拆。比如扣库存和查库存详情其实没有共享的可变状态没必要共用一把锁。锁的粒度原则是只锁真正需要串行化的共享资源变更能用小粒度就别用大粒度。不过要注意锁粒度太小也会有代价。比如一个订单包含多个商品要同时扣减多个 SKU 的库存如果分别锁每个 SKU就可能出现 A 订单先锁了 SKU1 等待 SKU2B 订单先锁了 SKU2 等待 SKU1形成死锁等待。这种场景要么按固定顺序加锁要么用 Redisson 提供的一次性批量加锁工具后面我会讲到。4.3 幂等控制中的锁使用模式另一个高频场景是幂等控制。典型的例子是支付回调支付平台通知订单已支付如果回调处理没有幂等保护重复通知会造成重复发货、重复入账。常用的模式是防重 key 分布式锁// 生成全局唯一的幂等 key String idempotentKey pay:callback: orderNo : bizScene; RLock lock redissonClient.getLock(idempotentKey); if (!lock.tryLock(0, 5, TimeUnit.SECONDS)) { // 拿不到锁说明已经有线程在处理直接忽略本次回调 return; } try { // 业务处理... } finally { lock.unlock(); }这里tryLock(0, ...)表示拿不到锁立即返回适合幂等控制场景因为重复回调本来就不需要等待直接返回即可。需要注意锁释放之后如果又来一次重复回调还是会被处理一次所以幂等标记落库这个动作必须配合使用锁只是为了控制同一时刻只有一个人在写标记真正防重复的是数据库里的唯一索引或者状态字段。5. 踩坑实录依次排查锁失效与死锁问题的完整链路5.1 锁加了但没生效最后发现 key 设计错了有一次代码 review我看到一个同事的锁 key 是lock: user.getId()第一眼感觉没问题但仔细一想user 对象是从登录态里拿的而登录态里面存的 userId 可能在不同子系统间格式不统一有的带前缀有的不带。结果同一个用户的操作在订单服务和库存服务里生成的 key 完全不一样锁自然形同虚设。这个坑的排查过程很有代表性。现象是用户重复下单问题复现日志里明显能看到两个请求都进入了临界区但锁代码明明加了。当时的排查链路是这样的先确认 RedissonClient 都指向同一个 Redis 实例确认无问题然后在加锁和解锁处分别打印 key发现两个服务打印出来的 key 不一致最后追踪 userId 的取值链路发现是登录态解析逻辑在不同服务里返回了不同格式的 ID。提示锁 key 必须用统一的标准生成最好是独立的LockKeyBuilder类统一拼接禁止在业务代码里手工拼字符串。同时建议在 key 里带上业务场景前缀避免两个毫无关系的业务模块因为恰好用了同一个 ID 而互相阻塞。5.2 锁超时自动释放导致的并发问题与看门狗参数调整另一个典型问题是锁的过期时间设置太短。那次业务是生成报表正常情况 2 秒能跑完但数据量大的时候要 8 秒。代码里写死了tryLock(5, 3, TimeUnit.SECONDS)意思是等待 5 秒、锁持有 3 秒。结果数据量一大锁在第 3 秒就自动过期第二个请求又进来自动跑了同样的报表任务两份数据互相覆盖。排查过程比较曲折因为问题不是必现的只在大数据量任务触发时出现。我先怀疑是 Redis 连接问题导致锁异常但看监控没有连接错误然后把锁的 key 打印到日志里发现两个请求的 key 完全相同最后看 Redisson 源码里 Watchdog 的触发逻辑才发现只要传了 leaseTime看门狗根本不会启动。修复方案是去掉tryLock里的 leaseTime让看门狗接管续期// waitTime 5秒不传 leaseTime启用看门狗续期 locked lock.tryLock(5, TimeUnit.SECONDS);同时把全局lockWatchdogTimeout从默认的 30 秒调到 60 秒因为报表任务偶尔会超过 30 秒续期频率从每 10 秒一次变成每 20 秒一次对 Redis 的压力也更小。5.3 主从切换导致的锁丢失与 RedLock 的权衡这是分布式锁领域争议最多的一个话题Redis 主从架构下锁写入主节点后在主节点把数据同步到从节点之前主节点挂了从节点被提升为新主节点此时新主节点里没有这条锁记录另一个请求就能加锁成功锁的安全性被破坏。网上对 RedLock 的争论很多我自己的实践心得是绝大多数业务场景普通 Redisson 锁足够了RedLock 的强一致代价不值得付。RedLock 需要向多个独立的 Redis 实例依次申请锁过半成功才算加锁成功这会导致加锁耗时变长并且在极端情况下会出现加锁部分成功但客户端响应超时的尴尬状态反而更复杂。如果业务对锁的安全性要求极高比如金融级别的转账操作正确做法不是依赖分布式锁而是把最终一致性放到数据库事务里用唯一约束、版本号等手段兜底。分布式锁解决的是并发控制问题不是数据一致性问题这个边界一定要清醒。普通服务实例的部署架构把 Redis 主从的高可用做好、避免选举期间写入比引入 RedLock 的复杂度更实际。6. 读写锁、信号量与批量工具应对更复杂的并发场景6.1 读多写少场景用 ReadWriteLock 优化所有请求都加同一把互斥锁在读多写少的场景里是一种浪费。比如一个配置中心配置项读取极频繁但修改极少。这时候互斥锁会让所有读请求排队吞吐量上不去。Redisson 提供了RedissonReadWriteLock它把锁分成读锁和写锁多个线程可以同时持有读锁写锁是排他的写锁持有期间读锁和其他写锁都会被阻塞读锁持有期间写锁会被阻塞。当一个线程持有了任意一种锁它再次进入该锁的读写操作是安全的。RReadWriteLock rwLock redissonClient.getReadWriteLock(config:cache:xxx); RLock readLock rwLock.readLock(); RLock writeLock rwLock.writeLock(); // 读配置 readLock.lock(); try { return configCache.get(key); } finally { readLock.unlock(); } // 改配置 writeLock.lock(); try { configCache.put(key, value); // 刷新本地缓存等操作 } finally { writeLock.unlock(); }用这个工具要注意一个很微妙的点读锁和读锁并发、写锁和其他锁互斥这个语义大家容易理解但很多人会忽略锁升级问题。Redisson 和很多实现一样不直接支持从读锁升级到写锁一旦获取了读锁再尝试获取写锁可能会造成死锁等待使用时要在设计层面规避比如修改操作直接走写锁不要用先读后写的模式。6.2 信号量与分布式限流Semaphore 的用法RedissonSemaphore是分布式信号量它可以控制同时访问某个资源的并发数量适合做简单分布式限流。假设某个下游接口限流最多允许 3 个并发调用初始化信号量RSemaphore semaphore redissonClient.getSemaphore(downstream:limit); semaphore.trySetPermits(3);每次调用前获取许可调用后释放try { boolean acquired semaphore.tryAcquire(2, TimeUnit.SECONDS); if (!acquired) { return Result.busy(); } // 调用下游接口 } finally { semaphore.release(); }注意跟 JUC 的 Semaphore 一样release 操作会无条件增加一个许可所以必须保证acquire 成功了才 release“释放”的次数和“获取”的次数要匹配否则许可总数会逐渐漂移最后形同虚设。我建议每次用过之后都顺手用getAvailablePermits()打一条 debug 日志方便观察许可数是否异常。RedissonCountDownLatch则是分布式版的倒计时闩适合多任务并行等所有任务完成后统一汇总的场景。跨机器的任务协作如果靠自己用 Redis 计数很容易出现并发问题用现成的工具更省心。6.3 批量加锁的 MultiLock 与分布式锁使用边界回到前面提到的死锁场景一个操作要同时锁多个 key。比如转账把 A 账户和 B 账户同时锁住RLock lockA redissonClient.getLock(account:lock:1001); RLock lockB redissonClient.getLock(account:lock:1002); // 同时加锁避免逐个加锁导致互相等待 RMultiLock multiLock redissonClient.getMultiLock(lockA, lockB); multiLock.lock(); try { // 处理转账逻辑 } finally { multiLock.unlock(); }getMultiLock会按顺序对多个锁挨个加锁但它的价值在于释放时统一处理避免锁了 A 还没锁 B 就被打断导致的手动释放遗漏。不过要注意MultiLock 内部仍然是逐个加锁的如果锁数量很多加锁耗时会线性增长设计上尽量控制在个位数。在使用分布式锁的边界上我的经验是不要在锁里面做耗时操作。网络请求、远程调用、大量数据库写操作都不应该在锁内执行因为锁内时间越长系统吞吐量越低死锁和超时的风险越高。尽量做到锁内只做必须的、最小范围的资源检查和修改。如果一定要调用外部服务给锁的等待时间和业务执行时间都设置明确的阈值并保证业务级别的快速失败。7. 性能调优与生产环境经验沉淀7.1 压测时的响应时间与吞吐对比我在整合完成后做过一轮简单的压测用来验证分布式锁对性能的影响。测试场景是模拟 1000 个并发请求抢一个商品的库存扣减接口。在未加锁的情况下接口平均响应时间约 15ms但正确性无法保证库存频繁超卖加上 Redisson 锁之后平均响应时间大约 25msP99 大约 60ms库存数据完全正确。如果把锁粒度细化到 SKU 维度而不是全局锁吞吐量可以再翻几倍。这个结论很朴素分布式锁的性能损耗主要在 Redis 的额外 RTT 上与其纠结如何减少这 1ms 的 RTT不如把锁粒度设计得更合理。7.2 锁时间参数如何设定结合业务判断很多同学会问tryLock的 waitTime 和 leaseTime 到底怎么设置这个问题真的没有统一答案但有个通用的思考框架waitTime等待获取锁的时间取决于业务能容忍多长时间的等待。比如用户请求同步等待拿锁最多等 3 秒后台任务可以等 10 秒甚至更长。如果 waitTime 设置得太大锁竞争激烈时请求会大量堆积在线程池里会导致线程池队列膨胀。一般同步接口场景我习惯设置在 25 秒之间。leaseTime锁持有时间的设置逻辑分两种。如果启用了看门狗leaseTime 不用传交给看门狗自动续期如果关闭看门狗leaseTime 必须大于业务最坏情况下的执行时间留出 20%50% 的余量。宁可让锁多持有一点时间也不能让它提前过期这是我在实战中得到的最直接教训。7.3 可观测性锁监控、日志与告警分布式锁一旦投入使用必须有配套的监控否则出问题的时候只能靠猜。我建议在锁的封装层做三件事第一记录每次加锁和解锁的耗时。加锁耗时超过 100ms 就要注意了说明 Redis 连接或锁竞争出现了问题。第二在抢锁失败时打 WARN 日志。日志里带上锁的 key 和当前等待时间便于事后复盘为什么竞争这么激烈。第三统计锁的等待时间分布和持有时间分布接入监控平台。我封装锁模板时的一个关键记录点如下public T T executeWithLock(String lockKey, long waitTime, TimeUnit unit, SupplierT action) { RLock lock redissonClient.getLock(lockKey); long start System.currentTimeMillis(); boolean locked false; try { locked lock.tryLock(waitTime, unit); if (!locked) { LockMetrics.recordFail(lockKey); return fallback(lockKey); } LockMetrics.recordWaitTime(lockKey, System.currentTimeMillis() - start); return action.get(); } catch (Exception e) { LockMetrics.recordException(lockKey, e); throw e; } finally { if (locked) { long holdMillis System.currentTimeMillis() - start; LockMetrics.recordHoldTime(lockKey, holdMillis); lock.unlock(); } } }通过这套封装某次线上排查某个商户的订单接口变慢直接通过监控发现order:create: 开头的锁等待时间异常偏高最终定位到是一个外部系统耗时导致某个请求长时间持有锁后面的订单全部排队。如果没有锁监控这个问题很可能要等到用户投诉才暴露。7.4 Redisson 与 Spring Cache 等其他组件的协作问题最后提一个很容易被忽略的坑Spring Boot 项目里如果同时使用了spring-boot-starter-data-redis和 Redisson要注意它们管理的是不同的连接资源。Redisson 的RedissonClient和 Spring 的RedisTemplate是各自独立的它们可以共用同一个 Redis 实例但不要在业务代码里混用两套 API 去操作同一个 key 的锁数据实际的锁数据结构是经过 Redisson 编码的直接用RedisTemplate去读取会得到一串无法解析的 byte 数组。整合多个 Redis 客户端时建议明确分工锁相关的操作全部走RedissonClient普通的缓存读写走RedisTemplate两者互不交叉。如果遇到需要同时使用锁和缓存的场景比如先抢锁再更新缓存然后把锁释放保持一个请求内所有缓存操作都走同一个客户端代码可维护性会高很多。用分布式锁这一年多我最深的感受是Redisson 这款组件本身的可靠性已经很高真正出问题的往往是我们理解不深导致的错误用法。写本文的目的不是说你照着敲就一定能跑通而是希望大家在决定引入分布式锁时能想清楚它帮你解决了什么、引入了什么新约束在最关键的业务链路里经得起并发考验。如果你正准备在项目里落地分布式锁先把我提到的 case 对照着过一遍能帮你少走不少弯路。