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.总结

相关推荐
TDengine (老段)9 小时前
TDengine 数学函数 DEGRESS 用户手册
大数据·数据库·sql·物联网·时序数据库·iot·tdengine
TDengine (老段)9 小时前
TDengine 数学函数 GREATEST 用户手册
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
RoboWizard9 小时前
扩容刚需 金士顿新款Canvas Plus存储卡
java·spring·缓存·电脑·金士顿
安当加密9 小时前
云原生时代的数据库字段加密:在微服务与 Kubernetes 中实现合规与敏捷的统一
数据库·微服务·云原生
爱喝白开水a9 小时前
LangChain 基础系列之 Prompt 工程详解:从设计原理到实战模板_langchain prompt
开发语言·数据库·人工智能·python·langchain·prompt·知识图谱
想ai抽9 小时前
深入starrocks-多列联合统计一致性探查与策略(YY一下)
java·数据库·数据仓库
武子康10 小时前
Java-152 深入浅出 MongoDB 索引详解 从 MongoDB B-树 到 MySQL B+树 索引机制、数据结构与应用场景的全面对比分析
java·开发语言·数据库·sql·mongodb·性能优化·nosql
longgyy10 小时前
5 分钟用火山引擎 DeepSeek 调用大模型生成小红书文案
java·数据库·火山引擎
学无止境w10 小时前
高并发系统架构设计原则:无状态、水平扩展、异步化、缓存优先
缓存·系统架构
ytttr87311 小时前
C# 仿QQ聊天功能实现 (SQL Server数据库)
数据库·oracle·c#