介绍基于Redis实现的分布式锁,包括Redis实现、Redission类实现及可重入锁的实现。文章侧重自己的理解,代码参考chatGPT。 (分享太密了,今天在肝一篇)
1、分布式锁的概念
锁是解决针对于对共享变量进行操作的安全性问题,分布式锁则是用于在分布式系统中控制。这里主要介绍基于Redis实现的分布式锁。
2、分布式锁的实现
2.1、Redis实现
2.1.1、SETNX
Redis是单线程处理命令,可以使用SETNX来实现分布式锁。
介绍:当键不存在时,设置键值,并返回 1;如果键已经存在,则返回 0。通过这一特性,可以确保同一时刻只有一个客户端能够成功获取锁,从而实现分布式锁的功能。
问题:锁如何设置过期时间
- 使用
SETNX先设置锁,然后再使用EXPIRE命令设置锁的过期时间,这两个操作并非原子操作。 - 借助Lua脚本,保证上述操作的原子性。
参考代码:
if redis.call("SETNX", KEYS[1], ARGV[1]) == 1 then
redis.call("PEXPIRE", KEYS[1], ARGV[2])
return 1
else
return 0
end
KEYS[1]:表示锁的键名(即lock_key)。ARGV[1]:表示锁的值(用于标识持有锁的客户端,可以是唯一的UUID或随机字符串)。ARGV[2]:表示锁的过期时间(单位为毫秒,保证锁在客户端故障时不会永久存在)。
2.1.2、使用 SET 命令结合 NX 和 PX 参数
从 Redis 2.6.12 版本开始,通过 SET key value NX PX expiration_time 来同时设置键值和过期时间。
NX:表示只有在键不存在时才会设置键值,相当于SETNX的功能。PX expiration_time:设置键的过期时间,单位为毫秒。
示例:
SET lock_key unique_value NX PX 10000
lock_key:表示锁的键。unique_value:表示用于标识锁持有者的唯一值(比如 UUID)。PX 10000:表示锁的过期时间为 10 秒(10000 毫秒)。
2.2、Redission实现
方法lock()、tryLock()、unlock()
示例:
import org.redisson.api.RedissonClient;
public class DistributedLockExample {
public static void main(String[] args) {
RedissonClient redisson = RedissonManager.getClient();
// 获取锁实例
RLock lock = redisson.getLock("myLock");
// 尝试加锁
try {
// 加锁(默认非阻塞,如果获取到锁则立即返回)
lock.lock();
// 或者可以设置锁的超时时间,比如10秒后自动释放锁
// lock.lock(10, TimeUnit.SECONDS);
// 业务逻辑
System.out.println("锁定中,执行任务...");
} finally {
// 释放锁
lock.unlock();
}
redisson.shutdown();
}
}
2.2.1、tryLock()和lock()
lock()(阻塞直到成功获取锁)
tryLock():
- 非阻塞:调用
tryLock()后,如果无法立即获取锁,它不会无限期地等待,而是返回false表示获取失败,从而避免线程长时间阻塞。 - 设置等待时间:可以指定一个最大等待时间,在这个时间段内会尝试获取锁。如果在规定时间内获取到了锁,则返回
true,否则返回false。 - 设置锁的自动释放时间:除了设置等待时间,还可以指定锁的持有时间,即使持有锁的线程忘记手动释放锁,它也会在设定时间后自动释放。
2.2.2、可重入性
Redisson 的 RLock 是 可重入的分布式锁,这意味着同一线程可以多次获取同一把锁,而不必担心死锁问题。例如:
java
复制代码
// 线程已经持有锁,可以再次获取锁
lock.lock();
// 在任务完成后必须释放相应次数的锁
lock.unlock();
可重入锁能够确保线程在持有锁的情况下多次进入临界区,并在相应次数的 unlock() 调用后才真正释放锁。
2.2.3自动续期(Watchdog)
Redisson 具有看门狗机制(Watchdog),在你没有指定锁的过期时间时,它会默认将锁的过期时间设置为 30 秒。如果在这段时间内,持有锁的线程没有主动释放锁,Redisson 的看门狗机制会自动延长锁的过期时间,直到持有锁的线程主动释放锁为止。
- 如果没有手动指定锁的超时时间,Redisson 会启动一个后台任务来每隔 10 秒自动续期,保证锁不会意外过期。
3、Redis如何实现可重入性
概述:使用Redis的hash结构存储可重入次数结合lua脚本来实现。hash结构存储“用户信息owner”、“重入次数count”、“过期时间PEXPIRE”。
可重入锁的基本思路
- 线程标识: 为了实现可重入锁,锁需要知道哪个线程已经持有锁。因此,每次获取锁时,需要将线程的唯一标识(如
UUID)作为锁的值存储在 Redis 中。 - 重入计数: 当一个线程多次获取同一把锁时,不应该阻塞或失败,而是增加锁的重入计数器。每次释放锁时,计数器减 1,直到计数器为 0 时才真正释放锁。
- 锁的过期时间: 为了防止锁在系统出现故障时无法释放,可重入锁同样需要设置锁的过期时间。每次重入时,可以延长锁的过期时间,确保锁在正常情况下不会自动过期。
参考代码:
获取锁的 Lua 脚本:
-- KEYS[1] 是锁的键名
-- ARGV[1] 是线程唯一标识 (UUID)
-- ARGV[2] 是过期时间 (毫秒)
-- 检查当前锁是否已经存在
local lockOwner = redis.call("HGET", KEYS[1], "owner")
-- 如果锁不存在,创建锁
if not lockOwner then
redis.call("HSET", KEYS[1], "owner", ARGV[1])
redis.call("HSET", KEYS[1], "count", 1)
redis.call("PEXPIRE", KEYS[1], ARGV[2])
return 1 -- 获取锁成功
end
-- 如果锁存在,且是当前线程持有的,则递增重入计数
if lockOwner == ARGV[1] then
redis.call("HINCRBY", KEYS[1], "count", 1)
redis.call("PEXPIRE", KEYS[1], ARGV[2]) -- 更新锁的过期时间
return 1 -- 获取锁成功
end
-- 如果锁存在,且不是当前线程持有的,返回 0 表示获取锁失败
return 0
这个 Lua 脚本做了以下几件事情:
- 检查 Redis
hash中是否有名为owner的字段(表示锁是否已经存在)。 - 如果锁不存在,创建锁,设置持有者(即
owner),重入计数为 1,并设置过期时间。 - 如果锁已经由当前线程持有,递增重入计数并延长锁的过期时间。
- 如果锁由其他线程持有,返回 0,表示获取锁失败。
释放锁的 Lua 脚本:
-- KEYS[1] 是锁的键名
-- ARGV[1] 是线程唯一标识 (UUID)
-- 检查当前锁是否由该线程持有
local lockOwner = redis.call("HGET", KEYS[1], "owner")
-- 如果锁不存在或不属于当前线程,返回 0 表示释放锁失败
if lockOwner ~= ARGV[1] then
return 0
end
-- 如果重入计数大于 1,则递减计数
local count = redis.call("HINCRBY", KEYS[1], "count", -1)
-- 如果重入计数为 0,释放锁
if count == 0 then
redis.call("DEL", KEYS[1])
end
return 1 -- 释放锁成功
这个 Lua 脚本执行以下操作:
- 检查当前线程是否持有锁(通过
hash中的owner字段)。 - 如果当前线程是锁的持有者,则递减重入计数。
- 当重入计数减少到 0 时,彻底删除锁。
- 如果锁不是当前线程持有,返回 0 表示释放失败。
4、防止其他线程误删
Reids:上面已经介绍了Redis中实现可重入锁时加入用户信息,只需在hash中添加线程的唯一信息标识,在解锁时判断是否为当前线程。
Redisson ,不需要手动处理误删锁的问题,Redisson内部通过多个机制,如唯一标识符(UUID)和Lua 脚本,来确保锁的安全性,防止其他线程误删锁。
5、锁续命
Reids:不具备自动续期机制,开发者需要在加锁时指定一个固定的过期时间,并且如果任务执行时间超过了过期时间,需要手动延长锁的过期时间。
Redis 实现锁续命的思路
在使用 Redis 分布式锁时,你可以通过定期刷新锁的过期时间来实现“续命”机制。实现思路如下:
- 获取锁时设置初始过期时间:在首次获取锁时,设置一个合理的过期时间,防止锁因程序异常或崩溃而长期存在,导致死锁。
- 定期续命:在持有锁的线程中,定期检查锁的剩余时间。如果锁快要过期,则发送一个命令来延长锁的过期时间,确保任务执行期间锁不会过期。
- 任务完成后手动释放锁:一旦任务完成,主动释放锁。
Redisson :可以自动管理锁的续期,它会监控持有锁的线程,在锁即将过期时自动延长过期时间。