从单机锁到 Redis 分布式锁
前言在单体服务架构下synchronized、ReentrantLock可以轻松解决多线程并发竞争问题。但微服务、集群部署普及后单机锁作用域仅局限于当前 JVM 进程多服务实例之间无法互斥超卖、数据覆盖等并发问题随之而来。行业通用解决方案是基于 Redis 实现分布式锁从最基础SETNX手写锁再到解决「服务宕机死锁」引入过期时间最后攻克「业务未执行完锁提前失效」痛点诞生看门狗WatchDog机制。本文按照单机锁→原生 Redis 分布式锁→Redisson 分布式锁 阻塞等待逻辑→看门狗底层原理完整演进脉络结合源码参数waitTime/leaseTime、本地Semaphore阻塞、Lua 原子脚本、续期逻辑进行讲解一、单机锁synchronized 与 ReentrantLock1. synchronized 内置锁底层依赖 JVM 监视器锁ObjectMonitor作用域为单个 JVM 进程优势语法简单、自动释放锁无需手动处理异常局限非公平锁、不可中断、无法手动控制锁等待时长致命缺陷集群多实例环境下完全失效A 服务加锁不会阻塞 B 服务并发请求。2. ReentrantLock 可重入显式锁基于 AQSAbstractQueuedSynchronizer底层实现同样仅单机生效核心特性可重入、公平 / 非公平可选、支持中断、支持tryLock(time)限时等待底层阻塞逻辑竞争失败的线程进入 AQS 双向阻塞队列由 JUC 本地Semaphore/LockSupport完成线程挂起与唤醒共性局限锁状态存储在 JVM 堆内存多进程、多服务器之间完全隔离无法跨实例互斥。单机锁缺点锁信息存储在进程内部内存无全局共享存储介质。当系统拆分为集群微服务多个 JVM 进程独立持有锁同一资源会被多个服务同时操作并发安全彻底失效。因此我们需要一个所有服务都能访问的全局共享存储Redis 成为最优选择分布式锁应运而生。二、原生 Redis 分布式锁分布式锁核心诉求全局互斥、防止死锁、可重入、阻塞等待、锁自动续期原生 Redis 从最简单 SETNX 逐步完善。1.初代方案SETNX 实现互斥SETNX key value仅当 key 不存在时设置成功模拟加锁逻辑// 加锁 Boolean success redisTemplate.opsForValue().setIfAbsent(stock_lock, client_id); if (success) { // 执行业务 // 释放锁 redisTemplate.delete(stock_lock); }致命漏洞服务加锁成功后突然宕机DELETE 释放锁代码无法执行锁永久存在永久死锁。2. 二代方案SETNX EXPIRE 过期时间解决死锁为锁设置 TTL即使服务宕机到期 Redis 自动删除 key 释放锁setIfAbsent(stock_lock, client_id); expire(stock_lock, 30, TimeUnit.SECONDS);漏洞两条命令非原子。若SETNX执行成功、EXPIRE执行前服务宕机依旧死锁。3.三代方案原子 SET 命令SET key value NX EXRedis 提供原子复合命令一步完成「不存在则设置 过期时间」彻底解决原子性问题SET stock_lock client_id NX EX 30新的局限诞生业务执行时长不可控锁过期但业务还未跑完。 举个场景锁过期 30s业务执行需要 50s30s 时 Redis 自动删除锁其他线程抢占锁两个线程同时操作库存出现超卖。 解决方案有两个方向人为预估业务最大时长设置超长过期时间弊端预估不准依然失效宕机后长时间锁占用自动续期机制锁持有期间持续刷新 TTL也就是 Redisson 看门狗 WatchDog。4.补充缺陷原生 SET 锁不支持可重入同一线程多次加锁会直接加锁失败递归场景、嵌套锁场景无法使用同时没有阻塞等待逻辑抢锁失败只能循环自旋CPU 空转消耗资源。三、Redisson 标准化分布式锁Redisson 封装 Lua 脚本、发布订阅、本地信号量、看门狗续期一次性补齐原生锁所有缺陷是企业主流生产方案。1. 核心两个关键参数waitTime、leaseTime理解看门狗开关核心方法原型boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)waitTime抢锁最大阻塞等待时长 线程第一次抢锁失败后最多循环尝试多久获取锁超时未拿到锁直接返回 false。底层依靠 Redis Pub/Sub JUC 本地Semaphore阻塞线程避免无限自旋。leaseTime锁在 Redis 中的硬过期时间看门狗的开关leaseTime -1无参 lock () 默认值开启看门狗自动续期leaseTime 0手动指定过期时间如 lock (10, s)禁用看门狗到期强制释放锁。if (leaseTime ! -1) { // 手动指定过期时间不启动看门狗 return tryLockInnerAsync(...); } // leaseTime-1抢锁成功后启动WatchDog续期任务 ttlRemainingFuture.onComplete((ttlRemaining, e) - { if (ttlRemaining null) { scheduleExpirationRenewal(threadId); // 开启看门狗 } });核心规则只有leaseTime0-1时Redisson 才认为业务执行时长不可预估启动后台定时续期。2.抢锁失败阻塞逻辑本地 Semaphore 实现线程挂起线程调用lock()抢锁失败不会无脑 while 循环空转完整流程执行 Lua 加锁脚本返回锁剩余 TTLttl0 代表锁被占用当前线程订阅锁专属 Pub/Sub 频道redisson_lock__channel:{lock_key}获取客户端本地维护的Semaphore信号量初始许可 0调用tryAcquire(ttl, 毫秒)线程直接阻塞挂起释放 CPU这里的Semaphore是 JDK 本地同步工具底层基于 AQS仅当前 JVM 内生效和分布式限流 RSemaphore 完全无关持有锁线程执行unlock()解锁时Lua 脚本执行PUBLISH推送解锁消息到频道订阅监听器收到消息调用semaphore.release()释放许可阻塞线程被唤醒唤醒后回到外层 while 循环再次调用tryAcquire竞争锁直到获取成功或 waitTime 超时。流程简化链路 抢锁失败 → 订阅频道 → Semaphore 阻塞线程AQS 挂起→ 其他线程解锁发布消息 → release 信号量唤醒线程 → 重新抢锁。3.可重入锁底层 Hash 存储结构Redis 锁 key 采用 Hash 结构天然支持可重入彻底解决原生 SET 锁不可重入问题key锁名称如stock_lock field客户端唯一UUID:当前线程ID全局唯一标识线程 value锁重入计数初始1每重入1解锁-1Lua 加锁原子逻辑key 不存在新建 Hash计数 1设置默认 30s 过期key 存在且 field 匹配当前线程计数 1刷新过期时间key 存在但 field 不匹配返回锁剩余 TTL加锁失败。解锁 Lua 强校验仅允许加锁线程释放锁非持有者解锁直接抛出异常防止误删他人锁计数递减至 0 才删除 Redis 锁 key。四、看门狗 WatchDog1.定义Redisson 内置后台异步定时任务仅在leaseTime-1无参 lock ()时激活业务线程持有锁期间定时刷新锁 TTL避免业务未执行完成锁自动释放。2.默认规则锁默认过期时间30s看门狗检测周期10s过期时间的 1/3逻辑 业务线程持有锁期间看门狗后台定时线程每 10s 执行一次把锁的过期时间重置回 30 秒。触发关闭条件满足其一就停止续期业务代码执行完毕主动unlock()释放锁持有锁的 JVM 进程宕机 / 崩溃无线程续期30s 后锁自动失效。3.完整执行流程调用无参lock()内部传入leaseTime-1加锁 Lua 脚本创建 Hash 锁设置 30s 过期加锁成功后调用scheduleExpirationRenewal()启动看门狗定时任务 定时任务基于 Netty 时间轮HashedWheelTimer调度不占用业务主线程每 10s 执行一次续期 Lua 脚本校验锁 key 存在且持有线程为当前客户端线程执行PEXPIRE key 30000将锁过期时间重置为 30s续期成功则递归调度下一次 10s 后的续期任务循环往复两种终止续期场景业务正常执行完毕调用unlock()Lua 脚本删除锁 key主动取消看门狗定时任务停止续期JVM 进程宕机 / 业务线程卡死看门狗定时线程同步销毁无续期操作30s 后 Redis 自动淘汰锁杜绝死锁。4.优缺点与适用场景优点无需人工预估业务执行时长长耗时接口、第三方调用场景无锁提前释放风险进程宕机自动兜底释放锁不会永久死锁底层 Lua 原子续期先校验锁归属再刷新 TTL不会给已被抢占的锁无效续期。缺点后台定时线程占用少量服务资源业务无限循环卡死时看门狗会持续续期锁需人工清理 Redis key仅单节点 Redis 下稳定主从切换异步复制可能短暂丢锁可使用红锁 RedLock 优化。场景区分使用lock()无参leaseTime-1长耗时业务、执行时长不可预估启用看门狗使用lock(10, TimeUnit.SECONDS)短任务可精准预估执行时间禁用看门狗到期自动释放。