Redis如何实现可重入锁的设计

55次阅读
没有评论

共计 3553 个字符,预计需要花费 9 分钟才能阅读完成。

自动写代码机器人,免费开通

这篇文章主要介绍 Redis 如何实现可重入锁的设计,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!

但是仍然有些场景是不满⾜的,例如⼀ 个⽅法获取到锁之后,可能在⽅法内调这个⽅法此时就获取不到锁了。这个时候我们就需要把锁改进成可 重⼊锁了。重⼊锁,指的是以线程为单位,当⼀个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,⽽其 他的线程是不可以的。可重⼊锁的意义在于防⽌死锁。实现原理是通过为每个锁关联⼀个请求计数器和⼀个占有它的线程。当计数为 0 时,认为锁是未被占有 的;线程请求⼀个未被占有的锁时,JVM 将记录锁的占有者,并且将请求计数器置为 1。如果同⼀个线程再次请求这个锁,计数将递增;每次占⽤线程退出同步块,计数器值将递减。直到计数器 为 0, 锁被释放。关于⽗类和⼦类的锁的重⼊:⼦类覆写了⽗类的 synchonized ⽅法,然后调⽤⽗类中的⽅法,此时如果没有重⼊的锁,那么这段代码将产⽣死锁。

代码演示

不可重⼊

不可重⼊锁
Redis 如何实现可重入锁的设计

使用不可重入锁 Redis 如何实现可重入锁的设计
当前线程执⾏ call() ⽅法⾸先获取 lock,接下来执⾏ inc() ⽅法就⽆法执⾏ inc() 中的逻辑,必须先释放锁。该例很好的说明了不可重⼊锁。

可重入锁

锁实现
Redis 如何实现可重入锁的设计

锁使用 Redis 如何实现可重入锁的设计

可重⼊意味着线程可进⼊它已经拥有的锁的同步代码块。

设计两个线程调⽤ call() ⽅法,第⼀个线程调⽤ call() ⽅法获取锁,进⼊ lock() ⽅法,由于初始 lockedBy 是 null,所以不会进⼊ while ⽽挂起当前线程,⽽是增量 lockedCount 并记录 lockBy 为第 ⼀个线程。

接着第⼀个线程进⼊ inc() ⽅法,由于同⼀进程,所以不会进⼊ while ⽽挂起,接着增量 lockedCount,当第⼆个线程尝试 lock,由于 isLocked=true, 所以他不会获取该锁,直到第⼀个线程调⽤两次 unlock() 将 lockCount 递减为 0,才将标记为 isLocked 设置为 false。

设计思路

假设锁的 key 为“lock”,hashKey 是当前线程的 id:“threadId”,锁自动释放时间假设为 20。

获取锁

判断 lock 是否存在 EXISTS lock

不存在,则自己获取锁,记录重入层数为 1.

存在,说明有人获取锁了,继续判断是不是自己的锁,即判断当前线程 id 作为 hashKey 是否存在:HEXISTS lock threadId

不存在,说明锁已经有了,且不是自己获取的,锁获取失败.

存在,说明是自己获取的锁,重入次数 +1:HINCRBY lock threadId 1,最后更新锁自动释放时间,EXPIRE lock 20
Redis 如何实现可重入锁的设计

释放锁

判断当前线程 id 作为 hashKey 是否存在:HEXISTS lock threadId

不存在,说明锁已失效

存在,说明锁还在,重入次数减 1:HINCRBY lock threadId -1,

获取新的重入次数,判断重入次数是否为 0,为 0 说明锁全部释放,删除 key:DEL lock

因此,存储在锁中的信息就必须包含:key、线程标识、重入次数。不能再使用简单的 key-value 结构,这里推荐使用 hash 结构。而且要让所有指令都在同一个线程中操作,那么使用 lua 脚本。

lua 脚本

lock.lua

local key = KEYS[1]; --  第 1 个参数, 锁的 keylocal threadId = ARGV[1]; --  第 2 个参数, 线程唯一标识 local releaseTime = ARGV[2]; --  第 3 个参数, 锁的自动释放时间 if(redis.call( exists , key) == 0) then --  判断锁是否已存在
 redis.call( hset , key, threadId,  1  --  不存在,  则获取锁
 redis.call(expire , key, releaseTime); --  设置有效期
 return 1; --  返回结果 end;if(redis.call( hexists , key, threadId) == 1) then --  锁已经存在,判断 threadId 是否是自己  
 redis.call( hincrby , key, threadId,  1  --  如果是自己,则重入次数 +1
 redis.call(expire , key, releaseTime); --  设置有效期
 return 1; --  返回结果 end;return 0; --  代码走到这里, 说明获取锁的不是自己,获取锁失败 

unlock.lua

--  锁的  keylocal key = KEYS[1];--  线程唯一标识 local threadId = ARGV[1];--  判断当前锁是否还是被自己持有 if (redis.call( hexists , key, threadId) == 0) then--  如果已经不是自己,则直接返回
 return nil;end;--  是自己的锁,则重入次数减一 local count = redis.call(hincrby , key, threadId, -1);--  判断重入次数是否已为 0if (count == 0) then--  等于  0,说明可以释放锁,直接删除
 redis.call(del , key);
 return nil;end;

在项目中集成

编写 RedisLock 类

@Getter@Setterpublic class RedisLock {
 private RedisTemplate redisTemplate;
 private DefaultRedisScript Long  lockScript;
 private DefaultRedisScript Object  unlockScript;
 public RedisLock(RedisTemplate redisTemplate) {
 this.redisTemplate = redisTemplate;
 //  加载释放锁的脚本
 this.lockScript = new DefaultRedisScript ();
 this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource( lock.lua)));
 this.lockScript.setResultType(Long.class);
 //  加载释放锁的脚本
 this.unlockScript = new DefaultRedisScript ();
 this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource( unlock.lua)));
 }
 /**
 *  获取锁
 * @param lockName  锁名称
 * @param releaseTime  超时时间 (单位: 秒)
 * @return key  解锁标识
 */
 public String tryLock(String lockName, long releaseTime) {
 //  存入的线程信息的前缀,防止与其它 JVM 中线程信息冲突
 String key = UUID.randomUUID().toString();
 //  执行脚本
 Long result = (Long)redisTemplate.execute(
 lockScript,
 Collections.singletonList(lockName),
 key + Thread.currentThread().getId(), releaseTime);
 //  判断结果
 if(result != null   result.intValue() == 1) {
 return key;
 }else {
 return null;
 }
 }
 /**
 *  释放锁
 * @param lockName  锁名称
 * @param key  解锁标识
 */
 public void unlock(String lockName, String key) {
 //  执行脚本
 redisTemplate.execute(
 unlockScript,
 Collections.singletonList(lockName),
 key + Thread.currentThread().getId(), null);
 }}

以上是“Redis 如何实现可重入锁的设计”这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注丸趣 TV 行业资讯频道!

向 AI 问一下细节

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-12-04发表,共计3553字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)