07. 分布式锁与协调服务

对比 Redis RedLock、Redisson、ZooKeeper Curator 与 Etcd 分布式锁实现,分析锁续期、可重入、羊群效应与脑裂问题

分布式锁是协调多节点并发访问共享资源的基础设施。从简单的 Redis SETNX 到工业级的 Redisson、Curator,每种方案都在一致性、可用性与性能间做着不同的取舍。

1. 分布式锁核心要求

特性说明
互斥性任意时刻只有一个客户端持有锁
防死锁客户端崩溃后锁自动释放(TTL / Session)
可重入同一线程可重复获取同一锁
容错性部分节点故障不影响整体
锁续期业务未完成时自动延长持有时间

2. Redis 分布式锁演进

2.1 阶段一:SETNX + EXPIRE(初级)

// 问题:SETNX 与 EXPIRE 非原子,中间崩溃导致死锁
Boolean acquired = jedis.setnx(lockKey, clientId);
if (acquired) jedis.expire(lockKey, 30);  // 非原子!

2.2 阶段二:SET key value NX PX(改进)

SET lock:order:123 my-client-id NX PX 30000
// Lua 释放锁(原子性检查 + 删除)
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] " +
             "then return redis.call('del', KEYS[1]) " +
             "else return 0 end";
jedis.eval(lua, Collections.singletonList(lockKey), 
           Collections.singletonList(clientId));

2.3 阶段三:RedLock(Redis 作者提出)

假设 5 个独立的 Redis Master 节点(无主从,无 Sentinel)

1. 记录当前时间 T1
2. 依次向 5 个节点请求加锁,超时时间设为锁 TTL 的很小比例(如 10ms)
3. 计算总耗时 = 当前时间 T2 - T1
4. 若成功加锁的节点数 ≥ N/2 + 1(即 3 个),且总耗时 < TTL
5. 则加锁成功,有效时间 = TTL - 总耗时
6. 若失败,向所有节点发送解锁请求

争议:Redisson 作者认为 RedLock 在时钟跳跃、网络分区下仍不完美;推荐直接使用 Redlock-based 实现即可。

2.4 Redisson:工业级实现

Config config = new Config();
config.useClusterServers()
    .addNodeAddress("redis://node1:7000", "redis://node2:7000");
RedissonClient redisson = Redisson.create(config);

