网络编程分布式锁怎么实现?

访客 网络编程 1

本文目录导读:

  1. 核心原则
  2. 方案一:基于 Redis 的分布式锁
  3. 方案二:基于 ZooKeeper / Etcd 的分布式锁(强一致性)
  4. 方案三:基于数据库的分布式锁(不推荐用于高并发)
  5. 选型建议

这是一个非常经典的分布式系统问题,网络编程中的分布式锁,其核心目标是:在分布式系统中,多个进程/节点之间,保证对同一共享资源的访问互斥(即同一时刻只有一个节点能获取到锁)。

因为传统的 synchronizedReentrantLock 只在单个JVM进程内有效,跨进程、跨网络时无法使用。

以下是目前最主流的三种分布式锁实现方式,从简单到可靠依次介绍。


核心原则

  • 互斥性:任何时刻,只有一个客户端能持有锁。
  • 安全性:锁在获取后,如果持有者崩溃(网络断开、进程死掉),锁必须能自动释放,避免死锁。
  • 高可用/容错:锁服务本身要稳定,不能单点故障(例如使用 Redis 集群或 Etcd 集群)。
  • 可重入性(可选):同一客户端是否可以多次获取同一把锁。

基于 Redis 的分布式锁

这是最常见、性能最高的方案,原理是利用 Redis 单线程处理命令的特性,通过 SETNX 命令实现互斥。

基础实现(不推荐用于生产)

思想:用 SETNX key value,key 不存在则设置成功(获得锁),存在则失败。 致命问题:无法处理死锁,如果获得锁的客户端崩溃,锁永远不释放。

正确实现:SET 命令 + 过期时间 + Lua 脚本(推荐)

这是目前 Redis 官方推荐的标准做法。

获取锁:

SET lock_key unique_value NX PX 30000
  • NX:只有 key 不存在时才设置(保证互斥)。
  • PX 30000:设置 30 秒自动过期(解决死锁问题)。
  • unique_value:客户端的唯一标识(如 UUID + 线程ID)。非常重要,用于释放锁时校验。

释放锁(保证原子性): 不能直接用 DEL lock_key,因为你可能释放掉别人刚获取的锁(比如自己业务执行太久,锁过期了),必须使用 Lua 脚本确保“先判断再删除”是原子操作。

-- Lua 脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

代码示例(Java + Jedis):

public class RedisLock {
    private Jedis jedis;
    private String lockKey;
    private String lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
    private int expireTime = 30000; // 30秒
    public boolean lock() {
        // SET key value NX PX expireTime
        String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);
        return "OK".equals(result);
    }
    public boolean unlock() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
        return "1".equals(result.toString());
    }
}

优点:高性能、简单。 缺点:锁过期时间不好设(业务执行太久会自动释放);Redis 主从切换时可能丢锁(异步复制导致)。

进阶:Redlock 算法(用于 Redis 集群)

为了解决 Redis 主从切换丢锁的问题,Redis 作者提出了 Redlock,思想是:在 N 个独立的 Redis 节点上同时加锁,只有超过半数(N/2+1)节点成功才算加锁成功。

原理

  1. 客户端获取当前时间 T1
  2. 依次向所有 Redis 节点申请锁(超时时间很短,10ms)。
  3. 当获取成功的节点数 >= N/2 + 1,且总耗时小于锁的有效时间时,才认为锁成功。
  4. 释放时,向所有节点发送 unlock 脚本。

缺点:实现复杂,性能下降,且存在理论争议(时钟漂移问题),一般业务场景用简单 SET NX PX + 正确释放就能满足,99% 的场景不需要 Redlock


基于 ZooKeeper / Etcd 的分布式锁(强一致性)

这类方案利用“临时顺序节点”和“Watch 机制”实现,原理依赖于 Paxos / Raft 共识算法,保证了强一致性和锁的严格互斥。

