Redission 分布式锁原理分析

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

43f8ee9b5d6352563a6ffff8b4415a6b.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号、CRM 等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:老周聊架构

10bea5c27e8172d240ab4eeeb074a69f.jpeg


一、前言

我们先来说说分布式锁,为啥要有分布式锁呢? 像 JDK 提供的 synchronized、Lock 等实现锁不香吗?这是因为在单进程情况下,多个线程访问同一资源,可以使用 synchronized 和 Lock 实现;在多进程情况下,也就是分布式情况,对同一资源的并发请求,需要使用分布式锁实现。而 Redisson 组件可以实现 Redis 的分布式锁,同样 Redisson 也是 Redis 官方推荐分布式锁实现方案,封装好了让用户实现分布式锁更加的方便与简洁。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

二、分布式锁的特性

  • 互斥性

    任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。

  • 同一性

    锁只能被持有该锁的客户端删除,不能由其它客户端删除。

  • 可重入性

    持有某个锁的客户端可继续对该锁加锁,实现锁的续租。

  • 容错性

    锁失效后(超过生命周期)自动释放锁(key失效),其他客户端可以继续获得该锁,防止死锁。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

三、Redisson 分布式锁原理

下面我们从加锁机制、锁互斥机制、锁续期机制、可重入加锁机制、锁释放机制等五个方面对 Redisson 分布式锁原理进行分析。

3.0 整体分析

注:redisson 版本 3.24.4-SNAPSHOT

public class RedissonLockTest {
    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer()
            .setPassword("admin")
            .setAddress("redis://127.0.0.1:6379");

        RedissonClient redisson = Redisson.create(config);
        RLock lock = redisson.getLock("myLock");

        try {
            lock.lock();
            // 业务逻辑
        } finally {
            lock.unlock();
        }
    }
}

初始化 RedissonLock

d4a9b7ee4621c5ea41943645feaf589f.png
/**
 * 加锁方法
 *
 * @param leaseTime 加锁到期时间(-1:使用默认值 30 秒)
 * @param unit 时间单位
 * @param interruptibly 是否可被中断标识
 * @throws InterruptedException
 */
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    // 获取当前线程ID
    long threadId = Thread.currentThread().getId();
    // 尝试获取锁(重点)
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
    // lock acquired
    // 成功获取锁, 过期时间为空。
    if (ttl == null) {
        return;
    }

    // 订阅分布式锁, 解锁时进行通知。
    CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
    pubSub.timeout(future);
    RedissonLockEntry entry;
    if (interruptibly) {
        entry = commandExecutor.getInterrupted(future);
    } else {
        entry = commandExecutor.get(future);
    }

    try {
        while (true) {
            // 再次尝试获取锁
            ttl = tryAcquire(-1, leaseTime, unit, threadId);
            // lock acquired
            // 成功获取锁, 过期时间为空, 成功返回。
            if (ttl == null) {
                break;
            }

            // waiting for message
            // 锁过期时间如果大于零, 则进行带过期时间的阻塞获取。
            if (ttl >= 0) {
                try {
                    // 获取不到锁会在这里进行阻塞, Semaphore, 解锁时释放信号量通知。
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    if (interruptibly) {
                        throw e;
                    }
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                }
            } else {
                // 锁过期时间小于零, 则死等, 区分可中断及不可中断。
                if (interruptibly) {
                    entry.getLatch().acquire();
                } else {
                    entry.getLatch().acquireUninterruptibly();
                }
            }
        }
    } finally {
        // 取消订阅
        unsubscribe(entry, threadId);
    }
}
cbc6d4263db8b8c4fef11a9bde81e366.png

当锁超时时间为 -1 时,而且获取锁成功时,会启动看门狗定时任务自动续锁:

d17dd7f116dc5a5658e8119ef3a4081b.png dc2162a9ecc52b82c2f8c7f55afc3da8.png

每次续锁都要判断锁是否已经被释放,如果锁续期成功,自己再次调度自己,持续续锁操作。

为了保证原子性,用 lua 实现的原子性加锁操作,见 3.1 加锁机制。

3.1 加锁机制

