分布式锁实现方案对比与Redlock算法工程实践

分布式锁微服务架构中控制共享资源访问的基础组件。从简单的Redis SETNX到Paxos共识算法,不同方案在一致性保证、性能开销和容错能力上各有取舍。本文对比Redis单节点锁、Redlock算法、Zookeeper锁和etcd锁的实现原理与工程实践。

Redis单节点分布式锁与可靠性问题

Redis分布式锁的最简实现使用SET key value NX PX命令,NX保证互斥性,PX设置过期时间防止死锁。但单节点Redis在主从故障切换时存在锁丢失风险。

import redis
import uuid
import time

class RedisDistributedLock:
    def __init__(self, redis_client, lock_key, expire_seconds=30):
        self.redis = redis_client
        self.lock_key = lock_key
        self.expire_seconds = expire_seconds
        self.lock_value = str(uuid.uuid4())

    def acquire(self, retry_count=3, retry_delay=0.2):
        for attempt in range(retry_count):
            result = self.redis.set(
                self.lock_key, self.lock_value,
                nx=True, px=int(self.expire_seconds * 1000)
            )
            if result:
                return True
            time.sleep(retry_delay)
        return False

    def release(self):
        release_script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """
        return self.redis.eval(release_script, 1, self.lock_key, self.lock_value)

    def __enter__(self):
        if not self.acquire():
            raise Exception(f"Failed to acquire lock: {self.lock_key}")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()

# 使用示例
r = redis.Redis(host='localhost', port=6379, db=0)
with RedisDistributedLock(r, "order:lock:1001", expire_seconds=10):
    process_order(1001)

单节点Redis锁的缺陷:主从异步复制场景下,客户端A在Master获取锁后,锁信息尚未同步到Slave时Master宕机,Sentinel提升Slave为新Master,客户端B获取同一把锁成功,导致互斥性被破坏。

Redlock算法多节点仲裁机制

Redis作者Antirez提出的Redlock算法通过多节点仲裁解决单点故障问题。核心思路:在N个独立Redis实例上同时获取锁,超过半数成功即认为锁获取成功。

import redis
import uuid
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

class Redlock:
    def __init__(self, redis_nodes, retry_count=3, retry_delay=0.2):
        self.clients = [
            redis.Redis(host=h, port=p, socket_timeout=0.1)
            for h, p in redis_nodes
        ]
        self.quorum = len(redis_nodes) // 2 + 1
        self.retry_count = retry_count
        self.retry_delay = retry_delay

    def acquire_instance(self, client, lock_key, lock_value, expire_ms):
        try:
            return client.set(lock_key, lock_value, nx=True, px=expire_ms)
        except Exception:
            return False

    def release_instance(self, client, lock_key, lock_value):
        release_script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """
        try:
            client.eval(release_script, 1, lock_key, lock_value)
        except Exception:
            pass

    def acquire(self, lock_key, expire_seconds=10):
        lock_value = str(uuid.uuid4())
        expire_ms = int(expire_seconds * 1000)

        for attempt in range(self.retry_count):
            start_time = time.time()
            success_count = 0

            with ThreadPoolExecutor(max_workers=len(self.clients)) as executor:
                futures = [
                    executor.submit(
                        self.acquire_instance,
                        client, lock_key, lock_value, expire_ms
                    )
                    for client in self.clients
                ]
                for future in as_completed(futures):
                    if future.result():
                        success_count += 1

            elapsed = (time.time() - start_time) * 1000
            validity_time = expire_ms - elapsed

            if success_count >= self.quorum and validity_time > 0:
                return {'value': lock_value, 'validity_time': validity_time}

            for client in self.clients:
                self.release_instance(client, lock_key, lock_value)
            time.sleep(self.retry_delay)

        return None

    def release(self, lock_key, lock_info):
        for client in self.clients:
            self.release_instance(client, lock_key, lock_info['value'])

