分布式锁的设计与实现
字数 2864 2025-12-14 17:51:51

分布式锁的设计与实现

分布式锁是在分布式系统中控制多个进程/服务互斥访问共享资源的机制,其核心目标是在并发场景下保证数据的正确性。下面从应用场景、设计目标、常见实现方案、关键细节四个方面,循序渐进讲解分布式锁的设计与实现。


1. 应用场景与设计目标

应用场景

  • 多台机器同时操作同一条数据库记录(如库存扣减)
  • 分布式任务调度,防止同一任务被多个节点重复执行
  • 对共享配置的并发更新保护

设计目标

  • 互斥性:任意时刻只有一个客户端能持有锁
  • 避免死锁:即使锁持有者崩溃,锁最终也能被释放
  • 高可用:锁服务本身需高可用(如集群部署)
  • 高性能:加锁、释放锁的操作延迟要低

2. 常见实现方案及其原理

常见的实现方案有:基于数据库、基于 Redis、基于 ZooKeeper/etcd。下面重点讲解最典型的 Redis 实现ZooKeeper 实现

方案一:基于 Redis 的分布式锁

这是最常用的方案,利用 Redis 的原子命令实现锁。

(1)基本加锁命令
最简单的加锁方式是使用 SET 命令的 NX(不存在才设置)和 EX(设置过期时间)选项:

SET lock_key unique_value NX EX 10
  • lock_key:锁的名称
  • unique_value:客户端生成的唯一值(如 UUID),用于安全释放锁,避免误删其他客户端的锁
  • NX:仅在键不存在时设置成功,实现互斥
  • EX 10:设置 10 秒过期时间,防止持有者崩溃后锁无法释放

(2)释放锁的逻辑
释放锁时,需要先检查锁的值是否为自己设置的 unique_value,是才删除。由于检查 + 删除需要原子性,通常用 Lua 脚本实现:

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

脚本将 KEYS[1](锁键)与 ARGV[1](预期值)比较,匹配才删除,避免误删。

(3)锁的续期问题
如果业务执行时间超过锁的过期时间,锁会自动释放,导致互斥失效。为此,可以引入看门狗(watchdog)机制

  • 客户端启动一个后台线程,定期(如过期时间的 1/3 时间)检查锁是否仍持有,并刷新过期时间(通过 EXPIRE 命令)
  • 业务执行完毕,主动停止续期并释放锁

(4)RedLock 算法
在单点 Redis 故障时,锁可能失效。RedLock 算法尝试在多个独立的 Redis 节点上同时获取锁,多数节点成功才算获取成功。其步骤如下:

  1. 获取当前时间戳 T1
  2. 依次向 N 个 Redis 节点发送加锁请求(使用相同键和值,设置较短的超时时间)
  3. 当从多数(≥ N/2 + 1)节点获得锁成功,且总耗时小于锁的过期时间,则认为加锁成功
  4. 锁的实际有效时间 = 初始过期时间 - 加锁总耗时
  5. 如果加锁失败,需向所有节点发送释放锁请求(即使未成功加锁的节点也尝试)

RedLock 仍有争议(如时钟漂移问题),但适用于对锁的可靠性要求较高的场景。


方案二:基于 ZooKeeper 的分布式锁

ZooKeeper 通过临时有序节点(Ephemeral Sequential Node)实现锁,更侧重一致性。

(1)加锁过程

  1. 客户端在锁对应的父节点(如 /locks/my_lock)下创建临时有序节点,如 /locks/my_lock/node_00000001
  2. 客户端获取父节点下所有子节点,并按序号排序
  3. 如果自己创建的节点是序号最小的节点,则获得锁
  4. 否则,监听前一个节点(比自己序号小 1 的节点)的删除事件
  5. 当前一个节点被删除(即锁被释放)时,自己被唤醒,重新检查自己是否成为最小节点

(2)释放锁

  • 客户端完成操作后,主动删除自己创建的临时节点(ZooKeeper 会自动清理,但主动删除可减少延迟)
  • 删除后,后一个节点会收到通知,尝试获取锁

(3)特性分析

  • 临时节点:客户端会话结束(如崩溃)时,节点自动删除,锁被释放,避免死锁
  • 有序节点:实现了公平锁(按申请顺序获得锁)
  • 监听机制:避免客户端不断轮询,减少网络开销

缺点:ZooKeeper 性能不如 Redis,且需要维护集群。


3. 关键细节与优化

(1)避免误删锁
必须用唯一标识(如 UUID + 线程 ID)作为锁的值,释放时验证,防止:

  • 客户端 A 因阻塞导致锁过期释放
  • 客户端 B 获得锁
  • 客户端 A 恢复后误删 B 的锁