加锁机制的核心就是这段,将 Lua 脚本被 Redisoon 包装最后通过 Netty 进行传输。

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    /**
     * // 1
     * KEYS[1] 代表上面的 myLock
     * 判断 KEYS[1] 是否存在, 存在返回 1, 不存在返回 0。
     * 当 KEYS[1] == 0 时代表当前没有锁
     * // 2
     * 查找 KEYS[1] 中 key ARGV[2] 是否存在, 存在回返回 1
     * // 3
     * 使用 hincrby 命令发现 KEYS[1] 不存在并新建一个 hash
     * ARGV[2] 就作为 hash 的第一个key, val 为 1
     * 相当于执行了 hincrby myLock 91089b45... 1
     * // 4
     * 设置 KEYS[1] 过期时间, 单位毫秒
     * // 5
     * 返回 KEYS[1] 过期时间, 单位毫秒
     */
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
            "if ((redis.call('exists', KEYS[1]) == 0) " + // 1
                        "or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " + // 2
                    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // 3
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " + // 4
                    "return nil; " +
                "end; " +
                "return redis.call('pttl', KEYS[1]);", // 5
            Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

断点走一波就很清晰了:

3be3922f7d9d2aba9cb7f261abd864da.png

KEYS[1]) :加锁的key ARGV[1] :key的生存时间,默认为30秒 ARGV[2] :加锁的客户端ID (UUID.randomUUID()) + “:” + threadId)

84a4b1622136815dd3b242e50a5a4c46.png

上面这一段加锁的 lua 脚本的作用是:第一段 if 判断语句,就是用 exists myLock 命令判断一下,如果你要加锁的那个锁 key 不存在的话(第一次加锁)或者该 key 的 field 存在(可重入锁),你就进行加锁。如何加锁呢?使用 hincrby 命令设置一个 hash 结构,类似于在 Redis 中使用下面的操作:

cfd7047ed78ff9aa8246070feb315b53.png

整个 Lua 脚本加锁的流程画图如下:

5c3adef027d8f520b0bc7f2d5b4ad7cf.png

可以看出,最新版本的逻辑比之前的版本更简单清晰了。

3.2 锁互斥机制

此时,如果客户端 2 来尝试加锁,会如何呢?首先,第一个 if 判断会执行 exists myLock,发现 myLock 这个锁 key 已经存在了。接着第二个 if 判断,判断一下,myLock 锁 key 的 hash 数据结构中,是否包含客户端 2 的 ID,这里明显不是,因为那里包含的是客户端 1 的 ID。所以,客户端 2 会执行:

return redis.call('pttl', KEYS[1]);

返回的一个数字,这个数字代表了 myLock 这个锁 key 的剩余生存时间。

锁互斥机制主流程其实在 3.0 整体分析 里有讲,具体可以看这个 org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean) 方法。

3.3 锁续期机制

客户端 1 加锁的锁 key 默认生存时间是 30 秒,如果超过了 30 秒,客户端 1 还想一直持有这把锁,怎么办呢?

Redisson 提供了一个续期机制, 只要客户端 1 一旦加锁成功,就会启动一个 Watch Dog。

3.4 可重入加锁机制

ce02ffdb6275943cbd40af0dcc694be0.png e56ba2cf168e245b49aa3664dce3dda4.png

Watch Dog 机制其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个 RedissonBaseLock.EXPIRATION_RENEWAL_MAP 里面,然后每隔 10 秒 (internalLockLeaseTime / 3) 检查一下,如果客户端 1 还持有锁 key(判断客户端是否还持有 key,其实就是遍历 EXPIRATION_RENEWAL_MAP 里面线程 id 然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间),那么就会不断的延长锁 key 的生存时间。

注:

  1. 如果服务宕机了,Watch Dog 机制线程也就没有了,此时就不会延长 key 的过期时间,到了 30s 之后就会自动过期了,其他线程就可以获取到锁。

  2. 如果调用带过期时间的 lock 方法,则不会启动看门狗任务去自动续期。

3.5 锁释放机制

5b91fe76de0c72ee9b657e45005ea513.png
// 判断 KEYS[1] 中是否存在 ARGV[3]
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
    "return nil;" +
"end; " +
// 将 KEYS[1] 中 ARGV[3] Val - 1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 如果返回大于0 证明是一把重入锁
"if (counter > 0) then " +
    // 重置过期时间
    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
    "return 0; " +
"else " +
    // 删除 KEYS[1]
    "redis.call('del', KEYS[1]); " +
    // 通知阻塞等待线程或进程资源可用
    "redis.call('publish', KEYS[2], ARGV[1]); " +
    "return 1; " +
"end; " +
"return nil;"

KEYS[1]: myLock KEYS[2]: redisson_lock_channel:{myLock} ARGV[1]: 0 ARGV[2]: 30000 (过期时间) ARGV[3]: 66a84a47-3960-4f3e-8ed7-ea2c1061e4cf:1 (Hash 中的锁 field)

同理,锁释放断点走一波:

e77bff0f02e6896659cd7282c23682e6.png

