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  机房管理员  阅读(49)  评论(0)    收藏  举报