分布式锁实战:Redis、ZooKeeper、etcd 的方案对比与工程陷阱
分布式系统中,多个服务同时修改同一资源,不加控制就会出乱子:库存超卖、订单重复、余额错误……分布式锁就是解决这类问题的经典方案。但分布式锁的实现远比想象中复杂:Redis 的 Redlock 算法是否安全?ZooKeeper 的临时节点是否可靠?etcd 的租约机制是否适用?选择哪个?怎么避免死锁?这些问题没有标准答案,只有适合你场景的权衡。
二、分布式锁的核心需求与实现方案对比
一个可靠的分布式锁需要满足以下核心需求:
graph TB
A[分布式锁实现方案] –> B[基于Redis<br/>SET NX PX]
A –> C[基于ZooKeeper<br/>临时顺序节点]
A –> D[基于etcd<br/>租约机制]
A –> E[基于数据库<br/>唯一约束]
B –> B1[优点: 性能高, 简单<br/>缺点: 时钟漂移, 主从切换问题]
C –> C1[优点: 可靠, 顺序公平<br/>缺点: 性能较低, 依赖ZK]
D –> D1[优点: 可靠, 租约自动过期<br/>缺点: 性能中等, 依赖etcd]
E –> E1[优点: 无需额外依赖<br/>缺点: 性能差, 单点瓶颈]
style B fill:#e1f5fe
style C fill:#fff3e0
style D fill:#e8f5e9
style E fill:#f3e5f5
基于 Redis 的分布式锁是最流行的方案。核心命令是 SET lock_key unique_value NX PX 30000:
- NX:只在 key 不存在时设置(互斥性)。
- PX 30000:设置过期时间 30 秒(死锁预防)。
- unique_value:客户端唯一标识,解锁时验证(避免误删)。
但 Redis 分布式锁有经典问题:
Redlock 算法试图解决主从切换问题,通过向 N 个独立 Redis 节点申请锁,超过半数成功才加锁成功。但 Redlock 也面临时钟漂移和 GC 停顿的挑战。
基于 ZooKeeper 的分布式锁使用临时顺序节点(Ephemeral Sequential Node):
ZooKeeper 锁的优点是可靠性高(ZAB 协议保证一致性),且支持公平锁(按序号顺序获取)。缺点是性能较低(每次操作都需要写事务),且依赖 ZooKeeper 集群。
基于 etcd 的分布式锁使用租约(Lease)机制:
etcd 锁的优点是可靠性高(Raft 协议保证一致性),且租约机制天然支持自动释放。缺点是性能中等,且依赖 etcd 集群。
三、Redis 分布式锁的生产级实现
Redis 分布式锁的生产级实现需要考虑多个边界情况:
正确的加锁实现
import redis
import uuid
import time
class RedisDistributedLock:
def __init__(self, redis_client, lock_key, expire_ms=30000):
self.redis = redis_client
self.lock_key = lock_key
self.expire_ms = expire_ms
self.lock_value = str(uuid.uuid4()) # 唯一标识
self._locked = False
def acquire(self, timeout_ms=10000):
"""获取锁,timeout_ms 内重试"""
start_time = time.time() * 1000
while True:
# 尝试加锁
acquired = self.redis.set(
self.lock_key,
self.lock_value,
nx=True,
px=self.expire_ms
)
if acquired:
self._locked = True
return True
# 超时退出
if time.time() * 1000 – start_time > timeout_ms:
return False
# 等待后重试
time.sleep(0.1)
def release(self):
"""释放锁(使用 Lua 脚本保证原子性)"""
if not self._locked:
return False
# Lua 脚本:只有 value 匹配时才删除(避免误删)
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
result = self.redis.eval(lua_script, 1, self.lock_key, self.lock_value)
self._locked = False
return result == 1
def extend(self, additional_ms):
"""续期锁(使用 Lua 脚本保证原子性)"""
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end
"""
result = self.redis.eval(
lua_script,
1,
self.lock_key,
self.lock_value,
self.expire_ms + additional_ms
)
return result == 1
# 支持上下文管理器
def __enter__(self):
self.acquire()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.release()
使用示例
# 方式1:手动加锁解锁
lock = RedisDistributedLock(redis_client, "order:lock:12345")
if lock.acquire(timeout_ms=5000):
try:
# 执行业务逻辑
process_order(12345)
finally:
lock.release()
else:
print("获取锁超时")
# 方式2:上下文管理器(推荐)
with RedisDistributedLock(redis_client, "order:lock:12345") as lock:
if lock._locked:
process_order(12345)
else:
print("获取锁超时")
Redlock 的实现(使用 redis-py-cluster)
from redis import Redis
from redis.cluster import ClusterNode, RedisCluster
class Redlock:
def __init__(self, nodes, retry_count=3, retry_delay=0.2):
self.nodes = nodes # Redis 节点列表
self.retry_count = retry_count
self.retry_delay = retry_delay
self.quorum = len(nodes) // 2 + 1 # 多数派
def lock(self, resource, ttl_ms):
"""获取 Redlock"""
lock_value = str(uuid.uuid4())
for attempt in range(self.retry_count):
acquired_nodes = 0
start_time = time.time() * 1000
# 向所有节点申请锁
for node in self.nodes:
try:
# 使用 SET NX PX 命令
result = node.set(resource, lock_value, nx=True, px=ttl_ms)
if result:
acquired_nodes += 1
except Exception:
pass # 节点故障,继续
# 计算耗时
elapsed_time = time.time() * 1000 – start_time
# 判断是否成功:多数派 + 耗时小于 TTL
if acquired_nodes >= self.quorum and elapsed_time < ttl_ms:
return {
"resource": resource,
"value": lock_value,
"validity_time": ttl_ms – elapsed_time
}
# 失败:释放所有节点的锁
self.unlock(resource, lock_value)
# 等待后重试
time.sleep(self.retry_delay)
return False
def unlock(self, resource, value):
"""释放 Redlock"""
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
for node in self.nodes:
try:
node.eval(lua_script, 1, resource, value)
except Exception:
pass # 节点故障,继续
四、分布式锁的性能优化与监控
分布式锁的性能瓶颈通常在于网络延迟和锁竞争。优化策略包括:
graph LR
A[性能优化] –> B[锁粒度优化<br/>减少锁持有时间]
A –> C[分段锁<br/>提升并发度]
A –> D[本地缓存<br/>减少远程调用]
A –> E[异步处理<br/>非关键路径异步化]
B –> B1[只锁关键代码]
C –> C1[不同资源独立加锁]
D –> D1[热点数据本地缓存]
E –> E1[消息队列解耦]
style B fill:#e1f5fe
style C fill:#fff3e0
style D fill:#e8f5e9
style E fill:#f3e5f5
锁粒度优化:尽量缩小锁的保护范围,只锁真正需要互斥的代码段。
# 不好的做法:锁住整个函数
def process_order_bad(order_id):
with RedisDistributedLock(redis_client, f"order:{order_id}"):
# 读取订单
order = db.get_order(order_id)
# 检查库存
check_inventory(order)
# 更新订单
db.update_order(order)
# 发送通知(不需要锁)
send_notification(order)
# 好的做法:只锁更新操作
def process_order_good(order_id):
# 读取订单(无锁)
order = db.get_order(order_id)
# 检查库存(无锁)
check_inventory(order)
# 只锁更新操作
with RedisDistributedLock(redis_client, f"order:{order_id}") as lock:
if lock._locked:
db.update_order(order)
# 发送通知(无锁)
send_notification(order)
分段锁:将一个大锁拆分为多个小锁,提升并发度。
def get_segment_lock_key(resource_id, segments=16):
"""根据资源 ID 计算分段锁 key"""
segment = hash(resource_id) % segments
return f"lock:{segment}:{resource_id}"
# 使用:不同分段可以并行
lock_key = get_segment_lock_key(order_id, segments=16)
with RedisDistributedLock(redis_client, lock_key):
process_order(order_id)
监控关键指标:
# 监控实现示例
class LockMetrics:
def __init__(self):
self.acquire_success = 0
self.acquire_timeout = 0
self.acquire_latency = []
def record_acquire(self, success, latency_ms):
if success:
self.acquire_success += 1
else:
self.acquire_timeout += 1
self.acquire_latency.append(latency_ms)
def get_stats(self):
return {
"success_rate": self.acquire_success / (self.acquire_success + self.acquire_timeout),
"avg_latency_ms": sum(self.acquire_latency) / len(self.acquire_latency)
}
五、分布式锁的暗面与工程陷阱
分布式锁引入的复杂度往往被低估。以下是常见的工程陷阱:
陷阱一:锁的误删
如果客户端 A 持有锁,但处理时间过长,锁自动过期。客户端 B 随后获取锁。这时候客户端 A 处理完成,执行 DEL lock_key,就会误删客户端 B 的锁。解决方案是使用唯一 value + Lua 脚本原子删除(如前文代码所示)。
陷阱二:GC 停顿导致的锁过期
Java 应用的 Full GC 可能停顿数秒,远超锁的过期时间。这时候锁已经自动释放,但客户端还在处理,导致多个客户端同时进入临界区。解决方案是:
陷阱三:时钟漂移导致的问题
如果 Redis 服务器的时间跳变(如 NTP 同步),可能导致锁的过期时间不准确。Redlock 算法对时钟漂移敏感。解决方案是:
陷阱四:锁的公平性问题
Redis 分布式锁是非公平锁(先到先得),可能导致某些客户端长期获取不到锁。如果需要公平锁,使用 ZooKeeper 的临时顺序节点。
工程决策框架:
| 高性能要求,可容忍低概率失败 | Redis 分布式锁 | 性能最高 |
| 高可靠性要求,不能容忍失败 | ZooKeeper 或 etcd 锁 | 强一致性 |
| 简单场景,低并发 | 数据库唯一约束 | 实现最简单 |
| 需要公平锁 | ZooKeeper 锁 | 支持顺序节点 |
独立开发者的实用主义建议:
深夜的架构图终于完整,咖啡也凉了。分布式锁不是银弹,它只是解决特定并发问题的工具。真正重要的,是理解你的业务需求,选择合适的锁方案,并在性能、可靠性、复杂度之间找到平衡点。毕竟,技术的终极目标,是解决问题,而不是增加复杂度。



