做后端开发这几年分布式锁一直是一个绕不开的话题。只要系统从单机拆成多服务并发问题就立刻摆在你面前而Redisson几乎成了Java生态里谈分布式锁必提的框架。它不只是一把简单的锁而是基于Redis封装的一整套并发控制方案提供了可重入锁、读写锁、公平锁、信号量等多种工具既能解决最典型的“抢资源”场景也能应对复杂业务下的细粒度控制需求。这篇内容我尽量按实际项目推进的顺序来写先讲清楚分布式锁到底解决什么问题再拆解Redisson的底层原理最后直接给出一套可以落地到Spring Boot项目里的使用代码和排坑经验适合正在做微服务改造、需要控制并发访问的Java开发者也适合准备分布式锁相关面试内容的朋友。1. 为什么分布式锁这么难从单体锁到分布式锁的演进逻辑1.1 单体应用里的锁拿到分布式环境就失效在单机应用里锁几乎是透明的。两个线程同时修改一份库存synchronized或者ReentrantLock就能解决JVM会给这两个线程分配同一个锁对象线程之间通过内存屏障和等待队列实现互斥。这套机制简单直接稳定性也经过了无数项目的验证。问题在于服务拆成多节点之后同一个JVM里的锁对象就不再是“同一个”了。服务A节点上的线程抢到了锁服务B节点上的代码根本感知不到这个锁的存在——因为内存不相通。这时候如果两个节点同时处理同一个用户的下单请求库存就可能被扣两次。更早的解决方案是搞一张数据库表通过唯一索引和事务来模拟锁比如insert一条记录表示持有锁释放时删除记录。这种方式能用但性能和稳定性都很差数据库行锁的竞争会拖垮事务连接池也容易被长时间占用一旦事务回滚又没有清理记录锁就永久丢失了。所以分布式锁的核心诉求只有一条让多个独立的进程之间用一种高度可靠的共享存储来协调互斥。Redis因为高性能、单线程命令执行天然具备原子性成了最常见的载体。但直接对着Redis API写加锁解锁逻辑比大家想象中要复杂得多这也是为什么需要Redisson这样的框架来封装。1.2 用Redis自己撸一把锁坑在哪很多同学都看过那个最简单的实现SET key value NX EX 10意思是键不存在时才设置同时设置10秒过期。这个命令能保证加锁的原子性但自己实现时后面有一串问题等着你。第一个问题是解锁的原子性。解锁通常需要先GET判断是不是自己的锁再DEL删除。这两个操作不是原子的如果在判断后、删除前锁过期了别的线程就会重新加锁成功然后你把别人的锁删掉了。要解决必须用Lua脚本把GET和DEL合成一个原子操作这对没有写脚本习惯的人来说已经是门槛了。第二个问题是过期时间怎么定。设置太短业务没执行完锁就释放了并发穿透设置太长节点挂了锁一直不释放所有请求全部阻塞。没有续期机制的话很难选一个“刚刚好”的时间。第三个问题是可重入。同一个线程在业务里嵌套调用加锁逻辑比如外层锁保护了下单内层又锁了库存如果锁不记录线程身份第二次加锁会把自己阻塞死。所以自己写分布式锁要处理原子性、续期、重入、锁标识判定、Redis节点异常等一堆细节写出来容易出问题测试也难覆盖。1.3 为什么选择Redisson而不是自己写Redisson的出现本质上就是把上面这些坑全部提前填好了。它底层用的是Lua脚本保证加锁解锁的原子性内置了一个看门狗机制自动续期支持可重入还给出了公平锁、读写锁这类高一层抽象。开发者不需要关心SET NX和DEL之间那条危险的时间缝隙只要调用RLock的lock和unlock就可以了。选择Redisson的另一个理由是与Spring Boot体系贴合紧密。spring-boot-starter-redisson可以自动装配客户端直接用Autowired注入RedissonClient配置文件也支持YAML和JSON。集群、主从、哨兵模式都有对应的配置项生产环境切换部署形态时改动成本很低。而且Redisson不只有锁还提供了分布式集合、队列、限流器、原子计数等能力对后端项目来说一套客户端可以覆盖多种场景少引好多中间件依赖。2. Redisson分布式锁的核心机制拆解2.1 加锁与解锁的完整链路Redisson的锁实现基于Redis的Hash结构。锁的key对应一个HashHash里的field是加锁线程的标识value是重入计数。加锁时执行的Lua脚本核心逻辑大致是这样的先判断key是否存在不存在就直接创建Hash并写入线程标识和计数1同时设置过期时间如果key存在再判断field是不是当前线程是就加计数的值并重置过期时间否则返回0表示加锁失败。这里有几个细节值得多说一句。线程标识由UUID加线程ID组成锁不会被别的线程误释放。重入计数是int类型每次重入加1解锁时减1减到0才真正删除key并触发Redis的发布订阅消息通知正在等待锁的线程去抢锁。网上能看到的Redisson加锁脚本和这个描述基本一致目的就是为了在一个原子操作里完成判断、写入、过期时间设置三个动作。解锁的Lua脚本处理的是另一个方向的原子性。它会先检查field是否存在以及value是否大于0然后执行递减递减后如果大于0就只更新过期时间等于0就删除key并发布解锁消息。这个设计保证了无论嵌套多少层只有最外层解锁时才真正释放锁内部解锁只是减少计数并顺带续期。Redis在2.6版本之后支持了Lua脚本这给了Redisson很关键的施展空间。脚本在Redis服务端是原子的执行期间不会被其他命令插入所以并发竞争完全交给Redis单线程模型去排队从根本上避免了竞态条件。2.2 看门狗自动续期逻辑看门狗应该是Redisson最出名的一个机制。它的作用很简单业务线程还没执行完的时候锁不会因为超时被提前释放。默认情况下lock()加锁后锁的有效时间是30秒看门狗每10秒会做一次续期检查只要锁还存在就把过期时间重新设置为30秒。这个10秒是怎么算出来的30秒除以3也就是锁有效期的三分之一。看门狗并不是无限续期的。如果持有锁的线程所在JVM宕机或者客户端与Redis之间的连接断开锁就得不到续期最多30秒后自动释放避免死锁。这与本地锁不一样本地锁靠JVM销毁去自动释放分布式锁至少多了一个网络故障维度所以必须依赖过期兜底。使用lock()的时候内部逻辑是默认启用看门狗的。但如果你在调用时显式传了leaseTime比如lock(10, TimeUnit.SECONDS)Redisson就不会启动看门狗线程锁会在10秒后强制释放。也就是说看门狗和leaseTime是二选一的关系。实际业务里两种模式各有用途后面我会单独说怎么选。2.3 可重入原理可重入是Redisson里很容易被忽略但非常实用的特性。它跟Java里的ReentrantLock思路一样同一个线程可以多次获取同一把锁而不会像普通互斥锁那样直接阻塞自己。底层实现我们在前面拆解加锁脚本时已经看到了——锁数据里记录着线程标识同一个线程的每次加锁只是在value上加1。举一个具体例子。你在一个方法里加锁保护整个事务事务内部又要调用一个同样需要这把锁的公共方法比如库存扣减服务。如果锁不可重入第二次加锁会永远等待自己的第一次释放造成死锁。Redisson的可重入设计可以直接支持这种嵌套调用场景这也是它在实际项目中比裸Redis方案更好用的原因之一。解锁时同样依赖重入机制来保证安全。内部方法调用unlock只是把value从2变成1此时锁并没有真正释放外层的业务还能继续持有。要等到最外层unlockvalue从1变成0锁才彻底删除。这个机制对嵌套事务、AOP切面加锁等情况非常友好。2.4 公平锁、读写锁等扩展能力Redisson里除了最常用的非公平锁RLock还有几个值得关注的实现。FairLock是公平锁内部用Redis的List和ZSet组合实现排队。加锁时先通过ZSet记录每个等待线程的排队序号队首的线程才有资格获取锁避免线程饿死。公平锁的代价是性能比非公平锁低每次加锁解锁都要多维护一套排队结构。如果业务上没有必须按先来后到顺序执行的强诉求优先用默认锁就够了不要为了“听起来更高级”去上公平锁。ReadWriteLock是读写锁内部用Hash结构保存锁状态区分读锁和写锁。读锁之间可以共享读锁与写锁、写锁与写锁之间互斥。这个在缓存刷新场景里特别实用比如多个线程可以同时读取本地缓存但只有写线程能获得写锁去更新数据避免读操作反复被锁阻塞。Redisson还提供Semaphore、CountDownLatch等工具。Semaphore用于限制同时执行的线程数比如限制某个外部接口的最大并发请求数CountDownLatch用于等待多个分布式节点各自完成某项初始化任务后再统一放行。这些虽然不算严格意义上的“锁”但都属于Redisson分布式并发控制能力的一部分同一个客户端就能搞定。3. 从零上手实操Spring Boot Redisson 实现分布式锁3.1 依赖引入与配置文件在Spring Boot项目里引入Redisson基本上是无痛的。如果项目是基于Maven构建的直接在pom.xml里加入redisson-spring-boot-starter即可它会把Redisson客户端和Spring Boot的自动配置一起带进来。dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.2/version /dependency版本号建议用当前最新的稳定版本不要长期停留在旧版本。Redisson演进比较快新版本对Redis 7、Java 17、Spring Boot 3的支持更完整。如果项目还在用Spring Boot 2.x建议选3.20.x以上版本并查看对应兼容性说明。配置文件的写法可以直接用YAML。比如单机Redis实例spring: data: redis: redisson: file: classpath:redisson.yaml也可以直接在application.yml里写内联配置但更推荐单独的redisson.yaml结构清晰切换环境时也不用改动业务代码。redisson.yaml的基础内容如下singleServerConfig: address: redis://127.0.0.1:6379 password: null database: 0 connectionPoolSize: 64 connectionMinimumIdleSize: 16 connectTimeout: 10000 timeout: 3000 retryAttempts: 3 retryInterval: 1500这些都是最常见的基础参数。connectionPoolSize和connectionMinimumIdleSize会影响并发高峰时的连接表现生产环境建议根据压测结果调整不要照抄默认值。timeout设成3000毫秒意味着Redis命令执行超过3秒会走重试机制如果你的Redis处理能力存在波动可以适当调大。3.2 代码中加锁解锁的标准姿势使用Redisson的锁时我强烈建议固化成一套统一写法避免团队里每个人都按自己的想法写。首先注入RedissonClientAutowired private RedissonClient redissonClient;加锁的核心代码如下RLock lock redissonClient.getLock(order:create: userId); try { if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 业务逻辑 } else { throw new BizException(系统繁忙请稍后重试); } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }这里tryLock传了三个参数waitTime是等待获取锁的最长时间leaseTime是锁的自动释放时间unit是时间单位。waitTime设成3秒意味着最多等3秒拿不到锁就返回false这样可以快速给用户一个“系统繁忙”的响应而不是无限阻塞下去。leaseTime设成30秒意味着锁最多持有30秒超过这个时间自动释放避免业务异常导致死锁。finally块里的isHeldByCurrentThread()检查是必要的。lock()和tryLock()在获取锁失败时抛异常或返回false会不会触发finally里的unlock如果没有判断调用unlock就会抛出IllegalMonitorStateException干扰真正的异常信息。所以每次解锁前都要确认当前线程确实持有锁。如果你希望业务执行期间锁不被提前释放可以用不带leaseTime的lock()lock.lock(); try { // 长时间业务 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }这种写法会启用看门狗锁默认30秒有效且每10秒续期一次。适合那些执行时间不可控的业务比如对账作业、批量数据处理。不过要特别提醒一句看门狗虽然能续期但如果Redis连接中断锁最多只撑30秒所以不要依赖看门狗来掩盖业务处理过慢的问题该做异步化的还是要异步化。3.3 参数调优要点超时时间、等待时间怎么定刚开始用Redisson的人最容易犯的错就是随意拍脑袋设参数。比如waitTime设成5秒leaseTime设成60秒看起来宽松但实际效果可能很差。这里我给出一个可参考的思路。关于waitTime获取锁的最大等待时间它的大小决定了用户等待响应的上限。对于接口类请求waitTime最好控制在1至3秒内超过这个时间让用户直接看到失败提示比让他们一直转圈体验更好。对于后台任务waitTime可以适当放大到5到10秒因为这类任务的调用方本来就是系统可以接受稍长的调度延迟。关于leaseTime锁的持有时间它的核心价值是兜底不是直接约束业务时间。如果你用的是lock()让看门狗续期leaseTime其实是用不到的但如果你用tryLock显式传了leaseTime建议设置为该业务在正常情况下最大耗时的两倍左右留出一定缓冲。比如一个批量操作平常耗时2秒极端情况5秒leaseTime可以设成10秒。时间太短会导致正常业务还没跑完锁就被释放时间太长则会让异常情况下的锁泄漏时间变长。这里还有一个容易忽略的重入问题。如果你的锁会被同一个线程嵌套获取多次leaseTime设置时要考虑到内层加锁也会重置过期时间所以不用特意为嵌套层数人为加长底层脚本已经帮你处理好了。3.4 集群部署下要注意的配置细节生产环境几乎不会用单机Redis多以主从或Cluster为主。Redisson的配置和单机模式有很大差异。主从模式配置需要同时指定主节点地址和所有从节点地址Redisson会监控主节点状态在主节点切换时自动感知拓扑变化。Cluster模式则需要列出所有Master节点地址Redisson客户端会自己根据slot分配去路由命令。clusterServersConfig: nodeAddresses: - redis://10.0.0.1:7000 - redis://10.0.0.2:7000 - redis://10.0.0.3:7000 scanInterval: 2000这里特别提醒Cluster模式下锁的实现原理。Redis Cluster把key通过CRC16算法分到不同的slot里每个slot落在一个Master节点上所以一把锁对应的key会落在某一个Master上加锁和续期都只跟这个Master通信。这个设计让锁在Cluster模式下工作得挺稳定但也带出一个问题一旦这个Master宕机且锁数据还没来得及同步到从节点锁就可能丢失。Redisson本身的看门狗机制弥补了一部分但如果是强一致场景比如金融级的防重复扣款你可能需要额外考虑锁数据的高可用方案或者引入其他协调组件。主从模式下的竞态会更微妙一些。客户端写入Master后Master返回成功但异步复制还没把数据同步到Slave这时Master挂了Slave升级为Master后锁数据丢失新的线程就能加同一把锁成功。这个问题在极端的故障场景下确实存在Redisson官网对此也有相应说明所以做架构设计时不要把“锁不会丢”当成默认前提。4. 线上重点问题排查实录4.1 锁没生效或加锁报错锁没生效的原因排名第一的是Redis连接配错了。Spring Boot项目同时接入了spring-data-redis和Redisson时容易出现配置冲突导致RedissonClient初始化的不是预期的那份配置。排查时先从启动日志看Redisson连接的address是不是自己配置的实例再看控制台有没有WARN级别提示。另一种很常见的场景是多人协作时团队里有同学用了RedisTemplate去操作同一个key把Redisson写入的Hash结构给覆盖了。比如直接用SET key value命令就会把原来的Hash键类型改成String之后Redisson再去加锁执行Lua脚本就会返回类型错误。这种问题日志里会提示WRONGTYPE定位起来其实很快但要记住一个原则分布式锁这个key只能由Redisson框架来写业务代码别去碰。4.2 加锁与解锁不是同一线程导致卡死这是很多同学刚写AOP切面加锁时会踩的坑。切面方法里开了异步线程或者用了线程池去执行业务逻辑然后主线程在finally里执行unlock。但Redisson的锁是绑定线程的——加锁线程、解锁线程必须是同一个否则unlock时会抛出IllegalMonitorStateException。更麻烦的是如果你用了lock()阻塞式加锁而异步线程一直拿不到锁外层主线程去解锁还会因为状态判断不对导致异常。解决办法很简单加锁、解锁、业务逻辑全部放在同一个线程内执行。如果业务里确实需要另开线程那把锁放到异步线程内部去获取不要在主线程加锁后交给子线程解锁。还有一点如果用了Spring的Async注解同样会切换线程加锁方法要注意这一点。4.3 主从架构下的锁丢失问题前文提过Redis主从异步复制存在锁丢失窗口。这个问题多发生在Master异常宕机、Slave自动升级为Master的时刻。故障期间原本加锁成功的请求来了另一个线程因为新Master上没有锁数据会直接加锁成功于是同一份资源被两个线程同时操作。Redisson的看门狗能降低这个问题出现的概率但不能根治。因为看门狗续期也是基于Redis命令如果连不上Master续期也会失败。真要强一致业内主要有几个方向一是用RedLock算法在多个Redis节点上同时加锁必须满足大多数节点成功才算加锁成功二是换用ZooKeeper或etcd这类实现线性一致读写的组件三是在业务层做好幂等控制在极端锁失效场景下靠唯一索引、状态机等兜底。RedLock在业内还存在不少争议很多人认为它也不是绝对正确的方案所以不要盲信最好结合业务风险来评估。4.4 排查速查表遇到分布式锁相关的线上问题时下面这张表可以帮你快速定位异常现象常见原因排查方向锁一直抢不到接口超时持有锁的线程长时间阻塞或锁泄漏查看Redis中锁key是否存在TTL值是否超大定位holder线程unlock报IllegalMonitorStateException解锁线程不是加锁线程检查是否在finally里盲目调unlock确认异步线程没有跨线程传锁加锁后立刻被其他线程抢走业务执行时间超过了leaseTime改用lock()启用看门狗或调大leaseTimeREDIS命令执行错误WRONGTYPE业务代码用RedisTemplate覆盖了锁key用redis-cli查看key类型Hash类型是redisson写入的典型标识服务重启后锁还存在没用leaseTime兜底进程非正常退出检查是否有未解锁路径优化为tryLockfinally解锁Redis主从切换后锁失效异步复制丢数据考虑RedLock或业务幂等兜底5. 个人实操中的最后几点体会回到开头那句话Redisson分布式锁不是银弹但它确实把分布式锁的复杂度收敛到了一个相对可用的层次。我在实际项目里的体会是任何分布式锁的使用都要先想清楚业务对“一致性”的真实要求允许极端情况下重复执行就放宽心态用Redisson加业务幂等绝对不允许重复处理那锁之上还要叠加数据库约束。加锁代码写起来简单难的是判断这把锁该用什么粒度、设多长超时、挂了怎么办。每次遇到这类问题把锁的获取和释放做成统一模板方法再配合监控报警线上出问题的概率就会小很多。最后再分享一个小技巧一定要给锁的key设计一套容易追踪的命名规则比如business:action:resourceId同时在Redis里打开慢查询日志和键空间通知。这样一旦线上出现锁长时间未释放运维可以通过redis-cli快速定位到具体业务和方法不用翻遍代码找线索。分布式锁写得好不好从线上排障效率上就能看出来这往往是架构成熟度最真实的体现。