一篇文章搞懂分布式锁(下)
一.基于setnx实现的分布式锁存在的问题
基于setnx实现的分布式锁存在下面的问题:
01 不可重入
同一个线程无法多次获取同一把锁
02 不可重试
获取锁只尝试一次就返回 false,没有重试机制
03 超时释放
锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
04 主从一致性
如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现
那么我们如何解决这些问题呢?
这里我们直接使用Redission开源框架即可
二.初识Redission
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官网地址:https://redisson.org
GitHub地址:https://github.com/redisson/redisson
三.Redission快速入门
1.引入依赖
<!--添加Redission的依赖项-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.5</version>
</dependency>
2.配置Redisson客户端
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
return Redisson.create(config);
}
}
3.使用Redis分布式锁
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws InterruptedException {
// 获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
// 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
// 判断释放获取成功
if(isLock){
try {
System.out.println("执行业务");
} finally {
// 释放锁
lock.unlock();
}
}
}
4.源代码优化
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
//3.判断秒杀是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
//4.判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
//5.1获取用户id
Long UserId = UserHolder.getUser().getId();
//创建锁对象
// SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + UserId, stringRedisTemplate);
RLock lock = redissonClient.getLock("lock:order:" + UserId);
//获取锁
boolean isLock = lock.tryLock();
//判断获取锁成功
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
//获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId, voucher);
} finally {
//释放锁
lock.unlock();
}
}
四.Redis可重入锁底层原理
1.应用场景
当同一个线程中的一个方法获取锁之后调用另一个方法,另一个方法如果需要获取锁,那么获取锁肯定失败,方法一无法结束,锁一直不能释放,就会陷入思死锁。
2.解决方法

Redisson 可重入锁是怎么工作的?
其核心原理是用了Redis数据库中的哈希类型存储值
简单说:它让同一个线程能多次拿同一把锁,不会死锁;别人想拿?得等你彻底放完。
流程图拆解(对照你给的图):
-
开始 → 先看锁是否存在
- 如果不存在 → 当前线程拿到锁,记下“我是主人”,设个过期时间(比如30秒),然后执行业务。
- 如果存在 → 看看锁的主人是不是自己?
- 是自己 → 锁计数 +1(表示我又进了一层),继续执行业务。
- 不是自己 → 获取失败,要么等待,要么放弃。
-
执行业务过程中 → Redisson 后台有个“看门狗”
- 每隔10秒检查一下:这个线程还在跑吗?
- 如果在 → 自动给锁续期(重置有效期),防止业务没做完锁就掉了。
-
业务结束 → 准备释放锁
- 先确认:这把锁还是我的吗?(防误删)
- 不是我的 → 报错,不让你乱动别人的锁。
- 是我的 → 锁计数 -1。
- 如果计数归零 → 真正释放锁(删除 Redis 里的 key)。
- 如果计数还没到零 → 只减计数,锁还留着(因为可能还有嵌套调用没退出)。
- 先确认:这把锁还是我的吗?(防误删)
—
核心优势:
- 可重入:自己调自己,不怕死锁。
- 自动续期:业务再长也不怕锁提前失效。
- 安全释放:只允许主人解锁,避免误伤他人。
3.获取锁的Lua脚本
local key = KEYS[1] -- 锁的 key
local threadId = ARGV[1] -- 当前线程唯一标识
local releaseTime = ARGV[2] -- 锁的自动释放时间(秒)
-- 判断锁是否存在
if redis.call('exists', key) == 0 then
-- 不存在,当前线程获取锁
redis.call('hset', key, threadId, '1')
-- 设置过期时间
redis.call('expire', key, releaseTime)
return 1 -- 成功获取锁
end
-- 锁已存在,判断是否是当前线程持有
if redis.call('hexists', key, threadId) == 1 then
-- 是当前线程,重入次数 +1
redis.call('hincrby', key, threadId, '1')
-- 重置过期时间(续期)
redis.call('expire', key, releaseTime)
return 1 -- 成功重入
end
-- 既不是新锁,也不是自己持有的锁 → 获取失败
return 0
脚本说明
- 返回值 1:表示成功获取锁(首次获取或重入)
- 返回值 0:表示获取失败(锁被其他线程占用)
- 使用
Hash结构存储锁信息,key 是锁名,field 是线程 ID,value 是重入计数 - 每次获取或重入都会刷新过期时间,防止业务执行中锁意外失效
4.释放锁的Lua脚本
local key = KEYS[1] -- 锁的 key
local threadId = ARGV[1] -- 当前线程唯一标识
local releaseTime = ARGV[2] -- 锁的自动释放时间(秒)
-- 判断当前锁是否还是由自己持有
if redis.call('HEXISTS', key, threadId) == 0 then
return nil -- 不是自己的锁,直接返回,不操作
end
-- 是自己的锁,重入次数减 1
local count = redis.call('HINCRBY', key, threadId, -1)
-- 判断重入次数是否已经为 0
if count > 0 then
-- 大于 0,说明还有嵌套调用未退出,不能真正释放锁
-- 重置过期时间,防止后续业务执行中锁失效
redis.call('EXPIRE', key, releaseTime)
return nil
else
-- 等于 0,说明所有嵌套都已退出,可以彻底释放锁
redis.call('DEL', key)
return nil
end
脚本说明
- 返回值始终为
nil:因为释放操作不需要返回成功/失败状态给 Java 层(Java 层通过异常处理“非本人锁”的情况) - 核心逻辑:
- 先确认锁属于当前线程 → 防止误删他人锁
- 重入计数减 1 → 支持可重入特性
- 若计数仍 > 0 → 只续期,不删除(还在嵌套调用中)
- 若计数 == 0 → 删除整个锁 key(完全释放)
- 安全机制:即使业务超时或异常,只要不是当前线程持有,就不会被错误释放
5.Redis的锁重试和WatchDog机制

