Redis-实现分布式锁

目录

1.JVM锁在集群下的漏洞

2.分布式锁实现​

3.基于Redis实现(简易)

3.1线程安全问题

3.2解决分布式锁误删​

代码改动

3.3实现分布式锁原子性​

漏洞场景

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

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

4.1Redissoon​

4.2可重入锁​

[1. 存储结构:Redis Hash](#1. 存储结构:Redis Hash)

[2. 加锁/重入:一段 Lua 脚本完成](#2. 加锁/重入:一段 Lua 脚本完成)

[3. 看门狗(续期机制)](#3. 看门狗(续期机制))

[4. 解锁:计数减到 0 才真正删 Key](#4. 解锁:计数减到 0 才真正删 Key)

[5. 公平/非公平可选](#5. 公平/非公平可选)

[6. 一句话总结](#6. 一句话总结)

4.3不可重试与超时释放

[1. 入口:tryLock 带等待时间](#1. 入口:tryLock 带等待时间)

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

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

[4. 退出条件](#4. 退出条件)

[5. 与"看门狗"的关系](#5. 与“看门狗”的关系)

[6. 一句话总结](#6. 一句话总结)

4.4联锁

[1. 结构:只是一个 List 封装器](#1. 结构:只是一个 List 封装器)

[2. 加锁流程:顺序遍历 + 失败即回滚](#2. 加锁流程:顺序遍历 + 失败即回滚)

[3. 解锁流程:全部释放,异常也不停](#3. 解锁流程:全部释放,异常也不停)

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

[5. 典型应用场景](#5. 典型应用场景)

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

[7. 一句话总结](#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) then
    redis.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) then
    redis.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) then
    return nil;          -- 不是当前线程,直接返回
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) then
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 0;
else
    redis.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 的区别(易混淆)

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

7. 一句话总结

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

5.总结

相关推荐
论迹3 小时前
【Redis】-- 分布式锁
数据库·redis·分布式
小志开发4 小时前
SQL从入门到起飞:完整数据库操作练习
数据库·sql·学习·oracle·sqlserver·navicat
或与且与或非4 小时前
rust使用sqlx示例
开发语言·数据库·rust
王不忘.4 小时前
MySQL 数据库核心知识点详解
数据库·mysql
时序数据说4 小时前
时序数据库 IoTDB:支撑万亿级物联网设备的基石
大数据·数据库·物联网·时序数据库·iotdb
橙-极纪元4 小时前
11种数据库类型详解-第3种:时序数据库(TSDB)
数据库·时序数据库
摆个烂4 小时前
时序数据库深度解析:从基础概念到未来趋势
数据库·时序数据库
时序数据说5 小时前
哪些行业需要使用时序数据库?
大数据·数据库·物联网·时序数据库
开着拖拉机回家5 小时前
【MongoDB】mongoDB数据迁移
数据库·mongodb·nosql·数据库迁移·mongodump·mongorestore