Redis 分布式锁详细分析
展开全部
锁的作用,我想大家都理解,就是让不同的线程或者进程可以安全地操作共享资源,而不会产生冲突。
比较熟悉的就是 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的分布式锁到底安全吗?
比较熟悉的就是 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的分布式锁到底安全吗?
光点科技
2023-08-15 广告
2023-08-15 广告
通常情况下,我们会按照结构模型把系统产生的数据分为三种类型:结构化数据、半结构化数据和非结构化数据。结构化数据,即行数据,是存储在数据库里,可以用二维表结构来逻辑表达实现的数据。最常见的就是数字数据和文本数据,它们可以某种标准格式存在于文件...
点击进入详情页
本回答由光点科技提供
推荐律师服务:
若未解决您的问题,请您详细描述您的问题,通过百度律临进行免费专业咨询