(2)锁的可重入性
同一个线程可多次获取同一把锁,需在客户端维护重入计数(如 ThreadLocal),并在释放时递减计数,计数为 0 时才删除 Redis 中的锁键。

(3)锁的续期与超时
业务代码应尽量缩短锁占用时间,避免长时间持有锁。看门狗续期适用于执行时间不确定但较长的任务。

(4)网络分区与时钟问题
在 Redis 方案中,如果发生网络分区,可能出现多个客户端同时持有锁的情况(脑裂)。RedLock 试图缓解,但无法完全避免。ZooKeeper 基于 CP 设计,在网络分区时可能不可用,但不会出现多个锁。


4. 方案对比与选型

特性 Redis 锁 ZooKeeper 锁
一致性模型 AP(异步复制) CP(ZAB 协议)
性能 高(内存操作) 中(需持久化与同步)
实现复杂性 低(但 RedLock 较复杂) 中(需处理临时节点与监听)
锁是否公平 通常非公平(可结合队列实现公平) 公平(有序节点)
死锁处理 依赖过期时间 临时节点自动删除
典型应用场景 高并发、可容忍极短暂锁冲突 强一致性要求、锁持有时间长

选型建议

  • 对性能要求高、可容忍极端情况下少量锁冲突,选 Redis(多数业务场景)
  • 对锁的强一致性要求高、业务执行时间长,选 ZooKeeper/etcd(如金融核心交易)

5. 实战示例(Redis 锁的简单实现)

以下是一个使用 Redis 命令的 Python 伪代码示例(实际需用 redis 库):

import redis
import uuid
import time

