在复杂系统中,当存在递归调用或涉及多个方法间调用而多个方法需要同一个锁时,不可重入锁会出现死锁,比如:

public void A(long id) {
	lock(id);
	// 业务。。。
	B(id);
	// 业务。。。
	unlock(id);
}
 
public void B(long id) {
	lock(id);
	// 业务。。。
	unlock(id);
}

不可重入锁在A中已经获取了锁id,因此B无法获取锁id,从而导致死锁,所以此时需要可重入锁。

原理


可重入锁包括锁持有者重入次数两个关键信息。
锁持有者:记录当前是哪一个线程拿到了锁(UUID + 线程ID)。
重入次数:当前线程获取获取同一把锁的次数。
这里需要存储key、owner、count三个值。因此String不够用了,需要用到|Hash,结构为key:[owner:count]。

  • 获取锁时:
    先用exists key检查key是否存在
    • 若不存在,将count设为1,
    • 若存在,再检查hash中是否存在当前owner
      • 若存在,重入次数+1,并重置锁的过期时间
      • 若不存在,直接返回
  • 释放锁时:
    先用hexists key field检查key和owner当前线程的锁是否存在
    • 若存在,重入次数-1,并判断重入次数是否为0
      • 若为0,删除key,释放锁
      • 若不为0,重置锁的过期时间
    • 若不存在,直接返回

Lua实现


因为涉及多个获取判断操作和写操作,需要保证原子性,因此使用Lua编写Redis脚本实现:

获取锁

-- 可重入锁的Lua脚本  
local key = KEYS[1] -- 锁的key  
local expireTime = ARGV[1] -- 锁的自动过期时间  
local lockId = ARGV[2] -- 锁的Id,每个线程唯一  
-- 检查锁是否存在  
if(redis.call('exists', key) == 0) then  
    -- 锁不存在,直接加锁  
    redis.call('hset', key, lockId, 1) -- 重入次数初始化为1  
    redis.call('expire', key, expireTime) -- 设置锁的过期时间  
    return 1 -- 加锁成功  
end  
-- 锁已存在,检查是否是同一个线程  
if(redis.call('hexists', key, lockId) == 1) then  
    -- 是同一个线程,重入次数加1  
    redis.call('hincrby', key, lockId, 1) -- 重入次数加1  
    redis.call('expire', key, expireTime) -- 重置锁的过期时间  
    return 1 -- 加锁成功  
end  
-- 锁已存在且不是同一个线程,无法加锁  
return 0 -- 加锁失败

释放锁

-- 可重入锁的Lua脚本  
local key = KEYS[1] -- 锁的key  
local expireTime = ARGV[1] -- 锁的自动过期时间  
local lockId = ARGV[2] -- 锁的Id,每个线程唯一  
-- 检查是否存在当前线程的锁  
if(redis.call('hexists', key, lockId) == 0) then  
    return nil -- 当前线程没有锁,直接返回  
end  
-- 当前线程的锁存在,重入次数减1  
local count =redis.call('hincrby', key, lockId, -1) -- 重入次数减1  
if(count > 0) then --重入次数大于0,重置锁的过期时间  
    redis.call('expire', key, expireTime) -- 重置锁的过期时间  
else -- 重入次数为0,释放锁  
    redis.call('hdel', key, lockId) -- 释放锁  
end  
return nil