当前位置: 首页 > news >正文

Redis-实现分布式锁

目录

1.JVM锁在集群下的漏洞

2.分布式锁实现​

3.基于Redis实现(简易)

3.1线程安全问题

3.2解决分布式锁误删​

代码改动

3.3实现分布式锁原子性​

漏洞场景

Lua脚本实现分布式锁原子性

4.基于Redis的分布式锁优化​

4.1Redissoon​

4.2可重入锁​

1. 存储结构:Redis Hash

2. 加锁/重入:一段 Lua 脚本完成

3. 看门狗(续期机制)

4. 解锁:计数减到 0 才真正删 Key

5. 公平/非公平可选

6. 一句话总结

4.3不可重试与超时释放

1. 入口:tryLock 带等待时间

2. 重试总流程(简化源码)

3. 等待-唤醒机制(PubSub + 信号量)

4. 退出条件

5. 与“看门狗”的关系

6. 一句话总结

4.4联锁

1. 结构:只是一个 List 封装器

2. 加锁流程:顺序遍历 + 失败即回滚

3. 解锁流程:全部释放,异常也不停

4. 一致性保证(可重入、超时、回滚)

5. 典型应用场景

6. 与 RedLock 的区别(易混淆)

7. 一句话总结

5.总结


1.JVM锁在集群下的漏洞

在集群模式下,JVM 锁(如 synchronizedReentrantLock)存在根本性漏洞
它只能保证「同一个 JVM 进程」内的互斥,无法跨节点生效。因此,当业务部署了多台实例时,JVM 锁会完全失效,导致并发安全问题。具体表现与成因如下:


🔍 核心漏洞:锁的「作用域」仅限当前进程

场景单机集群(多节点)
锁实现synchronized/ReentrantLock同上
锁对象当前堆里的 Object每个节点各自独立的对象
结果互斥成功多节点同时拿到锁,互斥失败

2.分布式锁实现

3.基于Redis实现(简易)

分布式锁类-获得锁,释放锁方法

主类

3.1线程安全问题

线程1业务堵塞超时后被强制释放锁,随即线程2申请到锁开始执行业务。

此时线程1恢复业务执行完后释放锁——此时将线程2申请的锁释放了。

随即线程3申请到锁开始执行业务。

此时线程2与线程3都在执行业务,存在线程安全问题。

3.2解决分布式锁误删

在获取锁的时候存入线程标示。

释放锁的时候对比该锁的线程标示是否和自己相同,只有相同的情况下才能释放。

代码改动

由于线程自身id都是自增,若在集群环境下,系统生成的线程id会出现重复。

所以通过UUID为线程id实现唯一。

3.3实现分布式锁原子性

漏洞场景

虽然可以通过线程标识判断当前锁是否可释放。

但是若判断锁可释放后,在释放锁时发生堵塞,由于超时自动释放锁。

此时线程2获取锁后开始执行业务。

而线程1此时恢复由于之前已经判断过锁可释放,此时直接释放锁——将线程2锁释放了。

此时线程3又获取锁,线程2与线程3并发执行,存在线程安全。

Lua脚本实现分布式锁原子性

总结三句话

  1. Redis 单线程 ⇒ 脚本一旦开始就不会被别的命令打断;

  2. Lua 把“读-判断-写”打包 ⇒ 外界看就像一条命令;

  3. 所以用 Lua 就能在不加锁、不引入复杂协议的前提下,白嫖到原子性。

4.基于Redis的分布式锁优化

4.1Redissoon

4.2可重入锁

Redisson 可重入锁(RLock)在单台 Redis 上就能保证线程级可重入 + 分布式互斥,核心思路是:

“同一线程可以多次加锁,每加一次计数 +1;释放时计数 -1;计数归零才真正删 key。”

下面按存储结构 → 加锁/重入 → 看门狗 → 解锁四步拆解。