锁释放机制小结一下:

  • 删除锁(这里注意可重入锁)

  • 广播释放锁的消息,通知阻塞等待的进程(向通道名为 redisson_lock__channel:{myLock} publish 一条 UNLOCK_MESSAGE 信息)

  • 取消 Watch Dog 机制,即将 RedissonLock.EXPIRATION_RENEWAL_MAP 里面的线程 id 删除,并且 cancel 掉 Netty 的那个定时任务线程。

四、主从 Redis 架构中分布式锁存在的问题

  • 线程A从主redis中请求一个分布式锁,获取锁成功;

  • 从redis准备从主redis同步锁相关信息时,主redis突然发生宕机,锁丢失了;

  • 触发从redis升级为新的主redis;

  • 线程B从继任主redis的从redis上申请一个分布式锁,此时也能获取锁成功;

  • 导致,同一个分布式锁,被两个客户端同时获取,没有保证独占使用特性;

为了解决这个问题,redis引入了红锁的概念。

需要准备多台redis实例,这些redis实例指的是完全互相独立的Redis节点,这些节点之间既没有主从,也没有集群关系。客户端申请分布式锁的时候,需要向所有的redis实例发出申请,只有超过半数的redis实例报告获取锁成功,才能算真正获取到锁。跟大多数保证一致性的算法类似,就是多数原理。

public static void main(String[] args) {
    String lockKey = "myLock";
    Config config = new Config();
    config.useSingleServer().setPassword("123456").setAddress("redis://127.0.0.1:6379");
    Config config2 = new Config();
    config.useSingleServer().setPassword("123456").setAddress("redis://127.0.0.1:6380");
    Config config3 = new Config();
    config.useSingleServer().setPassword("123456").setAddress("redis://127.0.0.1:6381");

    RLock lock = Redisson.create(config).getLock(lockKey);
    RLock lock2 = Redisson.create(config2).getLock(lockKey);
    RLock lock3 = Redisson.create(config3).getLock(lockKey);

    RedissonRedLock redLock = new RedissonRedLock(lock, lock2, lock3);

    try {
        redLock.lock();
    } finally {
        redLock.unlock();
    }
}

当然, 对于 Redlock 算法不是没有质疑声,两位大神前几年吵的沸沸腾腾,大家感兴趣的可以去 Redis 官网查看Martin Kleppmann 与 Redis 作者Antirez 的辩论。

额,想收一收了,再讲下去感觉要绕不开分布式经典问题 CAP了。

五、分布式锁选型

鱼和熊掌不可兼得,如果你想强一致性的话可以选择 ZK 的分布式锁,但 ZK 的话性能就会有一定的下降,如果项目没有用到 ZK 的话,那就选择 Redis 的分布式锁吧,比较你为了那极小的概率而丢去性能以及引入一个组件很不划算,如果无法忍受 Redis 的红锁缺陷,那自己在业务中自己保证吧。

下面是常见的几种分布式锁选型对比:

8a2240acfd0cb285f58eda7350f5865b.png

欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

d2f0704db5aab554cc4f670e40136c7e.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

3f36ed4b1466489c9c7a0e369a074d91.png

