MonkeyCode实现分布式锁:从Redis到ZooKeeper的完整实战
分布式系统最怕两件事:数据不一致和并发写冲突。分布式锁就是解决后者的银弹——前提是你用对了。
为什么需要分布式锁?
场景1:库存超卖
# ❌ 没有锁:两个请求同时扣库存,结果变负数
async def deduct_inventory(sku: str, qty: int):
stock = await db.fetch_val("SELECT stock FROM inventory WHERE sku = ?", sku)
if stock >= qty:
await db.execute("UPDATE inventory SET stock = stock - ? WHERE sku = ?", qty, sku)
return True
return False
# 并发请求同时读到stock=10,都通过判断,最终stock=-10(超卖!)
场景2:定时任务重复执行
两台服务器同时跑Cron → 客户收到2封日报邮件
Redis分布式锁(最常用方案)
最简单的锁:SET NX EX
import redis.asyncio as redis
import uuid
class RedisDistributedLock:
def __init__(self, redis_url="redis://localhost:6379"):
self.redis_url = redis_url
self._r = None
async def _get_conn(self):
if not self._r:
self._r = await redis.from_url(self.redis_url)
return self._r
async def acquire(self, lock_key: str, timeout: int = 30, retry_times: int = 3, retry_delay: float = 0.1) -> str | None:
"""获取锁,返回唯一标识(用于安全释放),失败返回None"""
r = await self._get_conn()
identifier = str(uuid.uuid4())
for _ in range(retry_times):
# SET key value NX EX timeout —— 原子操作
result = await r.set(lock_key, identifier, nx=True, ex=timeout)
if result:
return identifier
await asyncio.sleep(retry_delay)
return None
async def release(self, lock_key: str, identifier: str) -> bool:
"""安全释放锁:用Lua脚本保证「检查+删除」原子性"""
r = await self._get_conn()
# Lua脚本:只有value匹配才删除,防止误解别人的锁
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
result = await r.eval(lua_script, 1, lock_key, identifier)
return result == 1
async def extend(self, lock_key: str, identifier: str, additional_time: int = 30) -> bool:
"""续期锁(长时间任务需要)"""
r = await self._get_conn()
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
else
return 0
end
"""
result = await r.eval(lua_script, 1, lock_key, identifier, str(additional_time))
return result == 1
实战:用锁防止库存超卖
lock = RedisDistributedLock()
async def safe_deduct_inventory(sku: str, qty: int) -> bool:
lock_key = f"inventory_lock:{sku}"
identifier = await lock.acquire(lock_key, timeout=10)
if not identifier:
raise Exception(f"获取库存锁失败: {sku}")
try:
stock = await db.fetch_val("SELECT stock FROM inventory WHERE sku = ?", sku)
if stock < qty:
return False
await db.execute("UPDATE inventory SET stock = stock - ? WHERE sku = ?", qty, sku)
return True
finally:
await lock.release(lock_key, identifier)
实战:防止定时任务重复执行
import asyncio
from datetime import datetime
async def run_daily_report():
"""每天0点执行,多实例只运行一次"""
today = datetime.now().strftime("%Y-%m-%d")
lock_key = f"cron:daily_report:{today}"
identifier = await lock.acquire(lock_key, timeout=3600) # 1小时超时
if not identifier:
print("今日报告已在其他实例执行")
return
try:
await generate_daily_report()
await send_report_email()
finally:
await lock.release(lock_key, identifier)
长时间任务:自动续期
async def long_running_task_with_lock(lock_key: str):
identifier = await lock.acquire(lock_key, timeout=30)
if not identifier:
raise Exception("获取锁失败")
async def renew_loop():
"""后台协程每10秒续期一次"""
while True:
await asyncio.sleep(10)
extended = await lock.extend(lock_key, identifier, 30)
if not extended:
print("续期失败,锁可能已被其他人获取")
break
renew_task = asyncio.create_task(renew_loop())
try:
await do_long_work() # 可能运行几分钟
finally:
renew_task.cancel()
await lock.release(lock_key, identifier)
Redlock算法(Redis集群场景)
单节点Redis有单点故障风险。Redlock用多节点投票保证可靠性:
import time
class RedLock:
def __init__(self, redis_nodes: list[str], quorum: int = None):
"""
redis_nodes: 多个Redis实例地址
quorum: 多数派数量,默认 (len(nodes) // 2) + 1
"""
self.nodes = redis_nodes
self.quorum = quorum or (len(redis_nodes) // 2 + 1)
async def acquire(self, lock_key: str, timeout: int = 30) -> str | None:
identifier = str(uuid.uuid4())
start_time = time.time()
acquired = 0
for node_url in self.nodes:
try:
r = await redis.from_url(node_url)
result = await r.set(lock_key, identifier, nx=True, px=timeout * 1000)
if result:
acquired += 1
except Exception:
continue
# 检查是否获得多数派
elapsed_ms = (time.time() - start_time) * 1000
if acquired >= self.quorum and elapsed_ms < timeout * 1000:
return identifier
# 获取失败,释放所有已获得的锁
for node_url in self.nodes:
try:
r = await redis.from_url(node_url)
lua = 'if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end'
await r.eval(lua, 1, lock_key, identifier)
except Exception:
continue
return None
ZooKeeper分布式锁(最严谨方案)
ZooKeeper通过临时顺序节点实现公平锁:
from kazoo.client import KazooClient
class ZooKeeperDistributedLock:
def __init__(self, hosts="localhost:2181", lock_path="/locks"):
self.zk = KazooClient(hosts=hosts)
self.lock_path = lock_path
self.lock = None
def connect(self):
self.zk.start()
def acquire(self, lock_name: str, timeout: float = None) -> bool:
"""获取锁(阻塞等待)"""
lock_path = f"{self.lock_path}/{lock_name}"
self.lock = self.zk.Lock(lock_path)
return self.lock.acquire(timeout=timeout)
def release(self):
"""释放锁"""
if self.lock:
self.lock.release()
self.lock = None
def close(self):
self.zk.stop()
# 使用
zk_lock = ZooKeeperDistributedLock()
zk_lock.connect()
if zk_lock.acquire("inventory_sku001"):
try:
deduct_inventory("SKU001", 5)
finally:
zk_lock.release()
三种方案对比
| 特性 | Redis SET NX | Redlock | ZooKeeper |
|---|---|---|---|
| 可靠性 | 中(单点故障) | 高(多节点投票) | 最高(ZAB协议) |
| 性能 | 极高(微秒级) | 高(毫秒级) | 中(10ms级) |
| 公平性 | 非公平(抢占式) | 非公平 | 公平(顺序节点) |
| 自动续期 | 需手动实现 | 需手动实现 | 临时节点自动释放 |
| 运维成本 | 低 | 中(多Redis实例) | 高(ZK集群) |
| 适用场景 | 绝大多数业务 | 金融级可靠性 | 强一致性需求 |
常见陷阱
陷阱1:锁过期但任务没完成
线程A获取锁(30秒超时)→ 任务运行了35秒 → 锁在第30秒自动释放
线程B获取到锁 → 线程A在第35秒释放了线程B的锁!
解决方案:释放时检查identifier + 长任务做续期
陷阱2:GC Pause导致锁失效
JVM Full GC暂停5秒 → Redis锁在5秒内过期 → 另一个线程获取到锁
GC恢复后两个线程同时持有"锁"
解决方案:对一致性要求极高的场景用Redlock或ZooKeeper
陷阱3:误删别人的锁
线程A的锁过期 → 线程B获取锁 → 线程A执行完调DEL → 删了B的锁!
线程C又获取到锁 → B和C同时持有
解决方案:用Lua脚本检查value再删除(已在代码中实现)
MonkeyCode Prompt模板
我的[Python/Go]服务需要分布式锁,场景是[库存扣减/定时任务/订单处理]。
请帮我:
1. 选择方案(Redis/Redlock/ZooKeeper),给出理由
2. 生成完整的锁实现代码(含安全释放、自动续期)
3. 集成到我的业务代码中
4. 编写并发测试(模拟100个请求同时竞争)
5. 配置Docker Compose(Redis/ZK集群)
总结
分布式锁的正确姿势:
- 大多数场景用Redis SET NX EX:简单、快速、够用
- 释放锁必须用Lua脚本:检查value再删除,防止误解锁
- 长任务必须续期:否则锁过期后并发问题又回来了
- 金融级用Redlock/ZooKeeper:不差那几毫秒延迟
MonkeyCode能帮你选型、生成代码、写并发测试,一站式搞定分布式锁。

浙公网安备 33010602011771号