基于 SETNX 实现分布式锁
·
1️⃣ 基于 SETNX 实现分布式锁
1. 🔹 SETNX 的基本原理
SETNX(SET if Not Exists)用于 当 key 不存在时,才设置值:
SETNX lock_key unique_id # 只有 lock_key 不存在时才会设置成功
但是 SETNX 只会设置 key,并不会自动过期。因此,如果客户端崩溃,锁可能会永远不会释放,导致死锁。
解决方案:使用 SET 命令 + EX 过期时间
SET lock_key unique_id NX EX 10 # 仅当 lock_key 不存在时设置,并 10 秒后自动释放
2. 🔹 加锁
String lockKey = "lock_key";
String uniqueId = UUID.randomUUID().toString();
boolean lockAcquired = jedis.set(lockKey, uniqueId, "NX", "EX", 10) != null;
NX:如果lock_key不存在,则创建(避免覆盖其他线程的锁)。EX 10:10 秒后锁自动释放,防止死锁。
3. 🔹 释放锁(用 Lua 脚本保证原子性)
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Java 代码:
String luaScript =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object result = jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(uniqueId));
✅ Lua 脚本确保 GET 和 DEL 在同一事务中执行,避免锁被误删。
4. 🔹 存在的问题
- 锁过期时任务未完成,可能导致锁失效(解决方案:滑块锁)。
- 不支持重入,同一线程无法重复加锁(解决方案:Redisson)。
- 锁可能被误删(解决方案:用 Lua 脚本释放)。
2️⃣ 使用 Redisson 实现分布式锁
Redisson 是基于 Redis 实现的 Java 分布式锁组件,支持:
- 可重入锁(Reentrant Lock)
- 读写锁(ReadWrite Lock)
- 公平锁
- 联锁 / 红锁
- 看门狗自动续期(防止锁过期)
🔹 Redisson 加锁
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("lock_key");
try {
lock.lock(); // 获取锁(默认 30 秒自动续约)
System.out.println("加锁成功,执行任务...");
Thread.sleep(15000); // 模拟任务执行
} finally {
lock.unlock(); // 释放锁
}
✅ 支持重入锁,同一线程可以多次 lock(),只需 unlock() 一次即可释放。
🔹 看门狗机制(自动续约)
Redisson 默认有 30 秒超时时间,并会 每 10 秒自动续约:
- 如果任务还在执行,锁不会过期。
- 如果任务完成,调用
unlock()释放锁。
lock.lock(10, TimeUnit.SECONDS); // 10 秒后自动释放(无看门狗)
lock.lock(); // 默认 30 秒超时 + 看门狗续期
Redisson 的各种锁机制
可重入锁(Reentrant Lock)
🔹 概念
- 同一线程可以多次加锁,必须释放相同次数才能完全释放锁。
- 解决了普通
SETNX无法重复加锁的问题,防止死锁。
🔹 工作原理
- 使用 Redis
hash结构,存储 锁的持有线程 ID 和重入次数。 - 每次相同线程加锁时,
count +1;每次释放锁时,count -1,当count == 0时,真正释放锁。
🔹 代码示例
RLock lock = redisson.getLock("reentrant_lock");
try {
lock.lock(); // 第一次加锁
lock.lock(); // 第二次加锁(重入)
System.out.println("锁被成功获取");
} finally {
lock.unlock(); // 释放一次锁
lock.unlock(); // 释放第二次锁(真正释放)
}
🔹 实际应用
- 支付扣款
-
- 防止用户多次支付导致金额扣减错误
- 线程 A:调用
lock.lock()进行扣款 - 线程 A 还需要检查库存,也需要
lock.lock(),如果不支持重入锁,则会死锁
读写锁(ReadWrite Lock)
🔹 概念
- 多个线程可以同时读
- 写操作必须互斥
- 适用于高读低写的场景,避免写操作影响读性能。
🔹 工作原理
- 读锁(共享锁):多个线程可以同时
lock.readLock().lock(),提高并发能力。 - 写锁(独占锁):当有线程
lock.writeLock().lock()时,其他读/写操作都要等待。
🔹 代码示例
RReadWriteLock rwLock = redisson.getReadWriteLock("rw_lock");
// 读锁(多个线程可以同时读)
rwLock.readLock().lock();
try {
System.out.println("正在读取数据...");
} finally {
rwLock.readLock().unlock();
}
// 写锁(写入时,其他线程不能读)
rwLock.writeLock().lock();
try {
System.out.println("正在写入数据...");
} finally {
rwLock.writeLock().unlock();
}
🔹 实际应用
- 商品库存查询
-
- 读多写少的场景:允许多个线程并发读取库存数据
- 防止写操作时数据不一致
- 秒杀活动
-
- 用户可以同时查看商品详情(读锁)
- 当库存减少(写锁)时,读操作必须等待,保证数据一致性
公平锁(Fair Lock)
🔹 概念
- 先请求的线程,先获取锁(FIFO 先进先出)
- 适用于高并发业务,防止某些线程长时间等待锁。
🔹 工作原理
- 非公平锁:默认情况下,Redis 直接尝试
SETNX,谁先抢到算谁的。 - 公平锁:
-
- 使用 Redis List 存储排队的线程
- 按顺序唤醒等待时间最长的线程,保证公平性。
🔹 代码示例
RLock fairLock = redisson.getFairLock("fair_lock");
try {
fairLock.lock();
System.out.println("获取公平锁成功");
} finally {
fairLock.unlock();
}
🔹 实际应用
- 秒杀抢购
-
- 传统非公平锁可能让某些用户连续抢到商品,导致部分用户一直抢不到
- 公平锁可以按用户请求时间排序,防止饥饿
- 分布式任务调度
-
- 避免任务调度时,某些任务长期得不到执行
联锁(MultiLock)
🔹 概念
- 需要同时获取多个锁才能成功加锁,防止单点故障。
- 适用于多个 Redis 实例的分布式环境。
🔹 工作原理
- 同时对多个 Redis 实例 执行
SETNX,只有全部成功才算加锁成功。 - 防止 Redis 宕机导致锁失效。
🔹 代码示例
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");
// 获取多个锁
RLock multiLock = redisson.getMultiLock(lock1, lock2, lock3);
try {
multiLock.lock();
System.out.println("成功获取多个锁");
} finally {
multiLock.unlock();
}
🔹 实际应用
- 跨数据中心的分布式锁
-
- 例如:你有多个 Redis 服务器(中国、美国、欧洲),你希望所有数据中心都能加锁,防止某个 Redis 挂掉导致锁失效。
红锁(RedLock)
🔹 概念
- 保证分布式锁的高可用性
- 适用于 多个 Redis 实例 的环境
🔹 工作原理
- 采用 N 个 Redis 实例,加锁时必须在 大多数(N/2+1)个实例 上成功,才算真正加锁成功。
- 这样,即使部分 Redis 宕机,锁仍然有效。
🔹 代码示例
RedissonClient redisson1 = createRedissonClient("redis://127.0.0.1:6379");
RedissonClient redisson2 = createRedissonClient("redis://127.0.0.2:6379");
RedissonClient redisson3 = createRedissonClient("redis://127.0.0.3:6379");
RLock redLock = redisson1.getRedLock(
redisson1.getLock("red_lock"),
redisson2.getLock("red_lock"),
redisson3.getLock("red_lock")
);
try {
redLock.lock();
System.out.println("获取红锁成功");
} finally {
redLock.unlock();
}
🔹 实际应用
- 银行转账
-
- 确保多个数据中心的账户不会因为 Redis 宕机而重复转账
- 高可用微服务
-
- 微服务 A/B/C 访问相同资源,确保 Redis 宕机时仍然可以加锁
看门狗自动续期
🔹 概念
- 防止锁过期,导致任务中断
- Redisson 默认锁 30 秒,如果任务未完成,会自动续期
🔹 工作原理
- 每 10 秒,Redisson 检查锁是否还在使用
- 如果任务还未完成,就自动 续期 30 秒
- 任务完成后,
unlock()释放锁
🔹 代码示例
RLock lock = redisson.getLock("auto_extend_lock");
try {
lock.lock(); // 看门狗自动续期
System.out.println("执行任务...");
Thread.sleep(20000); // 任务超过 30s 也不会丢失锁
} finally {
lock.unlock();
}
🔹 实际应用
- 防止任务执行超时
-
- 适用于 爬虫任务、视频转码 等长时间任务
- 防止 Redis 宕机导致任务失败
更多推荐
所有评论(0)