原始redis分布式锁的问题
下面代码中,我们的trylock是通过setnx来实现,redis中存储的类型是string。
当第一个方法获取锁成功时,调用method2,由于还是在同一个线程内,方法1的锁还未释放,method2获取锁失败,无法重入。
Demo
@Resource
private RedissonClient redissonClient;
private RLock lock;
@BeforeEach
void setUp() {
lock = redissonClient.getLock("test");
}
@Test
void testReentry() {
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("获取锁失败......1");
return;
}
try {
log.info("获取锁成功......1");
method2();
log.info("开始执行业务......1");
} finally {
log.info("准备释放锁......1");
lock.unlock();
}
}
private void method2() {
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("获取锁失败......2");
return;
}
try {
log.info("获取锁成功......2");
log.info("开始执行业务......2");
} finally {
log.info("准备释放锁......2");
lock.unlock();
}
}
Redisson的改进
redisson改进了锁的实现,不再使用string类型存储,而是hash类型。
key存储锁的标识,value中field存储当前线程标识,value存储重入次数。
当method2获取锁失败时,会继续判断是否是同一个线程,如果是的话,重入次数加一,获取锁成功,这样就达到了锁的重入。
释放锁时,不会直接删除锁。
method2先让重入次数减一,然后判断重入次数是否为0。等到重入次数为0时,再执行删除锁的逻辑.。
当然,上述的逻辑肯定不能通过java代码来实现,因为不能每一步操作结束后是否会因为一些未知原因阻塞。故是通过lua脚本来保证对redis的操作是原子性的。下面是lua脚本的实例:
获取锁
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程的唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
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; --锁不是当前线程的,获取锁失败
释放锁
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程的唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被当前线程自己持有
if(redis.call('hexists', key, threadId) == 0) then
return nil; --如果不是自己,则直接返回
end;
-- 是自己的锁,则重入次数-1
local count = redis.call("hincrby", key, threadId, -1);
-- 判断重入次数是否已经为0
if(count > 0) then
-- 大于0说明锁还不能释放,重置有效期然后返回
redis.call('expire', key, releaseTime);
return nil;
else
-- 等于0说明需要释放锁,直接删除
redis.call('del', key);
return nil;
end;
我们翻看redisson源码,其实也是一样的。