一. 分布式锁基础
在分布式系统中,当多个客户端(应用实例)需要访问同一资源时,可以使用分布式锁来确保同一时刻只有一个客户端能访问该资源。Redis作为高性能的内存数据库,提供了基于键值对的分布式锁实现,通常采用SETNX
命令(即SET if Not eXists
)来设置锁。
Redis的分布式锁大致实现流程如下:
- 客户端请求获取锁:通过
SETNX
命令设置一个锁键(如lock:<resource>
)。 - 锁被设置成功时,客户端可以访问资源;否则,它会等待或返回获取失败的结果。
- 客户端在完成工作后,释放锁:通过
DEL
命令删除锁键。
二. 可重入锁
在一个线程或进程中,如果该线程/进程已经获取了锁,那么它可以重复获取该锁,而不会发生死锁问题。也就是说,同一线程/进程可以多次获取锁,而每次释放锁时都需要释放一次,直到所有的锁请求都被释放。
对于分布式系统中的可重入锁,它确保:
- 当一个客户端(通常是线程或进程)已经持有锁时,它可以继续请求该锁而不会被阻塞或进入死锁。
- 锁的计数是线程级别的,客户端每次请求锁时都需要将锁的计数增加,释放锁时则减少计数,直到计数为零时才完全释放锁。
三. Redis实现可重入锁的步骤
1. 使用SET
命令(或SETNX
命令)来获取锁
- 锁是由一个特定的Redis键(例如,
lock:<resource>
)表示的。 - 锁的值通常是一个标识符,例如,客户端的
UUID
,或者一个线程标识符。这个值用于确保同一个客户端可以重复获取锁。
2. 记录锁的持有者
- 当客户端请求锁时,它不仅设置一个标识该锁的键(如
lock:<resource>
),而且还设置该锁的值(如UUID
)。这样,如果该锁已经存在且值不是当前客户端的标识符,则客户端无法获得锁。
3. 锁的可重入性
- 在可重入锁中,客户端获取锁时,如果该客户端的标识符(如
UUID
)已经在锁值中,则客户端可以继续获取锁,并且需要将锁的"持有次数"增加。 - 可以通过键值对的计数来实现这一点。每次客户端请求锁时,Redis可以增加锁的计数。当客户端释放锁时,Redis会减少锁的计数。
4. 锁的过期时间
- 为了防止死锁,Redis的锁通常会设置一个过期时间,通常是
SET
命令的EX
选项(设置过期时间)。这样,如果客户端在持有锁的过程中发生故障,锁会在一定时间后自动释放。 - 可重入锁的一个实现方式是,客户端在每次获取锁时刷新过期时间,确保锁在可重入期间不会过期。
5. 释放锁
- 当客户端完成工作后,它会释放锁。每次释放锁时,它会检查当前的锁计数:
- 如果锁计数大于1,表示该客户端还需要继续持有锁,于是减少计数。
- 如果锁计数为1,则客户端完全释放锁,通过删除锁键(如
DEL
)来释放该资源。
四 LUA 脚本实现简单重入锁
获取锁的 Lua 脚本:
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; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
释放锁的 Lua 脚本:
Lua
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;