Redisson 是如何实现分布式锁的

18 阅读3分钟

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}
类型字段含义
HashUUID:ThreadId持有者 + 重入次数
Listredisson_lock_queue:{resource}公平锁排队
ZSetredisson_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
高并发公平RFairLock30s
跨机房高可用RedLock (≥3 节点)30s
业务耗时 > 1s业务线程池 + 较长租约

常见坑 & FAQ

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

总结

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

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