资讯中心

黑马点评——分布式锁模块

📅 2026/9/29 21:47:51
黑马点评——分布式锁模块
黑马点评 · 分布式锁模块整理这一章是把上一章那个synchronized换掉的过程。要解决的问题只有一个synchronized是 JVM 级别的锁部署成集群之后两个节点各锁各的一人一单又失效了。整个模块的推进顺序是三版代码 ➡️ 一个已经成熟的 Redisson 框架版本做法留下了什么问题第一版SET NX EX加锁、DEL释放会误删别人的锁第二版值里存线程标识释放前先比对比对和删除是两步中间会被打断第三版用 Lua 把「比对 删除」合成一次执行不可重入、不可重试、超时释放最终换成 Redisson 的RLock只剩主从一致性问题一、学习链路与代码落点九行课程推进顺序。红色是问题背景蓝色是概念绿色是已经写完的三版实现紫色是 Redisson灰色是后面才讲的联锁和总结。代码集中在三个文件utils/ILock.java接口、utils/SimpleRedisLock.java三版实现都在这、config/RedissonConfig.javaVoucherOrderServiceImplRedisson 的接入点。二、为什么要分布式锁外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://i-blog.csdnimg.cn/direct/3b994086991b4a61ae378ecf39092178.png)同一个用户的两个请求被 nginx 分到 8081 和 8082两个 JVM 各有一把synchronized锁互不相识于是两个请求都通过了一人一单校验。分布式锁的定义就是冲着这一点来的满足分布式系统或集群模式下多进程可见并且互斥的锁。要满足五点互斥、多进程可见、高可用、高性能、安全性异常时能自动释放。常见实现有三种课程选 Redis实现互斥方式怎么自动释放性能MySQL数据库互斥锁 / 唯一索引断开连接自动释放一般RedisSET NX这样的互斥命令靠锁的超时时间到期释放好Zookeeper节点的唯一性和有序性临时节点断开连接自动释放一般Zookeeper 略讲不是重点真正的难点不在加锁而在后面四个坑超时怎么设、误删怎么防、不可重入和不可重试怎么办、主从切换时锁会不会丢。这一章剩下的篇幅都在填这四个坑。三、第一版SET NX EX课程先给了一个接口实现的关键就一行setIfAbsent三个要点①NX提供互斥。SET key value NX EX 10里的NX是只有当 key 不存在时才设置谁设置成功谁就拿到锁。对应 Java 里的setIfAbsent。②EX提供安全性也就是防死锁。拿到锁的线程如果宕机、或者业务卡死没释放锁没有超时时间的话这把锁就永远留在 Redis 里后面所有请求全部抢不到。必须给锁一个保命的超时时间这是分布式锁和synchronized最大的不同JVM 会在进程挂掉时自动释放监视器Redis 不会。③ 非阻塞。tryLock尝试一次就返回 true / false不像synchronized会一直等。拿不到锁的请求直接返回不允许重复下单让用户重试。④ 值存什么第一版随便存比如1第二版你会发现这里必须存持有者身份privatestaticfinalStringID_PREFIXUUID.randomUUID()-;StringthreadIdID_PREFIXThread.currentThread().getId();为什么要UUID 线程id两个一起线程 id 区分同一个 JVM 内的不同线程UUID 区分不同的 JVM集群里每个节点的线程 id 都是从 1、2、3 开始的光看线程 id 分不清是哪台机器。合起来才算唯一标识一个持有者。四、第二版解决误删问题出在一个很隐蔽的时序上线程 1 加锁成功锁的超时时间是 1 秒线程 1 业务卡住了超过 1 秒锁被 Redis 自动删除线程 2 加锁成功开始执行业务线程 1 终于跑完执行DEL lock——删掉的是线程 2 的锁线程 3 也能加锁成功了临界区里同时挤进三个线程这类问题只在业务耗时 锁超时时间时出现本地测试基本测不出来属于典型的生产事故。改进思路PPT 98 页加锁时把线程标识存进值里释放时先取出锁里的标识比对一致才删。你代码里注释掉的那版就是这么写的但这一版还有问题GET和DEL是两条独立的命令两条命令之间线程可能被切换、业务可能阻塞中间仍然有被插入的空间误删还是可能发生。五、第三版用 Lua 保证原子性Redis 提供了 Lua 脚本功能一个脚本里的多条命令会被当成一次执行Redis 执行脚本是单线程、不可中断的所以只要把比对 删除写进一个脚本就变成了原子操作。调用方式是把脚本交给StringRedisTemplate执行key 放KEYS数组其它参数放ARGV数组几个细节脚本用DefaultRedisScript加载setLocation指定 classpath 下的unlock.luasetResultType指定返回值类型脚本只加载一次放在static代码块里不用每次调用都读文件stringRedisTemplate.execute(script, keys, args...)第一个参数是脚本第二个是 key 列表后面是ARGV到这里一个能用的 Redis 分布式锁就完成了SET NX EX互斥 超时防死锁 线程标识防误删 Lua 保证释放的原子性。六、但它还有四个问题以下是SETNX 锁的短板列全了这也是 Redisson 出场的理由问题现象后果不可重入同一个线程第二次SET NX会因为 key 已存在而失败锁里再调用一个同样加锁的方法自己把自己挡在门外不可重试抢不到锁只尝试一次就返回 false高并发下大量请求直接失败而不是等一会儿超时释放业务没跑完锁就到期了别人能拿到锁临界区被并发进入第四节的误删就是这么来的主从一致性主节点宕机、锁还没同步到从节点新主上没有这把锁另一个线程也能加锁成功前三个是使用体验和正确性问题第四个是架构层面的问题分别由 Redisson 和联锁解决。七、Redisson一次把前三个坑填掉Redisson 不是简单的工具类而是一个 Redis 客户端内部提供了各种分布式对象分布式锁是其中一个。三个能力分别对应三个问题① 可重入 —— 用 hash 结构代替简单 string。锁的 key 还是锁名但值变成了 hashfield 客户端UUID:线程idvalue 重入次数。加锁时先判断锁不存在就hset 1并设有效期锁存在且 field 就是自己就hincrby把次数 1 放行是别人就返回剩余 TTL。释放时hincrby -1减到 0 才真正删除锁并通知等待的线程。⚠️ 这也带来一个使用上的坑多拿一次就必须多unlock()一次。如果重入之后漏放了一次重入次数不会归零锁不会释放看门狗还会一直续期——这个用户就被永久锁死了。② 可重试 —— 用 PubSub 代替睡一会儿再递归。抢不到锁时Redisson 会订阅锁被释放的消息在等待时间内被唤醒后重试等到超时才返回 false。API 是tryLock(等待时间, 释放时间, 单位)。③ 超时续约 —— 看门狗watchDog。调用tryLock()不传 leaseTime或传 -1时启用默认租期 30 秒每releaseTime / 310 秒自动续一次只要 JVM 还活着锁就不会因为业务没跑完而提前释放。⚠️ 反过来要注意一旦显式传了 leaseTime例如tryLock(10, TimeUnit.SECONDS)看门狗就不生效锁到点就释放业务超时照样出问题。接入只需要一个配置类和一个 Bean使用方式和你现在代码里的一样课程里用两个测试方法method1 调用 method2验证了可重入你测试类现在还是空的可以照这个补上注意锁的结构没变锁依然加在事务外层try/finally包住对代理对象的调用这个原则从synchronized一路用到 Redisson。八、最后一个问题主从一致性Redis 的主从复制是异步的。线程 1 向 Master 加锁成功这条数据还没来得及同步到 SlaveMaster 就宕机了Slave 被提升为新的 Master——新主上没有这把锁线程 2 于是也能加锁成功两个线程同时认为自己持有同一把锁。Redisson 的解法是multiLock联锁准备多个相互独立的 Redis 节点不是主从是各自独立加锁时向每个节点都申请全部成功才算拿到锁任意一个失败就把已加成功的全部释放。代价也很明显节点变多、维护成本上升、加锁耗时变长、整体可用性下降。所以实际项目要权衡——多数业务用单节点 Redisson 锁就够了只有对一致性要求极高的场景才上联锁。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案