Redisson分布式锁的简述

setnxRedLock,从 Lua 脚本WatchDog,一文彻底讲透
版本:Redisson 3.23 + Redis 7 + JDK 17


目录

  1. 背景与痛点
  2. 核心数据结构
  3. 加锁流程(可重入锁)
  4. 释放锁流程
  5. WatchDog 自动续期
  6. 公平锁实现
  7. RedLock & MultiLock
  8. 线程模型与性能
  9. 最佳实践
  10. 常见坑 & FAQ
  11. 总结

背景与痛点

原生 SETNX 缺陷 Redisson 解决方案
不可重入 Hash 结构 + 重入计数
无超时释放 过期时间 + WatchDog
无重试队列 Pub/Sub 等待通知
主从一致性 RedLock / MultiLock

核心数据结构

Redis Key

lock:{resource}
类型 字段 含义
Hash UUID:ThreadId 持有者 + 重入次数
List redisson_lock_queue:{resource} 公平锁排队
ZSet redisson_lock_timeout:{resource} 公平锁超时

加锁流程(可重入锁)

1. 入口方法

RLock lock = redisson.getLock("order:123");
lock.lock();   // 默认 30s 租约

2. Lua 脚本(精简版)

-- KEYS[1] = lockKey, ARGV[1] = 线程标识, ARGV[2] = 过期时间(ms)
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[1], 1);
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return nil;  -- 表示获取成功
end;
if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[1], 1);
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return nil;
end;
return redis.call('pttl', KEYS[1]);  -- 返回剩余时间,客户端重试

3. 重试机制

  • 自旋重试attempts × sleep(默认 3 次)
  • Pub/Sub 等待:客户端订阅 __keyspace@*__:{lockKey},解锁时收到通知,减少 CPU 空转。

释放锁流程

Lua 脚本

-- KEYS[1] = lockKey, KEYS[2] = 解锁频道, ARGV[1] = 线程标识, ARGV[2] = 过期时间
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
    return nil;  -- 不是自己持有
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
if (counter > 0) then
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
else
    redis.call('del', KEYS[1]);
    redis.call('publish', KEYS[2], 1);  -- 通知等待队列
    return 1;
end;

WatchDog 自动续期

触发时机 续期间隔 停止条件
首次加锁成功 leaseTime / 3(默认 10s) 线程主动 unlock()
每次续期 重置 leaseTime 进程崩溃 / 网络分区

源码位置:RedissonLock.scheduleExpirationRenewal()Netty HashedWheelTimer 周期性执行续期 Lua。


公平锁实现

数据结构

  • List redisson_lock_queue:{resource} → 排队顺序
  • ZSet redisson_lock_timeout:{resource} → 超时淘汰

Lua 片段(公平锁加锁)

-- 当前线程必须在队首才能获取锁
if (redis.call('exists', KEYS[1]) == 0 and
    (redis.call('exists', KEYS[3]) == 0 or
     redis.call('lindex', KEYS[3], 0) == ARGV[1])) then
    redis.call('lpop', KEYS[3]);
    redis.call('zrem', KEYS[4], ARGV[1]);
    redis.call('hset', KEYS[1], ARGV[1], 1);
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return nil;
end;

RedLock & MultiLock

RedLock(多数派)

RedissonRedLock lock = new RedissonRedLock(
    redisson.getLock("lock1"),
    redisson.getLock("lock2"),
    redisson.getLock("lock3")
);
lock.lock();
  • 加锁成功 > N/2 节点
  • 故障恢复 依赖 时钟漂移 容错

MultiLock(全部成功)

RedissonMultiLock lock = new RedissonMultiLock(
    redisson.getLock("lock1"),
    redisson.getLock("lock2")
);
lock.lock();

线程模型 & 性能

维度 说明
内部线程 Netty EventLoop 或自定义 ExecutorService
回调线程 与加锁线程相同,避免上下文切换
连接池 redisson.nettyThreads = 0 自动 CPU*2

最佳实践清单

场景 推荐锁类型 租约时间
普通互斥 RLock 业务最大耗时 × 2
高并发公平 RFairLock 30s
跨机房高可用 RedLock (≥3 节点) 30s
业务耗时 > 1s 业务线程池 + 较长租约

常见坑 & FAQ

解决
误删锁 使用 isHeldByCurrentThread() 判断
时钟漂移 RedLock 增加 clock-drift-factor
网络分区 设置合理 lockWatchdogTimeout

总结

  • 加锁 = Lua 原子脚本 + Hash 重入
  • 续期 = WatchDog 定时任务
  • 一致性 = RedLock / MultiLock
  • 解锁 = 线程标识 + Lua 原子检查

掌握这四步,你就拥有了 分布式锁的终极武器

posted @ 2025-08-22 15:08  深圳蔓延科技有限公司  阅读(16)  评论(0)    收藏  举报