分布式锁是微服务架构中控制共享资源访问的核心机制。电商场景下的扣减库存、金融场景下的转账操作、定时任务防重复执行,都需要分布式锁保证原子性。Redis和ZooKeeper是两种主流实现方案,各有适用场景。本文涵盖单机Redis锁、Redlock算法、ZooKeeper分布式锁的实现细节和选型方案。
单机Redis分布式锁与常见陷阱
Redis分布式锁的基本思路是利用SET key value NX PX实现原子性的”不存在则设置并设置过期时间”。value使用唯一标识(如UUID),释放锁时校验标识防止误删他人持有的锁。
import redis
import uuid
import time
class RedisLock:
def __init__(self, client, lock_key, expire_seconds=30):
self.client = client
self.lock_key = lock_key
self.expire = expire_seconds
self.identifier = str(uuid.uuid4())
def acquire(self, retry_count=3, retry_delay=0.2):
for _ in range(retry_count):
result = self.client.set(
self.lock_key, self.identifier,
nx=True, px=self.expire * 1000
)
if result:
return True
time.sleep(retry_delay)
return False
def release(self):
lua_script = (
"if redis.call('get', KEYS[1]) == ARGV[1] then "
"return redis.call('del', KEYS[1]) "
"else return 0 end"
)
return self.client.eval(lua_script, 1, self.lock_key, self.identifier)
# 使用示例
lock = RedisLock(r, "order:lock:10001", expire_seconds=10)
if lock.acquire():
try:
deduct_stock(order_id)
finally:
lock.release()
else:
raise Exception("获取锁失败,请重试")
单机Redis锁存在两个核心问题:锁过期但业务未执行完导致并发问题;Redis主从故障切换可能导致锁丢失。第一个问题通过看门狗(Watchdog)续期解决——后台线程定期延长锁的过期时间。Redisson框架内置了看门狗机制,默认每10秒续期,过期时间30秒。第二个问题是单点故障的本质限制,Redlock算法试图解决。
Redlock算法实现与争议分析
Redlock是Redis作者Antirez提出的多节点分布式锁算法,核心思想是向多个独立Redis实例同时申请锁,多数成功即视为加锁成功。典型部署使用5个独立Redis实例。
class Redlock:
def __init__(self, redis_nodes, retry_count=3, retry_delay=200):
self.clients = [redis.Redis(**node) for node in redis_nodes]
self.retry_count = retry_count
self.retry_delay = retry_delay / 1000
self.quorum = len(self.clients) // 2 + 1
def lock_instance(self, client, lock_key, identifier, expire_ms):
return client.set(lock_key, identifier, nx=True, px=expire_ms)
def unlock_instance(self, client, lock_key, identifier):
lua = (
"if redis.call('get', KEYS[1]) == ARGV[1] then "
"return redis.call('del', KEYS[1]) "
"else return 0 end"
)
try:
client.eval(lua, 1, lock_key, identifier)
except:
pass
def acquire(self, lock_key, expire_ms=10000):
identifier = str(uuid.uuid4())
for _ in range(self.retry_count):
success_count = 0
start_time = time.time()
for client in self.clients:
try:
if self.lock_instance(client, lock_key, identifier, expire_ms):
success_count += 1
except:
continue
elapsed = (time.time() - start_time) * 1000
if success_count >= self.quorum and elapsed < expire_ms:
return identifier
for client in self.clients:
self.unlock_instance(client, lock_key, identifier)
time.sleep(self.retry_delay)
return None
def release(self, lock_key, identifier):
for client in self.clients:
self.unlock_instance(client, lock_key, identifier)
nodes = [
{"host": "10.0.1.1", "port": 6379},
{"host": "10.0.1.2", "port": 6379},
{"host": "10.0.1.3", "port": 6379},
{"host": "10.0.1.4", "port": 6379},
{"host": "10.0.1.5", "port": 6379},
]
lock = Redlock(nodes)
identifier = lock.acquire("inventory:lock:sku-001", expire_ms=10000)
if identifier:
try:
process_order()
finally:
lock.release("inventory:lock:sku-001", identifier)
Redlock的争议点在于其安全性是否足够。Martin Kleppmann在论文中指出Redlock依赖时钟同步,在GC暂停或时钟漂移场景下可能失效。Antirez反驳认为这些极端场景下任何分布式锁都不安全。实际工程中的建议:如果业务对正确性要求极高(金融转账),应使用ZooKeeper或etcd等基于一致性共识算法的方案。如果业务可容忍极低概率的锁失效(如商品库存偶尔超卖后人工补偿),Redis锁足够高效。
ZooKeeper临时顺序节点实现分布式锁
ZooKeeper基于ZAB一致性协议,天然适合实现可重入、公平的分布式锁。核心方案是临时顺序节点:客户端在锁节点下创建临时顺序子节点,序号最小的获得锁,其余客户端监听前一个节点删除事件。
from kazoo.client import KazooClient
import uuid
class ZkLock:
def __init__(self, hosts, lock_path):
self.zk = KazooClient(hosts=hosts)
self.lock_path = lock_path
self.node_name = str(uuid.uuid4())[:8]
self.locked_node = None
def connect(self):
self.zk.start()
def acquire(self, timeout=None):
self.zk.ensure_path(self.lock_path)
self.locked_node = self.zk.create(
f"{self.lock_path}/lock-",
value=self.node_name.encode(),
ephemeral=True,
sequence=True
)
while True:
children = self.zk.get_children(self.lock_path)
children.sort()
current_node = self.locked_node.split('/')[-1]
if children[0] == current_node:
return True
prev_index = children.index(current_node) - 1
prev_node = f"{self.lock_path}/{children[prev_index]}"
event = self.zk.handler.event_object()
self.zk.exists(prev_node, watch=lambda x: event.set())
if not event.wait(timeout):
self.zk.delete(self.locked_node)
self.locked_node = None
return False
def release(self):
if self.locked_node:
self.zk.delete(self.locked_node)
self.locked_node = None
def close(self):
self.zk.stop()
lock = ZkLock("10.0.1.10:2181,10.0.1.11:2181,10.0.1.12:2181", "/locks/order")
lock.connect()
if lock.acquire(timeout=30):
try:
process_payment()
finally:
lock.release()
lock.close()
ZooKeeper锁的核心优势在于锁的可靠性不依赖过期时间。临时节点在客户端会话断开时自动删除,不存在锁泄漏问题。顺序节点保证公平性,先到先得。缺点是每次加锁需要多次ZooKeeper交互(创建节点、获取子节点列表、设置监听),延迟比Redis高1-2个数量级。在锁竞争激烈场景下需要关注羊群效应——使用顺序节点而非监听父节点变更,天然规避此问题。
分布式锁方案选型与生产环境实践
三种方案对比:
- 单机Redis锁:延迟最低(1ms内),单点故障风险,适合非关键业务和单机房部署
- Redlock:延迟较低(5-10ms),需要5节点部署成本,安全性存在争议,适合业务可容忍极端场景失效的情况
- ZooKeeper锁:延迟较高(10-50ms),可靠性最高,适合金融级严格互斥场景
生产环境建议:优先使用Redisson框架的RLock,内置看门狗续期和可重入支持,单机Redis满足多数业务场景。对于跨机房部署,使用Redlock或切换到etcd(Raft协议共识,性能优于ZooKeeper)。定时任务防重复执行场景,可以使用一条简单的Redis SET NX指令实现,无需复杂方案。锁的粒度应尽量细——按用户ID或订单ID加锁而非全局锁,减少锁竞争。锁等待超时时间需合理设置,避免线程长时间阻塞影响吞吐量。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/fen-bu-shi-suo-shi-xian-shi-zhan-redisredlock-suan-fa-yu/