news 2026/8/29 23:12:44

一篇文章搞懂分布式锁(下)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一篇文章搞懂分布式锁(下)

一.基于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数据库中的哈希类型存储值

简单说:它让同一个线程能多次拿同一把锁,不会死锁;别人想拿?得等你彻底放完。

流程图拆解(对照你给的图):

  1. 开始 → 先看锁是否存在

    • 如果不存在 → 当前线程拿到锁,记下“我是主人”,设个过期时间(比如30秒),然后执行业务。
    • 如果存在 → 看看锁的主人是不是自己?
      • 是自己 → 锁计数 +1(表示我又进了一层),继续执行业务。
      • 不是自己 → 获取失败,要么等待,要么放弃。
  2. 执行业务过程中 → Redisson 后台有个“看门狗”

    • 每隔10秒检查一下:这个线程还在跑吗?
    • 如果在 → 自动给锁续期(重置有效期),防止业务没做完锁就掉了。
  3. 业务结束 → 准备释放锁

    • 先确认:这把锁还是我的吗?(防误删)
      • 不是我的 → 报错,不让你乱动别人的锁。
      • 是我的 → 锁计数 -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. 先确认锁属于当前线程 → 防止误删他人锁
    2. 重入计数减 1 → 支持可重入特性
    3. 若计数仍 > 0 → 只续期,不删除(还在嵌套调用中)
    4. 若计数 == 0 → 删除整个锁 key(完全释放)
  • 安全机制:即使业务超时或异常,只要不是当前线程持有,就不会被错误释放

5.Redis的锁重试和WatchDog机制

<1>、整体流程概览

Redisson 的锁机制主要分为两个部分:

  1. 左侧流程:获取锁(包含重试逻辑和 WatchDog 启动)
  2. 右侧流程:释放锁(包含通知等待者和取消 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),而是进入“订阅等待”模式。

  1. 判断剩余等待时间:如果是 tryLock 模式,会检查是否超过了用户设定的最大等待时间。如果超时,直接返回 false。
  2. 订阅释放信号:如果没有超时,当前线程会订阅一个特定的 Redis 频道(Channel)。
  3. 被动唤醒:线程进入阻塞状态,直到收到“锁已释放”的消息。
  4. 再次尝试:一旦收到消息,线程被唤醒,重新回到第一步尝试获取锁。如果还没抢到,继续等待。这个过程就是高效的“重试机制”。

<3>、释放锁流程详解(右侧)

当业务代码执行完毕,调用 unlock() 方法时,流程如下:

第一步:尝试释放锁
系统会执行一段 Lua 脚本。这段脚本非常关键,它会检查:

  • 锁是否存在?
  • 持有锁的线程 ID 是否是当前线程?
    只有满足这两个条件,才会真正执行删除操作或减少重入计数。如果不是当前线程持有的锁,直接忽略或抛出异常,防止误删别人的锁。

第二步:发送释放消息
一旦锁被成功释放(Key 被删除或计数归零),Redisson 会向之前提到的那个 Redis 频道发送一条消息。
这条消息的作用是:通知所有正在等待这把锁的线程,“锁已经没了,快来抢!”
这就是左侧流程中“订阅并等待释放锁的信号”的消息来源。

第三步:取消 WatchDog
既然锁已经释放了,后台的 WatchDog 线程就没有存在的必要了。系统会立即停止该线程,释放资源。

第四步:异常处理
如果在释放过程中发生网络异常等不可控因素,系统会记录异常日志,确保问题可追踪。

<4>、核心机制总结

