Redis 分布式锁详细分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 分布式锁详细分析相关的知识,希望对你有一定的参考价值。

参考技术A 锁的作用,我想大家都理解,就是让不同的线程或者进程可以安全地操作共享资源,而不会产生冲突。
比较熟悉的就是 Synchronized 和 ReentrantLock 等,这些可以保证同一个 jvm 程序中,不同线程安全操作共享资源。
但是在分布式系统中,这种方式就失效了;由于分布式系统多线程、多进程并且分布在不同机器上,这将使单机并发控制锁策略失效,为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问。
比较常用的分布式锁有三种实现方式:

本篇文章主要讲解基于 Redis 分布式锁的实现。

分布式锁最主要的作用就是保证任意一个时刻,只有一个客户端能访问共享资源。

我们知道 redis 有 SET key value NX 命令,仅在不存在 key 的时候才能被执行成功,保证多个客户端只有一个能执行成功,相当于获取锁。
释放锁的时候,只需要删除 del key 这个 key 就行了。

上面的实现看似已经满足要求了,但是忘了考虑在分布式环境下,有以下问题:

最大的问题就是因为客户端或者网络问题,导致 redis 中的 key 没有删除,锁无法释放,因此其他客户端无法获取到锁。

针对上面的情况,使用了下面命令:

使用 PX 的命令,给 key 添加一个自动过期时间(30秒),保证即使因为意外情况,没有调用释放锁的方法,锁也会自动释放,其他客户端仍然可以获取到锁。

注意给这个 key 设置的值 my_random_value 是一个随机值,而且必须保证这个值在客户端必须是唯一的。这个值的作用是为了更加安全地释放锁。

这是为了避免删除其他客户端成功获取的锁。考虑下面情况:

因此这里使用一个 my_random_value 随机值,保证客户端只会释放自己获取的锁,即只删除自己设置的 key 。

这种实现方式,存在下面问题:

上面章节介绍了,简单实现存在的问题,下面来介绍一下 Redisson 实现又是怎么解决的这些问题的。

主要关注 tryAcquireOnceAsync 方法,有三个参数:

方法主要流程:

这个方法的流程与 tryLock(long waitTime, long leaseTime, TimeUnit unit) 方法基本相同。

这个方法与 tryAcquireOnceAsync 方法的区别,就是一个获取锁过期时间,一个是能否获取锁。即 获取锁过期时间 为 null 表示获取到锁,其他表示没有获取到锁。

获取锁最终都会调用这个方法,通过 lua 脚本与 redis 进行交互,来实现分布式锁。
首先分析,传给 lua 脚本的参数:

lua 脚本的流程:

为了实现无限制持有锁,那么就需要定时刷新锁的过期时间。

这个类最重要的是两个成员属性:

使用一个静态并发集合 EXPIRATION_RENEWAL_MAP 来存储所有锁对应的 ExpirationEntry ,当有新的 ExpirationEntry 并存入到 EXPIRATION_RENEWAL_MAP 集合中时,需要调用 renewExpiration 方法,来刷新过期时间。

创建一个超时任务 Timeout task ,超时时间是 internalLockLeaseTime / 3 , 过了这个时间,即调用 renewExpirationAsync(threadId) 方法,来刷新锁的过期时间。

判断如果是当前线程持有的锁,那么就重新设置过期时间,并返回 1 即 true 。否则返回 0 即 false 。

通过调用 unlockInnerAsync(threadId) 来删除 redis 中的 key 来释放锁。特别注意一点,当不是持有锁的线程释放锁时引起的失败,不需要调用 cancelExpirationRenewal 方法,取消定时,因为锁还是被其他线程持有。

传给这个 lua 脚本的值:

这个 lua 脚本的流程:

调用了 LockPubSub 的 subscribe 进行订阅。

这个方法的作用就是向 redis 发起订阅,但是对于同一个锁的同一个客户端(即 一个 jvm 系统) 只会发起一次订阅,同一个客户端的其他等待同一个锁的线程会记录在 RedissonLockEntry 中。

方法流程:

只有当 counter >= permits 的时候,回调 listener 才会运行,起到控制 listener 运行的效果。

释放一个控制量,让其中一个回调 listener 能够运行。

主要属性:

这个过程对应的 redis 中监控的命令日志:

因为看门狗的默认时间是 30 秒,而定时刷新程序的时间是看门狗时间的 1/3 即 10 秒钟,示例程序休眠了 15 秒,导致触发了刷新锁的过期时间操作。

注意 rLock.tryLock(10, TimeUnit.SECONDS); 时间要设置大一点,如果等待时间太短,小于获取锁 redis 命令的时间,那么就直接返回获取锁失败了。

分析源码我们了解 Redisson 模式的分布式,解决了锁过期时间和可重入的问题。但是针对 redis 本身可能存在的单点失败问题,其实是没有解决的。
基于这个问题, redis 作者提出了一种叫做 Redlock 算法, 但是这种算法本身也是有点问题的,想了解更多,请看 基于Redis的分布式锁到底安全吗?

以上是关于Redis 分布式锁详细分析的主要内容,如果未能解决你的问题,请参考以下文章

Redis实现分布式锁方法详细

基于redis的分布式锁的分析与实践

Redis 作者 Antirez 讲如何实现分布式锁?Redis 实现分布式锁天然的缺陷分析&Redis分布式锁的正确使用姿势!...

Redis 作者 Antirez 讲如何实现分布式锁?Redis 实现分布式锁天然的缺陷分析&Redis分布式锁的正确使用姿势!...

Redis 作者 Antirez 讲如何实现分布式锁?Redis 实现分布式锁天然的缺陷分析&Redis分布式锁的正确使用姿势!...

Redis 作者 Antirez 讲如何实现分布式锁?Redis 实现分布式锁天然的缺陷分析&Redis分布式锁的正确使用姿势!...