// 可重入锁 + 看门狗自动续期
RLock lock = redisson.getLock("myLock");
try {
    // 尝试加锁,最多等待 10 秒,锁 30 秒后自动释放
    // 看门狗:持有锁的线程存活时,每 10 秒续期 TTL 到 30 秒
    boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
    if (acquired) {
        // 执行业务
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

看门狗原理

加锁成功 → 启动 Watchdog 线程(Netty HashedWheelTimer)
  │
  ├─ 每 lockWatchdogTimeout / 3 续期一次(默认 10s)
  │   └─ 延长锁 TTL = lockWatchdogTimeout(默认 30s)
  │
  └─ 业务线程结束 / 解锁 → Watchdog 取消

公平锁

RLock fairLock = redisson.getFairLock("fairLock");
// 按请求锁的先后顺序获取,避免饥饿

读写锁

RReadWriteLock rwLock = redisson.getReadWriteLock("myLock");
RLock readLock = rwLock.readLock();   // 共享
RLock writeLock = rwLock.writeLock(); // 排他

Redisson 锁类型矩阵

类型特点
RLock可重入非公平锁
RSemaphore信号量,限并发数
RCountDownLatch计数等待
RLock fairFIFO 公平锁

3. ZooKeeper 分布式锁

3.1 ZK 的优势

  • 强一致性:ZAB 协议保证写入顺序一致
  • 临时节点:会话断开自动删除,天然防死锁
  • Watcher 通知:事件驱动,无需阻塞轮询

3.2 基于临时顺序节点的锁

/locks/order
  ├── lock-0000000001  ← 最小序号,获取锁
  ├── lock-0000000002  ← 监听前一个节点 lock-0001
  └── lock-0000000003  ← 监听前一个节点 lock-0002

3.3 Curator 框架实现

RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("zk1:2181,zk2:2181", retryPolicy);
client.start();

// 可重入互斥锁
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order");
try {
    if (lock.acquire(10, TimeUnit.SECONDS)) {
        // 业务逻辑
    }
} finally {
    lock.release();
}

Curator 锁对比

实现类特点
InterProcessMutex可重入互斥锁
InterProcessSemaphoreMutex不可重入互斥锁
InterProcessReadWriteLock读写锁
InterProcessMultiLock多锁原子获取

3.4 羊群效应 vs 监听链优化

方案行为风险
监听父节点所有等待者收到通知,同时竞争羊群效应
监听前一个节点只有紧邻的下一个收到通知无羊群效应,但监听链过长增加 ZK 负担

Curator 使用 监听前一个节点 方案。

4. Etcd 分布式锁

4.1 Etcd 优势

  • Raft 共识:强一致性
  • TTL + Lease:租约机制,自动过期
  • Revision / Event:全局递增版本号,天然 FIFO

4.2 基于 Etcd 的锁实现(jetcd)

Client etcdClient = Client.builder().endpoints("http://etcd:2379").build();
KV kvClient = etcdClient.getKVClient();
Lease leaseClient = etcdClient.getLeaseClient();

// 创建租约(10 秒 TTL)
LeaseGrantResponse lease = leaseClient.grant(10).get();
long leaseId = lease.getID();

// 原子写入:key = lock:resource, value = node-id, 绑定租约
ByteSequence key = ByteSequence.from("lock:order:123", StandardCharsets.UTF_8);
ByteSequence value = ByteSequence.from("node-1", StandardCharsets.UTF_8);

PutOption putOpt = PutOption.newBuilder().withLeaseId(leaseId).build();
kvClient.put(key, value, putOpt).get();

// 租约续期(心跳)
ew KeepAliveListener keepAlive = leaseClient.keepAlive(leaseId);

// 释放:删除 key(原子)
kvClient.delete(key).get();

4.3 Etcd 分布式锁的租约续期

// 持续续期直到业务完成
Executors.newSingleThreadExecutor().submit(() -> {
    try {
        while (!Thread.currentThread().isInterrupted()) {
            leaseClient.keepAliveOnce(leaseId).get();
            Thread.sleep(5000);
        }
    } catch (Exception e) {
        // 续期失败,锁可能已释放
    }
});

5. 三种方案对比

维度Redis (Redisson)ZooKeeper (Curator)Etcd (jetcd)
一致性最终一致(主从异步)强一致(ZAB)强一致(Raft)
可用性AP(主从可切换)CP(过半存活)CP(过半存活)
性能极高(单节点 10K+ QPS)中等(写需 leader)中等
死锁防护TTL + 看门狗临时节点 + 会话Lease TTL
可重入自行实现
公平锁自行实现
适用场景高并发、短期锁协调服务、长期锁K8s 生态、服务发现

6. 锁的最佳实践

6.1 锁粒度控制

// ❌ 粗粒度:锁住整个表
lock.lock("products");

// ✅ 细粒度:按业务维度拆分
lock.lock("product:" + productId);
lock.lock("inventory:" + sku);

6.2 锁 + 业务事务

// 错误:锁在事务之外
lock.lock();                // 锁 A
transactionService.execute(); // 事务开始 → 锁可能已超时释放!
lock.unlock();

// 正确:锁在事务内部(存储过程 / AOP 环绕)
transactionService.execute(() -> {
    lock.lock();
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
});

6.3 防御性编程

try {
    if (!lock.tryLock(5, TimeUnit.SECONDS)) {
        throw new LockAcquisitionException("获取锁超时");
    }
    // 双重检查
    if (alreadyProcessed(orderId)) return;
    processOrder();
} catch (Exception e) {
    log.error("处理异常", e);
    throw e;
} finally {
    // 只释放自己持有的锁
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析