在复杂系统中,当存在递归调用或涉及多个方法间调用而多个方法需要同一个锁时,不可重入锁会出现死锁,比如:
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,重置锁的过期时间
- 若不存在,直接返回
- 若存在,重入次数-1,并判断重入次数是否为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