<1>、整体流程概览
Redisson 的锁机制主要分为两个部分:
- 左侧流程:获取锁(包含重试逻辑和 WatchDog 启动)
- 右侧流程:释放锁(包含通知等待者和取消 WatchDog)
这两个流程通过 Redis 的发布订阅(Pub/Sub)机制紧密连接,形成一个完整的闭环。
<2>、获取锁流程详解(左侧)
当我们调用 redisson.getLock("myLock").lock() 时,系统会执行以下步骤:
第一步:尝试获取锁
系统首先会尝试向 Redis 设置一个 Key。这里底层的加锁操作是通过 Lua 脚本保证原子性的。
第二步:判断锁是否存在(TTL 检查)
系统会检查这个 Key 是否存在以及它的剩余存活时间(TTL)。
- 如果 TTL 为 null:说明锁不存在,或者已经过期。此时当前线程有机会获取锁。
- 如果 TTL 有值:说明锁正被其他线程持有。当前线程无法直接获取,进入等待逻辑。
第三步:决定是否需要 WatchDog
如果成功获取到了锁,系统会检查用户是否指定了锁的持有时间(leaseTime)。
- 情况 A:用户指定了时间(例如 tryLock(5, 10, SECONDS))。
此时 leaseTime 不等于 -1。锁会在 10 秒后自动过期,不需要后台维护。直接返回 true,业务开始执行。 - 情况 B:用户未指定时间(例如直接调用 lock())。
此时 leaseTime 等于 -1。为了防止业务执行时间超过锁的默认有效期(通常是 30 秒)导致锁意外释放,系统会立即启动 WatchDog 机制。
第四步:什么是 WatchDog(看门狗)?
WatchDog 是 Redisson 的一个后台线程。
- 工作原理:它每隔 10 秒(默认值)检查一次,发现当前线程还持有这把锁,就会把锁的过期时间重新重置为 30 秒。
- 作用:只要业务线程不结束且不调用 unlock(),锁就永远不会过期。这解决了“业务执行时间不确定”的难题。
- 停止条件:当业务执行完毕,主动调用 unlock() 释放锁后,WatchDog 会自动停止。
第五步:获取失败后的重试机制
如果第二步发现锁被别人占用了,线程不会傻乎乎地一直循环空转(那样太消耗 CPU),而是进入“订阅等待”模式。
- 判断剩余等待时间:如果是 tryLock 模式,会检查是否超过了用户设定的最大等待时间。如果超时,直接返回 false。
- 订阅释放信号:如果没有超时,当前线程会订阅一个特定的 Redis 频道(Channel)。
- 被动唤醒:线程进入阻塞状态,直到收到“锁已释放”的消息。
- 再次尝试:一旦收到消息,线程被唤醒,重新回到第一步尝试获取锁。如果还没抢到,继续等待。这个过程就是高效的“重试机制”。
<3>、释放锁流程详解(右侧)
当业务代码执行完毕,调用 unlock() 方法时,流程如下:
第一步:尝试释放锁
系统会执行一段 Lua 脚本。这段脚本非常关键,它会检查:
- 锁是否存在?
- 持有锁的线程 ID 是否是当前线程?
只有满足这两个条件,才会真正执行删除操作或减少重入计数。如果不是当前线程持有的锁,直接忽略或抛出异常,防止误删别人的锁。
第二步:发送释放消息
一旦锁被成功释放(Key 被删除或计数归零),Redisson 会向之前提到的那个 Redis 频道发送一条消息。
这条消息的作用是:通知所有正在等待这把锁的线程,“锁已经没了,快来抢!”
这就是左侧流程中“订阅并等待释放锁的信号”的消息来源。
第三步:取消 WatchDog
既然锁已经释放了,后台的 WatchDog 线程就没有存在的必要了。系统会立即停止该线程,释放资源。
第四步:异常处理
如果在释放过程中发生网络异常等不可控因素,系统会记录异常日志,确保问题可追踪。
<4>、核心机制总结
为了让大家更清晰地理解,我们将图中的关键点总结如下:
-
为什么需要重试机制?
因为分布式环境下,锁竞争激烈。如果不重试,一次抢不到就失败,业务就无法进行。Redisson 的重试不是盲目轮询,而是基于 Redis 的发布订阅功能,有消息才醒,没消息就睡,极大节省了 CPU 资源。 -
为什么需要 WatchDog?
在实际业务中,我们很难预估一个接口到底要执行多久。如果锁设置的时间太短,业务没跑完锁就断了,会导致并发安全问题;如果设置太长,服务宕机后锁长时间不释放,又会影响其他线程。
WatchDog 完美解决了这个问题:它让锁的有效期动态跟随业务线程的生命周期,只要线程活着,锁就活着;线程死了(宕机),WatchDog 也死了,锁自动过期。 -
Lua 脚本的作用是什么?
图中所有的“尝试获取”和“尝试释放”,底层都是 Lua 脚本。
- 获取锁时:Lua 保证“检查锁是否存在”和“设置新锁”这两个动作是原子的,不会出现多线程同时认为锁空闲的情况。
- 释放锁时:Lua 保证“校验线程 ID"和“删除锁/减少计数”是原子的,防止误删。
<5>、注意
在使用 Redisson 锁时,结合上述原理,有几点建议:
-
尽量使用默认的 lock() 方法
除非你有非常明确的短时间锁定需求,否则建议使用无参的 lock() 方法,让 WatchDog 自动帮你续期。手动设置过期时间风险较大,容易因业务超时导致锁失效。 -
务必在 finally 块中释放锁
无论业务是否成功,都必须在 finally 代码块中调用 unlock()。这不仅是为了释放资源,更是为了触发“发送释放消息”和“取消 WatchDog”的流程,唤醒其他等待的线程。 -
注意 Redis 集群环境
上述原理基于主从架构。在极端情况下(如主节点挂掉,锁还没同步到从节点),可能会丢失锁。如果对数据一致性要求极高,建议使用 Redisson 提供的 RedLock(红锁)算法,或者考虑使用 ZooKeeper 等强一致性组件。
Redisson 的分布式锁之所以好用,是因为它把复杂的分布式协调问题(重试、续期、原子性、通知)都封装在了内部。通过这张流程图,我们可以看到它巧妙地结合了 Lua 脚本的原子性、Redis 的发布订阅机制以及本地后台线程(WatchDog),实现了一个高可用、高性能的分布式锁方案。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)