class RedisDistributedLock:
    def __init__(self, redis_client, lock_key, expire_time=10):
        self.redis = redis_client
        self.lock_key = lock_key
        self.expire_time = expire_time
        self.identifier = str(uuid.uuid4())  # 唯一标识

    def acquire(self, timeout=10):
        end_time = time.time() + timeout
        while time.time() < end_time:
            # 尝试加锁
            if self.redis.set(self.lock_key, self.identifier, nx=True, ex=self.expire_time):
                return True
            time.sleep(0.001)  # 短暂等待后重试
        return False

    def release(self):
        # 使用 Lua 脚本保证原子性
        lua_script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """
        self.redis.eval(lua_script, 1, self.lock_key, self.identifier)

总结

分布式锁的核心是互斥、防死锁、高可用。Redis 锁性能高但需处理脑裂与续期;ZooKeeper 锁一致性强但性能较低。实际选型需权衡业务场景,并注意唯一标识、原子操作、锁续期等关键细节,避免常见坑点(如误删锁、锁过期等)。

分布式锁的设计与实现 分布式锁是在分布式系统中控制多个进程/服务 互斥访问共享资源 的机制,其核心目标是在并发场景下保证数据的正确性。下面从 应用场景、设计目标、常见实现方案、关键细节 四个方面,循序渐进讲解分布式锁的设计与实现。 1. 应用场景与设计目标 应用场景 : 多台机器同时操作同一条数据库记录(如库存扣减) 分布式任务调度,防止同一任务被多个节点重复执行 对共享配置的并发更新保护 设计目标 : 互斥性 :任意时刻只有一个客户端能持有锁 避免死锁 :即使锁持有者崩溃,锁最终也能被释放 高可用 :锁服务本身需高可用(如集群部署) 高性能 :加锁、释放锁的操作延迟要低 2. 常见实现方案及其原理 常见的实现方案有: 基于数据库、基于 Redis、基于 ZooKeeper/etcd 。下面重点讲解最典型的 Redis 实现 和 ZooKeeper 实现 。 方案一:基于 Redis 的分布式锁 这是最常用的方案,利用 Redis 的原子命令实现锁。 (1)基本加锁命令 最简单的加锁方式是使用 SET 命令的 NX (不存在才设置)和 EX (设置过期时间)选项: lock_key :锁的名称 unique_value :客户端生成的唯一值(如 UUID),用于安全释放锁,避免误删其他客户端的锁 NX :仅在键不存在时设置成功,实现互斥 EX 10 :设置 10 秒过期时间,防止持有者崩溃后锁无法释放 (2)释放锁的逻辑 释放锁时,需要先检查锁的值是否为自己设置的 unique_value ,是才删除。由于检查 + 删除需要原子性,通常用 Lua 脚本实现: 脚本将 KEYS[1] (锁键)与 ARGV[1] (预期值)比较,匹配才删除,避免误删。 (3)锁的续期问题 如果业务执行时间超过锁的过期时间,锁会自动释放,导致互斥失效。为此,可以引入 看门狗(watchdog)机制 : 客户端启动一个后台线程,定期(如过期时间的 1/3 时间)检查锁是否仍持有,并刷新过期时间(通过 EXPIRE 命令) 业务执行完毕,主动停止续期并释放锁 (4)RedLock 算法 在单点 Redis 故障时,锁可能失效。RedLock 算法尝试在多个独立的 Redis 节点上同时获取锁,多数节点成功才算获取成功。其步骤如下: 获取当前时间戳 T1 依次向 N 个 Redis 节点发送加锁请求(使用相同键和值,设置较短的超时时间) 当从多数(≥ N/2 + 1)节点获得锁成功,且总耗时小于锁的过期时间,则认为加锁成功 锁的实际有效时间 = 初始过期时间 - 加锁总耗时 如果加锁失败,需向所有节点发送释放锁请求(即使未成功加锁的节点也尝试) RedLock 仍有争议(如时钟漂移问题),但适用于对锁的可靠性要求较高的场景。 方案二:基于 ZooKeeper 的分布式锁 ZooKeeper 通过临时有序节点(Ephemeral Sequential Node)实现锁,更侧重一致性。 (1)加锁过程 客户端在锁对应的父节点(如 /locks/my_lock )下创建 临时有序节点 ,如 /locks/my_lock/node_00000001 客户端获取父节点下所有子节点,并按序号排序 如果自己创建的节点是序号最小的节点,则获得锁 否则,监听前一个节点(比自己序号小 1 的节点)的删除事件 当前一个节点被删除(即锁被释放)时,自己被唤醒,重新检查自己是否成为最小节点 (2)释放锁 客户端完成操作后,主动删除自己创建的临时节点(ZooKeeper 会自动清理,但主动删除可减少延迟) 删除后,后一个节点会收到通知,尝试获取锁 (3)特性分析 临时节点 :客户端会话结束(如崩溃)时,节点自动删除,锁被释放,避免死锁 有序节点 :实现了公平锁(按申请顺序获得锁) 监听机制 :避免客户端不断轮询,减少网络开销 缺点:ZooKeeper 性能不如 Redis,且需要维护集群。 3. 关键细节与优化 (1)避免误删锁 必须用唯一标识(如 UUID + 线程 ID)作为锁的值,释放时验证,防止: 客户端 A 因阻塞导致锁过期释放 客户端 B 获得锁 客户端 A 恢复后误删 B 的锁 (2)锁的可重入性 同一个线程可多次获取同一把锁,需在客户端维护重入计数(如 ThreadLocal),并在释放时递减计数,计数为 0 时才删除 Redis 中的锁键。 (3)锁的续期与超时 业务代码应尽量缩短锁占用时间,避免长时间持有锁。看门狗续期适用于执行时间不确定但较长的任务。 (4)网络分区与时钟问题 在 Redis 方案中,如果发生网络分区,可能出现多个客户端同时持有锁的情况(脑裂)。RedLock 试图缓解,但无法完全避免。ZooKeeper 基于 CP 设计,在网络分区时可能不可用,但不会出现多个锁。 4. 方案对比与选型 | 特性 | Redis 锁 | ZooKeeper 锁 | |------------------|-----------------------------|---------------------------| | 一致性模型 | AP(异步复制) | CP(ZAB 协议) | | 性能 | 高(内存操作) | 中(需持久化与同步) | | 实现复杂性 | 低(但 RedLock 较复杂) | 中(需处理临时节点与监听) | | 锁是否公平 | 通常非公平(可结合队列实现公平)| 公平(有序节点) | | 死锁处理 | 依赖过期时间 | 临时节点自动删除 | | 典型应用场景 | 高并发、可容忍极短暂锁冲突 | 强一致性要求、锁持有时间长 | 选型建议 : 对性能要求高、可容忍极端情况下少量锁冲突,选 Redis (多数业务场景) 对锁的强一致性要求高、业务执行时间长,选 ZooKeeper/etcd (如金融核心交易) 5. 实战示例(Redis 锁的简单实现) 以下是一个使用 Redis 命令的 Python 伪代码示例(实际需用 redis 库): 总结 分布式锁的核心是 互斥、防死锁、高可用 。Redis 锁性能高但需处理脑裂与续期;ZooKeeper 锁一致性强但性能较低。实际选型需权衡业务场景,并注意 唯一标识、原子操作、锁续期 等关键细节,避免常见坑点(如误删锁、锁过期等)。