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集群)

总结

分布式锁的正确姿势:

  1. 大多数场景用Redis SET NX EX:简单、快速、够用
  2. 释放锁必须用Lua脚本:检查value再删除,防止误解锁
  3. 长任务必须续期:否则锁过期后并发问题又回来了
  4. 金融级用Redlock/ZooKeeper:不差那几毫秒延迟

MonkeyCode能帮你选型、生成代码、写并发测试,一站式搞定分布式锁。

posted @ 2026-05-29 21:49  机房管理员  阅读(25)  评论(0)    收藏  举报