为了让大家更清晰地理解,我们将图中的关键点总结如下:

  1. 为什么需要重试机制?
    因为分布式环境下,锁竞争激烈。如果不重试,一次抢不到就失败,业务就无法进行。Redisson 的重试不是盲目轮询,而是基于 Redis 的发布订阅功能,有消息才醒,没消息就睡,极大节省了 CPU 资源。

  2. 为什么需要 WatchDog?
    在实际业务中,我们很难预估一个接口到底要执行多久。如果锁设置的时间太短,业务没跑完锁就断了,会导致并发安全问题;如果设置太长,服务宕机后锁长时间不释放,又会影响其他线程。
    WatchDog 完美解决了这个问题:它让锁的有效期动态跟随业务线程的生命周期,只要线程活着,锁就活着;线程死了(宕机),WatchDog 也死了,锁自动过期。

  3. Lua 脚本的作用是什么?
    图中所有的“尝试获取”和“尝试释放”,底层都是 Lua 脚本。

  • 获取锁时:Lua 保证“检查锁是否存在”和“设置新锁”这两个动作是原子的,不会出现多线程同时认为锁空闲的情况。
  • 释放锁时:Lua 保证“校验线程 ID"和“删除锁/减少计数”是原子的,防止误删。

<5>、注意

在使用 Redisson 锁时,结合上述原理,有几点建议:

  1. 尽量使用默认的 lock() 方法
    除非你有非常明确的短时间锁定需求,否则建议使用无参的 lock() 方法,让 WatchDog 自动帮你续期。手动设置过期时间风险较大,容易因业务超时导致锁失效。

  2. 务必在 finally 块中释放锁
    无论业务是否成功,都必须在 finally 代码块中调用 unlock()。这不仅是为了释放资源,更是为了触发“发送释放消息”和“取消 WatchDog”的流程,唤醒其他等待的线程。

  3. 注意 Redis 集群环境
    上述原理基于主从架构。在极端情况下(如主节点挂掉,锁还没同步到从节点),可能会丢失锁。如果对数据一致性要求极高,建议使用 Redisson 提供的 RedLock(红锁)算法,或者考虑使用 ZooKeeper 等强一致性组件。

Redisson 的分布式锁之所以好用,是因为它把复杂的分布式协调问题(重试、续期、原子性、通知)都封装在了内部。通过这张流程图,我们可以看到它巧妙地结合了 Lua 脚本的原子性、Redis 的发布订阅机制以及本地后台线程(WatchDog),实现了一个高可用、高性能的分布式锁方案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 17:14:10

深入学习Windows系统安全1

一、Windows 系统基础认知&#xff1a;安全学习的前提想要做好 Windows 系统安全防护&#xff0c;首先要理解系统的核心基础&#xff0c;明确不同版本的安全特性和渗透关注点。操作系统作为管理和控制计算机软硬件资源的核心程序&#xff0c;是所有软件运行的基础&#xff0c;而…

作者头像 李华
网站建设 2026/7/14 17:14:12

4中claw(openclaw)工具调研决策核心逻辑

目录 1 Agent Loop 核心逻辑分析&#xff1a;为什么 Agent 能自动调用 Tool 和 LLM&#xff1f; 1 Agent Loop 核心逻辑分析&#xff1a;为什么 Agent 能自动调用 Tool 和 LLM&#xff1f; 分析项目&#xff1a;learn-claude-code&#xff08;Python&#xff09;、nanobot&am…

作者头像 李华
网站建设 2026/7/14 17:14:11

当NPC三电平遇上离散化:并网仿真的那些坑

NPC三电平逆变器离散化并网仿真&#xff0c;主电路参数见图。 使用载波层叠方式&#xff0c;可以正常运行。最近在搞NPC三电平逆变器的离散化并网仿真&#xff0c;真是踩坑无数。主电路参数懒得手敲了&#xff08;反正图里都有&#xff09;&#xff0c;今天重点说说怎么让这个铁…

作者头像 李华
网站建设 2026/7/14 17:14:11

AEAD 技术详解

AEAD&#xff08;Authenticated Encryption with Associated Data&#xff0c;带关联数据的认证加密&#xff09;是现代对称密码学的核心标准化原语&#xff0c;它在单次算法执行中&#xff0c;同时完成数据加密&#xff08;保证机密性&#xff09;与消息认证&#xff08;保证完…

作者头像 李华
网站建设 2026/7/14 17:14:09

python包发布流程

1. 整理包结构 myfirstpkg/ # 项目根目录&#xff08;名字任意&#xff09; ├── myfirstpkg/ # 核心包目录&#xff08;必须和包名一致&#xff09; │ ├── __init__.py # 包标识文件&#xff08;必填&#xff09; │ └──…

作者头像 李华