Redis可重入锁的实现设计

1 可重入锁的需求

单纯的 Redis 分布式锁仍然有些场景不满⾜的,如⼀个⽅法获取到锁后,可能在⽅法内继续调这个⽅法,就获取不到锁了。这时就要把锁改进成可重⼊锁。

重⼊锁,是以线程为单位,当⼀个线程获取对象锁后,该线程还可再次获取该对象上的锁,⽽其它线程不可以。可重⼊锁是为防⽌死锁。

2 实现原理

为每个锁关联⼀个请求计数器和⼀个占有它的线程。当计数为0,认为锁未被占有。

线程请求⼀个未被占有的锁时,JVM将记录锁的占有者,并将请求计数器置1。

  • 若同⼀线程再请求该锁,计数将递增
  • 每次占⽤线程退出同步块,计数器值将递减。直到计数器为0, 锁被释放

⽗类和⼦类的锁的重⼊:⼦类重写⽗类的 synchonized ⽅法,然后调⽤⽗类中的⽅法,此时若没有重⼊锁,这段代码将死锁。

3 案例

3.1 不可重⼊锁

实现

使用不可重入锁

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

3.2 可重入锁

实现

锁使用

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

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

4 Redis可重入锁设计

假设锁的K=“lock”,hashKey是当前线程的id:“threadId”,TTL假设20。

4.1 获取锁

① 判断lock是否存在(EXISTS lock)
  • 不存在,则自己获取锁,记录重入层数为1
  • 存在,说明有人获取锁了,转到②
② 判断是不是自己的锁

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

  • 不存在,说明锁已有&&不是自己获取的,锁获取失败
  • 存在,说明自己获取的锁,重入次数+1(HINCRBY lock threadId 1) ,更新TTL
释放锁

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

  • 不存在,说明锁已失效
  • 存在,说明锁还在,重入次数减1: HINCRBY lock threadId -1 ,
    • 获取新的重入次数,判断重入次数是否为0,为0说明锁全部释放,删除key: DEL lock

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

5 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;
-- 锁已存在
-- 判断threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then
    -- 如果是自己,则重入次数+1
    redis.call('hincrby', key, threadId, '1');
  	-- 设置有效期
    redis.call('expire', key, releaseTime);
  	-- 返回结果
    return 1;
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

② 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;

6 项目集成

① 编写 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
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值