详解Redisson分布式锁原理和实现
Redisson 的分布式锁设计用**“Hash + TTL + Lua + Pub/Sub + Watchdog”组合拳,解决了互斥、可重入、阻塞等待与误释放**等核心问题;在哨兵与集群场景下,它通过 HashTag 与重试机制保证原子性与可用性。真正落地时,租期策略、临界区粒度、幂等等工程细节比选择哪种锁更重要。把这些做好,才能既“锁得住”,又“跑得快”
一、现有的各种分布式锁
Java 里主流的 Redis 客户端有 Jedis、Lettuce、Redisson 等。Redisson 在此基础上提供了分布式可重入锁、读写锁、公平锁、红锁、多锁等高层并发对象,用最少的业务代码实现分布式同步。
二、Redisson 分布式锁的核心目标
-
互斥:任一时刻只有一个“拥有者(Owner)”持有锁。
-
可重入:同一线程可对同一把锁重复加锁,计数 +1/-1。
-
自动续期(Watchdog):持锁线程未显式设置租期时,锁不会因超时误释放。
-
原子性:加锁/解锁由 Lua 脚本一次性在 Redis 端完成,避免多条命令的竞态。
-
等待/唤醒机制:阻塞等待 + Pub/Sub 唤醒,避免无意义自旋。
三、数据结构与键设计
Redisson 使用一个 Redis Hash 存储锁的“所有者标识与重入计数”,再配合一个 Channel 做唤醒通知。
-
锁键:
{lockName}(Hash 类型)-
field:
<clientId>:<threadId>(JVM 唯一标识 + 线程 ID) -
value:重入计数(integer)
-
-
过期时间:设置在 Hash 键本身(PEXPIRE)
-
发布订阅通道:
redisson_lock__channel:{lockName}(用于通知等待者“锁已释放”) -
(公平锁/读写锁会再引入排队/票据键,后面详述)
注意
{}是 Hash Tag,在 Redis Cluster 中保证所有与这把锁相关的键落在同一槽位,从而 Lua 能在单节点内原子执行。
四、加锁流程(RLock)——用 Lua 保证原子性
尝试加锁的 Lua 逻辑(简化伪码):
-- KEYS[1] = 锁Hash键, ARGV[1] = leaseTime(ms), ARGV[2] = ownerId(clientId:threadId)
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil -- nil 表示加锁成功
endif (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1) -- 可重入
redis.call('pexpire', KEYS[1], ARGV[1]) -- 续期
return nil
endreturn redis.call('pttl', KEYS[1]) -- 返回剩余 TTL,表示被别人持有
客户端侧策略:
-
若返回
nil:加锁成功。 -
若返回 TTL:进入阻塞等待(或 tryLock 里继续等待),并订阅
...channel,收到解锁消息再重试一次(避免惊群通过延迟/jitter 控制)。
五、看门狗(Watchdog)与租期(leaseTime)
-
默认行为:如果你不传租期(
lock.lock()),Redisson 启动看门狗线程,默认lockWatchdogTimeout=30s,并以每 1/3 周期(约 10s)自动给锁续期,直到线程释放或 JVM 退出。 -
显式租期:如果你传了
lock.lock(10, TimeUnit.SECONDS),不会启用看门狗,这把锁在 10s 后必定过期(即使线程还活着)。
建议:任务耗时不确定 → 用看门狗;任务耗时可预估且严格控制 → 指定固定租期,确保最坏情况下也能自动释放。
六、解锁流程 —— 只有“自己”能解
解锁 Lua(简化伪码):
-- KEYS[1] = 锁Hash键, ARGV[1] = ownerId
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
return 0 -- 非持有者解锁,返回0(客户端会抛 IllegalMonitorStateException)
endlocal counter = redis.call('hincrby', KEYS[1], ARGV[1], -1)
if (counter > 0) then
redis.call('pexpire', KEYS[1], <leaseTime>)
return 1 -- 仍有重入余量
else
redis.call('del', KEYS[1])
redis.call('publish', 'redisson_lock__channel:{lockName}', 'unlock')
return 1 -- 完全释放,通知等待者
end
关键点:
-
只有同一个
<clientId>:<threadId>可以递减重入计数; -
计数到 0 才真正删除锁键,并通过 Pub/Sub 通知等待的客户端。
七、可重入、公平、读写与多锁
1)可重入锁(RLock)
-
同一线程多次
lock()会递增计数。 -
必须成对
unlock();否则看门狗会一直续期(造成锁“粘住”)。
示例
RLock lock = redisson.getLock("order:pay:lock");
lock.lock(); // 第一次获锁
try {
// do something
lock.lock(); // 可重入
try {
// do nested things
} finally {
lock.unlock();
}
} finally {
lock.unlock(); // 计数归零,真正释放
}
2)公平锁(RLock fair = getFairLock)
-
维护一个先进先出的等待队列(通过队列键 + 票据 + 过期控制)。
-
适用于严格避免“插队”的场景(开销略大于普通锁)。
RLock fair = redisson.getFairLock("ticket:fair:lock"); fair.lock(30, TimeUnit.SECONDS);
3)读写锁(RReadWriteLock)
-
多读共享、写独占;同一线程可对写锁重入。
-
内部用两个键管理读/写状态 + Pub/Sub 协调。
RReadWriteLock rw = redisson.getReadWriteLock("doc:123");
RLock r = rw.readLock();
RLock w = rw.writeLock();
4)红锁(RedLock)与多锁(MultiLock)
-
RedissonRedLock:将多把锁(来自不同 Redis 主节点)组合为一把;需在多个实例上都成功(达到法定票数)才算获锁。
-
注意:RedLock 有学术争议(网络分区 / 时钟漂移下的安全性)。多数业务里优先用单实例 + Sentinel/Cluster即可,跨机房强一致场景建议考虑 Zookeeper/Etcd。
RLock l1 = redisson1.getLock("key");
RLock l2 = redisson2.getLock("key");
RLock l3 = redisson3.getLock("key");
RedissonRedLock redLock = new RedissonRedLock(l1, l2, l3); redLock.lock();
八、在不同部署形态下如何工作
1)主从 + 哨兵(Sentinel)
-
写入只在主节点;从节点异步复制。
-
主故障切换时,尚未复制到从的锁可能丢失 → 新主不包含该锁键。
-
缓解:对关键段设置较短业务租期 + 幂等补偿;或通过 RedLock/多机写(权衡复杂度与争议)。
2)Redis Cluster
-
使用
{hashTag}确保相关键同槽位,Lua 可原子执行。 -
分片升降级时注意键迁移短暂不可用;Redisson内部已做重试与感知。
九、典型故障与“坑”
-
STW/长 GC 导致锁过期
-
看门狗没来得及续期,锁被别人拿走 → 业务需幂等;或缩短临界区、指定固定租期并保证上限时间内完成。
-
-
解锁与加锁不在同线程
-
Redisson按
<clientId>:<threadId>识别所有者,跨线程解锁会抛异常。 -
如需跨线程,考虑
RPermitExpirableSemaphore或在同一业务线程中完成临界区。
-
-
忘记 finally 解锁
-
必须
try/finally;监控锁键存活时间,超过阈值报警。
-
-
使用
SETNX自己造锁却没校验“锁的所有者”-
可能把别人的锁删掉。正确做法:对 owner 做比对后再删除,或直接用 Redisson。
-
-
大规模突发等待导致惊群
-
Redisson通过 Pub/Sub 唤醒并在客户端侧做限流/延迟重试;你可在业务层再做排队/漏斗限流。
-
十、最佳实践清单
-
优先用
tryLock(wait, lease, unit):可设置等待超时与租期,避免无限阻塞和“粘锁”。
if (lock.tryLock(200, 10, TimeUnit.MILLISECONDS)) {
try { /* 临界区 ≤ 10ms */ }
finally { lock.unlock(); }
} else {
// 降级/重试
}
-
临界区做“短快小”:只包含必须的原子修改。把 IO/远调用移出锁内。
-
任务时长不可控 → 让看门狗续期;可控 → 固定租期 + 兜底超时。
-
关键业务幂等化:拿到锁≠绝对独占;面对极端网络/故障需能重复执行或回滚。
-
命名规范:
业务域:资源标识:lock,避免不同场景锁名冲突。 -
监控:
-
锁键数量、存活时长分布、等待失败率
-
业务异常:等待超时/解锁异常统计
-
Redis 侧:延迟、复制落后、主从切换告警
-
十一、与“ZooKeeper 分布式锁”的对比(简述)
| 维度 | Redisson(基于 Redis) | ZK/Etcd |
|---|---|---|
| 延迟/吞吐 | 极低延迟,吞吐高 | 一致性强,吞吐较低 |
| 复杂度 | 接入简单 | 运维与范式更复杂 |
| 一致性 | BASE/最终一致(主从异步时有窗口) | 强一致(CP) |
| 适用场景 | 高并发短临界区、缓存侧业务 | 强一致协调、配置、选主 |
十二、完整示例(配置 + 使用)
Redisson 配置(Sentinel)
Config cfg = new Config();
cfg.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("redis://10.0.0.1:26379",
"redis://10.0.0.2:26379")
.setPassword("pwd")
.setDatabase(0);
// 看门狗超时(默认 30s)
cfg.setLockWatchdogTimeout(30000);
RedissonClient redisson = Redisson.create(cfg);
业务代码(健壮模式)
RLock lock = redisson.getLock("inventory:sku123:lock");
boolean ok = lock.tryLock(300, 10, TimeUnit.MILLISECONDS);
// 等待最多 300ms,获锁后 10ms 到期(不续期)
if (!ok) {
// 降级:走缓存/排队/快速失败
return;
}
try {
// 临界区:扣减库存(≤10ms)
} finally {
// 防御式解锁:判断当前线程是否持有
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
结语
Redisson 的分布式锁设计用**“Hash + TTL + Lua + Pub/Sub + Watchdog”组合拳,解决了互斥、可重入、阻塞等待与误释放**等核心问题;在哨兵与集群场景下,它通过 HashTag 与重试机制保证原子性与可用性。
真正落地时,租期策略、临界区粒度、幂等等工程细节比选择哪种锁更重要。把这些做好,才能既“锁得住”,又“跑得快”
更多推荐
所有评论(0)