9972180740fbe61c4f413abd070d487d.png3ae458861bb10fbef55daa91fc5f6f94.png1407faef6fbf8d6b09a3e75dc42382e4.pngaef48810ff7138592dbc68a8e7d2fcda.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
<think>嗯,用户问的是Redission实现分布式锁原理,这个我得好好理清楚。首先,Redission是一个基于Redis的Java客户端,它提供了很多分布式相关的功能,其中分布式锁是比较常用的部分。那我得先回忆一下分布式锁的基本概念,为什么需要它,以及常见的实现方式。 分布式锁主要是为了解决在分布式系统中多个节点竞争同一资源时的同步问题。比如,在微服务架构中,多个服务实例可能需要同时修改某个共享资源,这时候就需要一种机制来确保同一时间只有一个实例能进行操作。传统的单机锁无法跨JVM工作,所以需要基于外部存储系统实现,比如Redis、Zookeeper等。 Redission实现分布式锁,核心应该还是基于Redis的特性。Redis的单线程模型和原子操作非常适合用来做分布式锁。不过,具体的实现细节可能需要更深入的分析。记得Redission分布式锁叫RedLock吗?或者是不是RedLock是另一种算法?可能记混了。需要确认一下。 对了,Redission分布式锁实现主要依赖Redis的SET命令,结合NX和PX参数,这样可以保证设置键的值和过期时间的原子性。NX表示只有当键不存在时才设置,PX设置过期时间,防止锁被长时间占用导致死锁。但Redission可能还做了一些优化和改进,比如自动续期、可重入等特性。 然后,锁的释放也需要保证原子性,通常是通过Lua脚本来执行,因为Lua脚本在Redis中是原子执行的。Redission应该会在释放锁的时候检查当前线程是否持有锁,只有持有锁的线程才能释放,避免误删其他线程的锁。 另外,Redission可能还实现了看门狗机制,也就是在获取锁之后,启动一个后台线程,定期检查锁的状态,并在锁快过期时自动续期。这样可以避免业务逻辑执行时间超过锁的过期时间导致的问题。不过,这个机制的具体实现细节需要再想想,比如续期的频率、如何停止续期等。 还有,Redission的锁是否可重入?应该是的,因为它内部可能维护了一个计数器,记录同一个线程获取锁的次数,每次释放锁时计数器减一,直到计数器为零时才真正释放锁。这样同一个线程可以多次获取锁,避免死锁。 不过,用户可能关心的是具体的实现步骤,比如加锁、解锁、续期的流程。需要把这些步骤详细地列出来,并解释每一步的作用。比如,加锁时使用Lua脚本设置键,同时存储线程标识和计数器;解锁时同样用Lua脚本来判断线程标识并减少计数器,如果计数器为零则删除键。 可能还需要提到Redission如何处理节点故障的情况,比如Redis集群中的主从切换是否会导致锁失效,这时候Redission是否有相应的容错机制,比如RedLock算法,但RedLock本身也存在一些争议,需要说明其优缺点。 另外,需要比较一下Redission分布式锁和其他实现方式,比如基于Zookeeper的临时顺序节点,各自的优缺点。比如Redis实现的锁性能更高,但可能在极端情况下(如主从切换)存在安全性问题;而Zookeeper的锁更可靠,但性能稍差。 不过用户的问题主要集中在Redission的实现原理,所以应该重点放在Redission如何利用Redis的特性,以及它的机制设计上。比如如何保证原子性、如何处理锁过期、如何实现可重入和自动续期,以及如何确保释放锁的安全性。 可能还需要提到Redission的一些高级功能,比如公平锁、联锁(MultiLock)、红锁(RedLock)等,但用户问的是基本实现原理,所以这些高级特性可以简要带过,或者不提及,视情况而定。 总结一下,回答的结构应该是先介绍分布式锁的必要性,然后详细说明Redission的实现步骤,包括加锁、续期、解锁的过程,强调其如何利用Redis的原子操作和Lua脚本,以及看门狗机制和可重入设计。最后可以简要提及优缺点或与其他方案的对比,但以用户的问题为导向,重点在原理部分。</think>Redission实现分布式锁的核心原理基于Redis的原子性操作和Lua脚本,以下是分步解析: --- ### **1. 加锁过程** - **核心命令**:使用`SET key value NX PX <expireTime>`实现原子性加锁: - `NX`:仅当key不存在时才设置值(互斥性) - `PX`:设置锁的过期时间(避免死锁) - **唯一标识**:锁的`value`包含客户端唯一标识(如UUID+线程ID),确保只有加锁者能解锁。 - **可重入性**:通过计数器记录锁的重入次数(Hash结构存储`<客户端ID, 重入次数>`)。 ```lua -- 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 1 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 1 end return 0 ``` --- ### **2. 锁续期(看门狗机制)** - **后台线程**:加锁成功后启动守护线程(Watchdog),定期(默认10秒)检查锁是否仍持有。 - **自动续期**:若锁未被释放,重置过期时间为初始值(默认30秒),避免业务未完成锁已过期。 --- ### **3. 解锁过程** - **原子性释放**:通过Lua脚本验证客户端身份并减少重入次数,计数器归零时删除锁。 ```lua -- Lua脚本实现原子性解锁 if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil end local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1) if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]) return 0 else redis.call('del', KEYS[1]) return 1 end ``` --- ### **4. 容错机制** - **Redis集群支持**:通过`MultiLock`关联多个主节点,半数以上节点加锁成功视为成功(类似RedLock算法)。 - **失败重试**:提供`tryLock()`支持超时等待和重试机制。 --- ### **优缺点分析** | **优点** | **缺点** | |-----------------------------------|-----------------------------------| | 高可用(Redis集群) | 依赖Redis持久化,极端情况可能丢锁 | | 自动续期避免业务中断 | 网络延迟可能导致锁竞争 | | 可重入设计适配复杂业务 | 需要合理设置超时时间 | --- ### **对比其他方案** - **ZooKeeper锁**:基于临时顺序节点,可靠性更高,但性能低于Redis。 - **数据库锁**:通过唯一索引实现,简单但性能差,不适合高并发。 --- 通过上述机制,Redission分布式场景下实现了高效、可靠的锁管理,平衡了性能与安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值