Redis可重入锁的实现设计

举报
JavaEdge 发表于 2021/06/03 22:51:25 2021/06/03
【摘要】 但是仍然有些场景是不满⾜的,例如⼀ 个⽅法获取到锁之后,可能在⽅法内调这个⽅法此时就获取不到锁了。这个时候我们就需要把锁改进成可 重⼊锁了。 重⼊锁,指的是以线程为单位,当⼀个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,⽽其 他的线程是不可以的。可重⼊锁的意义在于防⽌死锁。 实现原理是通过为每个锁关联⼀个请求计数器和⼀个占有它的线程。当计数为 0 时,认为锁是...

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

代码演示

不可重⼊

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

可重入锁

  • 锁实现
  • 锁使用

可重⼊意味着线程可进⼊它已经拥有的锁的同步代码块。
设计两个线程调⽤ 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

释放锁

判断当前线程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个参数,锁的key
local 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; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

unlock.lua

-- 锁的 key
local 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);

-- 判断重入次数是否已为0
if (count == 0) then
-- 等于 0,说明可以释放锁,直接删除 redis.call('del', key); return nil;
end;

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在项目中集成

编写 RedisLock 类

@Getter
@Setter
public 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); }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

文章来源: javaedge.blog.csdn.net,作者:JavaEdge.,版权归原作者所有,如需转载,请联系作者。

原文链接:javaedge.blog.csdn.net/article/details/113242367

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。