# 使用 - 5个独立Redis实例
nodes = [
    ('redis1.example.com', 6379),
    ('redis2.example.com', 6379),
    ('redis3.example.com', 6379),
    ('redis4.example.com', 6379),
    ('redis5.example.com', 6379),
]
redlock = Redlock(nodes)
lock = redlock.acquire("inventory:deduct:sku_1001", expire_seconds=10)
if lock:
    try:
        check_and_deduct_inventory("sku_1001")
    finally:
        redlock.release("inventory:deduct:sku_1001", lock)

Zookeeper分布式锁与Watch机制

Zookeeper通过临时顺序节点和Watch机制实现分布式锁,天然具备会话超时自动释放和公平排队特性。与Redis的主动过期不同,Zookeeper锁依赖session心跳维持。

from kazoo.client import KazooClient
from kazoo.exceptions import NoNodeError
import threading

class ZkDistributedLock:
    def __init__(self, hosts, lock_path, timeout=10):
        self.zk = KazooClient(hosts=hosts, timeout=timeout)
        self.lock_path = lock_path
        self.node_path = None

    def connect(self):
        self.zk.start()

    def acquire(self, blocking=True, timeout=None):
        self.zk.ensure_path(self.lock_path)
        self.node_path = self.zk.create(
            f"{self.lock_path}/lock-",
            ephemeral=True,
            sequence=True
        )
        node_name = self.node_path.split('/')[-1]

        while True:
            children = self.zk.get_children(self.lock_path)
            children.sort()
            my_index = children.index(node_name)

            if my_index == 0:
                return True

            prev_node = f"{self.lock_path}/{children[my_index - 1]}"
            event = threading.Event()

            @self.zk.DataWatch(prev_node)
            def watch_node(data, stat):
                if stat is None:
                    event.set()

            if self.zk.exists(prev_node):
                event.wait(timeout=timeout)
                if not event.is_set():
                    self.zk.delete(self.node_path)
                    return False

    def release(self):
        if self.node_path:
            try:
                self.zk.delete(self.node_path)
            except NoNodeError:
                pass
            self.node_path = None

    def close(self):
        self.zk.stop()
        self.zk.close()

# 使用示例
zk_lock = ZkDistributedLock("zk1:2181,zk2:2181,zk3:2181", "/locks/payment")
zk_lock.connect()
if zk_lock.acquire(timeout=5):
    try:
        process_payment()
    finally:
        zk_lock.release()
zk_lock.close()

分布式锁方案选型对比与工程建议

三种主流方案的横向对比:

维度         Redis单节点      Redlock           Zookeeper         etcd
一致性       弱(主从异步)     多数派仲裁         ZAB共识(CP)       Raft共识(CP)
性能         最优(~10k QPS)  良好(~1k QPS)     中等(~500 QPS)    良好(~1k QPS)
可用性       AP              AP偏向            CP                CP
自动续期     需手动实现      需手动实现         Session心跳自动    Lease自动续期
公平性       非公平          非公平             公平(顺序节点)    非公平
故障恢复     锁可能丢失      少数节点故障可容忍 Leader故障短暂不可 Leader故障短暂不可
部署复杂度   低              高(需5+独立实例)  中(3-5节点集群)   中(3-5节点集群)
适用场景     缓存防击穿      资金交易           配置分发/选主     服务发现/配置管理

工程实践中,分布式锁的选择应遵循”够用即可”原则:

缓存防击穿、限流计数等容忍极小概率不一致的场景,使用Redis单节点锁即可,配合watchdog自动续期机制(如Redisson的LockWatchdogTimeout)避免业务执行超时导致锁过期。

资金扣减、库存扣减等强一致性要求的场景,优先选择基于Raft/ZAB共识的etcd或Zookeeper锁。Redlock在网络分区下的安全性存在争议(Martin Kleppmann的批评),在极端情况下仍可能出现锁失效,不建议用于最严格的资金场景。

所有分布式锁都应配合数据库唯一约束作为兜底方案。分布式锁解决的是”减少冲突”,数据库约束解决的是”最终正确性”。两层防护才能在分布式环境下实现既高性能又正确的并发控制。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/fen-bu-shi-suo-shi-xian-fang-an-dui-bi-yu-redlock-suan-fa/

(0)
小编小编
上一篇 7小时前
下一篇 7小时前

相关推荐