具体实现流程(以 ZK 为例):

  1. 创建临时顺序节点: 每个客户端去锁目录 /locks/my_lock 下创建 EPHEMERAL_SEQUENTIAL 节点。

    • 客户端A:/locks/my_lock/lock_0000000001
    • 客户端B:/locks/my_lock/lock_0000000002
    • 客户端C:/locks/my_lock/lock_0000000003
  2. 获取最小节点: 所有客户端获取 /locks/my_lock 下的所有子节点列表,并对节点编号排序。

  3. 判断是否持有锁

    • 如果自己创建的节点是列表中最小的节点(序号最小),则成功获取锁。
    • 如果不是最小的,则对比自己节点序号小1的节点(即前一个节点)设置一个 Watch(监听器),然后阻塞等待。
  4. 释放锁

    • 正常释放:删除自己创建的临时节点。
    • 异常释放:客户端崩溃,会话断开,ZK 会自动删除该临时节点(天然避免死锁)。
  5. 通知下一个节点: 当上一个节点(如 lock_0000001)被删除时,ZK 会通知 Watch 它的节点(lock_0000002),该节点重新获取列表,发现自己变成最小,拿到锁。

完美解决

  • 死锁:临时节点机制。
  • 锁竞争:顺序排队,类似公平锁(公平性比 Redis 好)。
  • 锁超时:通过会话超时机制,比 Redis 固定过期时间更合理。

缺点

  • 性能:远低于 Redis(需多次网络交互,节点创建删除)。
  • 复杂度:ZK 集群运维比 Redis 重。
  • 羊群效应:高并发下大量节点 Watch 前一个节点,释放时惊群,可以优化(只 Watch 前一个)。

基于数据库的分布式锁(不推荐用于高并发)

利用数据库的唯一索引或行锁。

乐观锁(基于版本号)

适用于读多写少场景,不阻塞。

UPDATE resource SET version = version + 1, status = 'locked'
WHERE resource_id = ? AND version = ?; 

如果更新行数为0,说明版本号变了,获取锁失败。

悲观锁(基于 SELECT ... FOR UPDATE

BEGIN;
SELECT * FROM resource WHERE resource_id = ? FOR UPDATE; 
-- 如果查到了,就认为拿到锁
-- 执行业务...
COMMIT; -- 释放锁

利用数据库的行级锁实现互斥。

优点:简单,不需要额外组件。 缺点

  • 性能差(数据库连接池、磁盘I/O)。
  • 容易死锁(事务超时后回滚释放)。
  • 数据库压力大,不适合高并发。

方案 实现难度 性能 可靠性 自动释放锁 可重入 典型场景
Redis (SET NX PX) 最高 中等(存在丢锁风险) 需设置过期时间 需额外实现 高并发、允许小概率锁丢失(如秒杀、缓存更新)
Redlock 较好(理论上容错强) 同上 同上 对可靠性要求更高的 Redis 场景
ZooKeeper/Etcd 中等 最高(强一致性) 天然支持(临时节点) 天然实现 对一致性要求极高(如金融、配置中心、调度)
数据库 最低 中等 需事务/超时控制 可简单实现 老系统、低并发、内网环境

选型建议

  • 追求极致性能,能接受小概率锁丢失:选 Redis(SET NX PX + Lua),这是目前互联网公司的首选。
  • 金融、计费、调度等要求严格互斥,不能有一点差池:选 ZooKeeper / Etcd
  • 公司完全没用 Redis/ZK,只用了 MySQL,并发极低:用 数据库乐观锁FOR UPDATE
  • 需要保护锁节点,避免引入第三方中间件(如云原生环境):可以考虑基于 Etcd 的锁(比 ZK 更云原生、更轻量)。

最后提醒:无论选哪种方案,一定要保证锁的释放逻辑(unlock)在 finally 块中执行,并且要处理业务执行时间超过锁过期时间的问题(可以给锁加一个“看门狗”线程自动续期,或者使用 Redisson 等成熟框架)。

标签: ZooKeeper分布式锁

抱歉,评论功能暂时关闭!