1. 存储结构:Redis Hash

  • key = 锁名(如 myLock

  • field = UUID:threadId(客户端实例唯一标识 + 线程 ID)

  • value = 重入次数(初始 1,每次重入 +1)

复制

myLock: {"7f3e7498-36a7-4c65-996b-3a2b3c4d5e6f:123": 3
}

2. 加锁/重入:一段 Lua 脚本完成 

逻辑

  1. 锁不存在 → hincrby 创建计数并设置过期时间(默认 30 s)。

  2. 锁已存在且 field 存在 → 同一线程重入,计数 +1,重新刷过期时间。

  3. 锁已存在但 field 不存在 → 获取失败,返回剩余 TTL,供外层阻塞或重试。

Lua 骨架(简化):

if (redis.call('exists', KEYS[1]) == 0) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;
return redis.call('pttl', KEYS[1]);
  • KEYS[1] = 锁名

  • ARGV[1] = 过期毫秒数

  • ARGV[2] = UUID:threadId


3. 看门狗(续期机制)

  • 如果调用 lock()不指定 leaseTime,Redisson 会启动一个 后台定时任务(10 s 检查一次),只要当前线程还持有锁,就把过期时间重新刷成 30 s,防止业务没完锁就被 Redis 删掉

  • 一旦线程调用 unlock() 把计数减到 0,定时任务随之取消。


4. 解锁:计数减到 0 才真正删 Key

同样用 Lua 保证原子性:

if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) thenreturn nil;          -- 不是当前线程,直接返回
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) thenredis.call('pexpire', KEYS[1], ARGV[1]);return 0;
elseredis.call('del', KEYS[1]);return 1;
end;
  • 计数 > 0 → 只减计数,不删 Key(重入释放)。

  • 计数 = 0 → 删除整个 Key(真正释放)。


5. 公平/非公平可选

  • 非公平锁(默认 RLock)——抢锁线程无序,谁先到谁得。

  • 公平锁RFairLock)——用 Redis List + ZSet 维护FIFO 排队,先请求的线程先拿到锁。


6. 一句话总结

Redisson 可重入锁把 “线程身份” 保存在 Redis Hash 的 field 中,用计数器实现重入,用Lua 脚本保证原子,用看门狗自动续期,计数归零才真正释放,从而单 Redis 节点即可支持分布式 + 可重入 + 不死锁

4.3不可重试与超时释放

Redisson 的“可重试锁”并不是一种新的锁类型,而是 RLock.tryLock(long waitTime, long leaseTime, TimeUnit)waitTime > 0 时自动表现出来的 获取失败后再重试 机制。
核心目标:在指定等待时间内,尽可能让线程最终拿到锁,而不是失败就立即返回 false

下面按 调用入口 → 重试流程 → 等待-唤醒机制 → 超时退出 四段说明。


1. 入口:tryLock 带等待时间

// 最多等 3s,锁 10s 后自动过期
boolean isLock = redisson.getLock("orderLock").tryLock(3, 10, TimeUnit.SECONDS);
  • leaseTime=10s 只是“万一我没解锁”的兜底 TTL,不影响重试

  • waitTime=3s 才是“可重试”的总时限;内部会循环+阻塞直到超时。


2. 重试总流程(简化源码)

long remain = unit.toMillis(waitTime);
while (remain > 0) {// 2.1 一次原子化抢锁(Lua 脚本)Long ttl = tryAcquireOnceAsync(remain, leaseTime, threadId);if (ttl == null) {          // 返回 null 表示抢到了return true;}// 2.2 没抢到,决定等多久long await = Math.min(remain, ttl);   // 等“剩余等待时间”与“锁过期时间”较小值boolean gotSignal = subscribeAndAwaitLockReleased(await); // 订阅解锁事件if (!gotSignal) {           // 等待超时仍没通知return false;}remain -= (System.currentTimeMillis() - start); // 扣掉已用时间
}
return false;

remain获取等待时间(毫秒)

当等待时间>0时,进入抢锁主循环。

RedissonLock.tryLockInnerAsync方法中拼接了Lua脚本保持抢锁的原子性。


