-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Redisson 分布式锁实现分析 #4
Comments
hi,您好,看了分析有一点疑惑: 在获取锁阶段,如果多个线程或者多个客户端同时执行redis.call('exists', KEYS[1]) == 0),恰好这个key也不存在,那么是不是就导致了 一个锁被多个线程或多个客户端的情况? |
@lywhlao 不会的,redis中setNX同一时间点只能有一个线程执行该命令,不会出现多个线程同时执行这个命令的情况 |
Hi, @yangdailin 你好。 `
`
请教一下这是为什么呢? |
这个貌似是你的redis不支持eval,你看看你的redis版本是不是过低了 |
有道理,好像的确有点低了,我换个高版本的试试 |
有一个地方没看懂 |
Redisson 分布式锁实现分析
图片来源:mrniko/Redisson
Why 分布式锁
java.util.concurrent.locks
中包含了 JDK 提供的在多线程情况下对共享资源的访问控制的一系列工具,它们可以帮助我们解决进程内多线程并发时的数据一致性问题。但是在分布式系统中,JDK 原生的并发锁工具在一些场景就无法满足我们的要求了,这就是为什么要使用分布式锁。我总结了一句话,分布式锁是用于解决分布式系统中操作共享资源时的数据一致性问题。
设计分布式锁要注意的问题
互斥
分布式系统中运行着多个节点,必须确保在同一时刻只能有一个节点的一个线程获得锁,这是最基本的一点。
死锁
分布式系统中,可能产生死锁的情况要相对复杂一些。分布式系统是处在复杂网络环境中的,当一个节点获取到锁,如果它在释放锁之前挂掉了,或者因网络故障无法执行释放锁的命令,都会导致其他节点无法申请到锁。
因此分布式锁有必要设置时效,确保在未来的一定时间内,无论获得锁的节点发生了什么问题,最终锁都能被释放掉。
性能
对于访问量大的共享资源,如果针对其获取锁时造成长时间的等待,导致大量节点阻塞,是绝对不能接受的。
所以设计分布式锁时要能够掌握锁持有者的动态,若判断锁持有者处于不活动状态,要能够强制释放其持有的锁。
此外,排队等待锁的节点如果不知道锁何时会被释放,则只能隔一段时间尝试获取一次锁,这样无法保证资源的高效利用,因此当锁释放时,要能够通知等待队列,使一个等待节点能够立刻获得锁。
重入
考虑到一些应用场景和资源的高效利用,锁要设计成可重入的,就像 JDK 中的 ReentrantLock 一样,同一个线程可以重复拿到同一个资源的锁。
RedissonLock 实现解读
本文中 Redisson 的代码版本为 2.2.17-SNAPSHOT。
这里以
lock()
方法为例,其他一系列方法与其核心实现基本一致。先来看 lock() 的基本用法
getLock()
方法取得一个 RLock 实例。lock()
方法尝试获取锁,如果成功获得锁,则继续往下执行,否则等待锁被释放,然后再继续尝试获取锁,直到成功获得锁。unlock()
方法释放获得的锁,并通知等待的节点锁已释放。下面来看看 RedissonLock 的具体实现
org.redisson.Redisson#getLock()
这里的 RLock 是继承自 java.util.concurrent.locks.Lock 的一个 interface,
getLock
返回的实际上是其实现类 RedissonLock 的实例。来看看构造 RedissonLock 的参数
eval
命令来执行 Lua 脚本,所以要求 Redis 的版本必须为 2.6 或以上,否则你可能要自己来实现 CommandExecutor。关于 Redisson 的 CommandExecutor 以后会专门解读,所以本次就不多说了。"foobar"
,具体业务中通常可能使用共享资源的唯一标识作为该名称。org.redisson.RedissonLock#lock()
此处略过前面几个方法的层层调用,直接看最核心部分的方法
lockInterruptibly()
,该方法在 RLock 中声明,支持对获取锁的线程进行中断操作。在直接使用lock()
方法获取锁时,最后实际执行的是lockInterruptibly(-1, null)
。null
则说明没有已存在的锁并成功获得锁。release()
方法会被调用,此时被信号量阻塞的等待队列中的一个线程就可以继续尝试获取锁了。下面着重看看
tryAcquire()
方法的实现,EVAL
命令执行上面的 Lua 脚本来完成获取锁的操作:exists
命令发现当前 key 不存在,即锁没被占用,则执行hset
写入 Hash 类型数据 key:全局锁名称(例如共享资源ID), field:锁实例名称(Redisson客户端ID:线程ID), value:1,并执行pexpire
对该 key 设置失效时间,返回空值nil
,至此获取锁成功。hexists
命令发现 Redis 中已经存在当前 key 和 field 的 Hash 数据,说明当前线程之前已经获取到锁,因为这里的锁是可重入的,则执行hincrby
对当前 key field 的值加一,并重新设置失效时间,返回空值,至此重入获取锁成功。pttl
获取锁的剩余存活时间并返回,至此获取锁失败。以上就是对
lock()
的解读,不过在实际业务中我们可能还会经常使用tryLock()
,虽然两者有一定差别,但核心部分的实现都是相同的,另外还有其他一些方法可以支持更多自定义参数,本文中就不一一详述了。org.redisson.RedissonLock#unlock()
最后来看锁的释放,
EVAL
命令执行 Lua 脚本来释放锁:publish
命令发布释放锁消息并返回1
。nil
。hincrby
对锁的值减一。0
;如果刚才释放的已经是最后一把锁,则执行del
命令删除锁的 key,并发布锁释放消息,返回1
。nil
的情况(即第2中情况),因为自己不是锁的持有者,不允许释放别人的锁,故抛出异常。1
的情况,该锁的所有实例都已全部释放,所以不需要再刷新锁的失效时间。总结
写了这么多,其实最主要的就是上面的两段 Lua 脚本,基于 Redis 的分布式锁的设计完全体现在其中,看完这两段脚本,再回顾一下前面的 设计分布式锁要注意的问题 就豁然开朗了。
本文同时发布于我的微信订阅号
The text was updated successfully, but these errors were encountered: