一、现有的各种分布式锁

Java 里主流的 Redis 客户端有 Jedis、Lettuce、Redisson 等。Redisson 在此基础上提供了分布式可重入锁、读写锁、公平锁、红锁、多锁高层并发对象,用最少的业务代码实现分布式同步。


二、Redisson 分布式锁的核心目标

  1. 互斥:任一时刻只有一个“拥有者(Owner)”持有锁。

  2. 可重入:同一线程可对同一把锁重复加锁,计数 +1/-1。

  3. 自动续期(Watchdog):持锁线程未显式设置租期时,锁不会因超时误释放。

  4. 原子性:加锁/解锁由 Lua 脚本一次性在 Redis 端完成,避免多条命令的竞态。

  5. 等待/唤醒机制:阻塞等待 + 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 表示加锁成功
end

if (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
end

return 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)
end

local 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内部已做重试与感知。


九、典型故障与“坑”

  1. STW/长 GC 导致锁过期

    • 看门狗没来得及续期,锁被别人拿走 → 业务需幂等;或缩短临界区、指定固定租期并保证上限时间内完成。

  2. 解锁与加锁不在同线程

    • Redisson按 <clientId>:<threadId> 识别所有者,跨线程解锁会抛异常

    • 如需跨线程,考虑 RPermitExpirableSemaphore 或在同一业务线程中完成临界区。

  3. 忘记 finally 解锁

    • 必须 try/finally;监控锁键存活时间,超过阈值报警。

  4. 使用 SETNX 自己造锁却没校验“锁的所有者”

    • 可能把别人的锁删掉。正确做法:对 owner 做比对后再删除,或直接用 Redisson。

  5. 大规模突发等待导致惊群

    • 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 与重试机制保证原子性与可用性。
真正落地时,租期策略、临界区粒度、幂等等工程细节比选择哪种锁更重要。把这些做好,才能既“锁得住”,又“跑得快”

Logo

更多推荐