3. 等待-唤醒机制(PubSub + 信号量)

  1. 抢锁失败后线程执行
    SUBSCRIBE redisson_lock__channel:{orderLock}
    监听锁释放事件

  2. 持有锁的线程释放锁时,Lua 脚本里会
    PUBLISH redisson_lock__channel:{orderLock} 1
    解锁消息推送给正在等待的客户端。

  3. 等待线程收到消息后立即被唤醒再次循环抢锁
    若规定时间内没收到消息,则放弃订阅,返回 false。

这种“先订阅再阻塞,解锁事件唤醒”的方案避免了无脑自旋,对 CPU 非常友好 。


4. 退出条件

条件行为
抢到锁(Lua 返回 null)立即返回 true
剩余等待时间耗尽返回 false
解锁消息及时到达被唤醒后继续下一轮抢锁

5. 与“看门狗”的关系

  • 可重试解决“获取”阶段的失败重试。

  • 看门狗解决“拿到锁后”业务还没执行完导致 TTL 过期的续租问题。
    两者独立但可共存:先重试拿到锁,再看门狗定时续期。


6. 一句话总结

Redisson 的“可重试锁”利用 Lua 原子抢锁 + 解锁 PubSub 通知 + 限时循环等待 三板斧,在 waitTime阻塞-唤醒-再抢,既保证最终成功率,又避免忙等耗 CPU

通过 “leaseTime 强制过期”“看门狗自动续期” 两种互补策略,既保证锁最终一定会被释放(无死锁),又允许业务执行时间不确定(无误释放);开发者只需按任务长短选择是否指定 leaseTime,即可优雅解决“超时释放”问题

4.4联锁

Redisson 的 MultiLock(联锁)并不是一种“新锁”,而是把多个独立的 RLock 打包成一把逻辑锁——
要么全部加锁成功,要么全部回滚释放,对外暴露的 API 和单机锁一模一样,内部却用**“遍历 + 回滚”策略实现分布式场景下的原子性**。

下面按 结构 → 加锁流程 → 解锁流程 → 一致性保证 → 应用场景 五步彻底剖开。


1. 结构:只是一个 List<RLock> 封装器

public class RedissonMultiLock implements Lock {private final List<RLock> locks;   // 内部维护的普通锁列表public RedissonMultiLock(RLock... locks) {this.locks = Arrays.asList(locks);}
}
  • 可以来自同一 Redis 实例的不同 key,也可以跨实例 / 跨机房

  • 对外暴露的 lock() / tryLock() / unlock() 与单机锁无差别。


2. 加锁流程:顺序遍历 + 失败即回滚

核心代码(精简):

long remain = unit.toMillis(waitTime);
List<RLock> acquired = new ArrayList<>();
for (RLock lock : locks) {long elapsed = System.currentTimeMillis() - start;remain = time - elapsed;if (remain <= 0 && waitTime != -1) {unlockInner(acquired);          // ① 超时了,回滚已拿到的锁return false;}boolean ok = lock.tryLock(remain, unit);if (!ok) {unlockInner(acquired);          // ② 当前锁获取失败,回滚return false;}acquired.add(lock);                 // ③ 记录成功
}
return true;                            // 全部成功
  • 顺序尝试,实时扣减剩余等待时间,防止因某个节点慢导致整体卡死;

  • 任何一步失败立即 逆序释放已拿到的锁,保证“整锁整放”。


3. 解锁流程:全部释放,异常也不停

public void unlock() {for (RLock lock : locks) {try {lock.unlock();} catch (Exception ignore) {// 个别锁解锁失败也不中断,继续释放其余}}
}
  • 不判断持有者不中断循环,最大限度避免“残留锁”。


4. 一致性保证(可重入、超时、回滚)

问题MultiLock 解法
部分成功失败即 unlockInner 逆序释放,原子性靠回滚实现
可重入不自己维护计数,依赖单个 RLock 的可重入语义;同线程重复调用,内部 tryLock 会返回 true
超时控制遍历里实时 remain = time - elapsed防止单节点慢拖死整体
节点宕机若某 RLock 一直 tryLock 失败,整体拿不到锁,不会形成“半锁”状态

5. 典型应用场景

  1. 转账 / 聚合订单
    需要同时锁定多个账户或订单,“只锁一半”会出资金风险

    RLock l1 = redisson1.getLock("acc:1001");
    RLock l2 = redisson2.getLock("acc:1002");
    RedissonMultiLock ml = new RedissonMultiLock(l1, l2);
    ml.lock();
    try { /* 执行转账 */ } finally { ml.unlock(); }
  2. 跨机房调度
    不同机房各有一组 Redis,MultiLock 把多机房锁打包,避免主从同步延迟带来的“锁丢失”。

  3. 批量库存扣减
    多个商品库存分布在 不同分片 Redis一次性全锁住再批量扣,防止超卖。


6. 与 RedLock 的区别(易混淆)

特性MultiLockRedLock
节点角色任意(可主从、可单实例不同 key)必须 N 个独立 Master
成功条件全部成功≥ N/2+1 成功
实现层面Java 端遍历+回滚Lua 脚本 + 时钟漂移校正
一致性强度全有或全无(业务原子)多数派(容忍少数节点故障)

7. 一句话总结

Redisson MultiLock 通过Java 端顺序遍历加锁、失败逆序回滚的简单策略,把多把分布式锁组合成一把“整体原子”锁,既解决了多资源同时互斥的需求,又天然规避了主从复制延迟带来的锁丢失问题,是“全有或全无”高一致性场景的首选利器

5.总结

http://www.dtcms.com/a/391003.html

相关文章:

  • 软件工程实践五:Spring Boot 接口拦截与 API 监控、流量控制
  • 【LINUX网络】NAT _ 代理_ 内网穿透
  • 智慧养老+数字大健康:当科技为“银发时代”按下温暖加速键
  • rook-ceph的ssd类osd的纠删码rgw存储池在迁移时的异常处理
  • Http升级Https使用Certbot申请证书并免费续期
  • scTenifoldKnk:“虚拟敲除基因”,查看转录组其他基因的变化幅度(升高or降低)
  • 牛客算法基础noob47 校门外的树
  • AD-GS:稀疏视角 3D Gaussian Splatting 的“交替致密化”,同时抑制浮游物与保留细节
  • maven package多出来一个xxx.jar.original和一个xxx-shaded.jar是什么?怎么去掉
  • Gin 框架中使用 Validator 进行参数校验的完整指南
  • apt install nvidia-cuda-toolkit后cuda不在/usr/local/cuda怎么办
  • SpringBoot整合Kafka总结
  • Parasoft C/C++test 针对 CMake 项目的自动化测试配置
  • LED强光手电筒MCU控制方案开发分析
  • linux中为什么 rm 命令能删除自己 | linux使用rm命令删自己会怎样?
  • django登录注册案例(下)
  • 【TES600G】基于JFM7K325T FPGA+FT-M6678 DSP的全国产化信号处理平台
  • 卷积神经网络深度解析:从基础原理到实战应用的完整指南
  • 企业档案管理系统:精准破局制造行业档案管理困境
  • 【完整源码+数据集+部署教程】考古坑洞私挖盗洞图像分割系统: yolov8-seg-act
  • MMDB详解
  • TC8:SOMEIP_ETS_130测试用例解析
  • 等效学习率翻倍?梯度累积三连坑:未除以 accum_steps、调度器步进错位、梯度裁剪/正则标度错误(含可复现实验与修复模板)
  • 嵌入式学习笔记(44)IMX6ULL
  • OpenStack 学习笔记(五):网络管理和虚拟网络实践与存储管理实验(下)
  • 博睿数据携手华为共筑智能未来,深度参与HUAWEI CONNECT 2025并发表主题演讲
  • 陈童理论物理新讲1 哈密顿力学初步
  • 9.19 Sass
  • 设计模式详解:单例模式、工厂方法模式、抽象工厂模式
  • 终端同居物语:Shell咏唱术式与权限结界の完全解析书