分布式锁实现实战:Redis Redlock算法与ZooKeeper临时节点方案对比

分布式锁是微服务架构中控制共享资源访问的核心机制。电商场景下的扣减库存、金融场景下的转账操作、定时任务防重复执行,都需要分布式锁保证原子性。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/

赞 (0)
小编小编
上一篇 2026年9月18日
下一篇 2026年9月18日

相关推荐

分布式锁实现实战:Redis RedLock算法与ZooKeeper方案对比与选型

分布式锁是微服务架构中解决并发资源竞争的核心机制。在库存扣减、订单创建、定时任务去重等高并发设计场景中,单机锁无法满足跨进程跨节点的互斥需求。Redis和ZooKeeper是两种主流的分布式锁实现方案,前者追求高性能和低延迟,后者强调一致性和可靠性。理解两种方案的原理和适用边界,是后端开发中服务治理能力的基本要求。

Redis单节点分布式锁实现与SETNX原理解析

Redis分布式锁最基础的实现是SET key value NX EX命令。NX保证只有一个客户端能设置成功,EX设置过期时间防止持锁客户端宕机后死锁。解锁操作必须使用Lua脚本保证原子性,避免误删其他客户端的锁。

import redis
import uuid
import time

class RedisDistributedLock:
    def __init__(self, redis_client, lock_key, expire_time=30):
        self.redis = redis_client
        self.lock_key = lock_key
        self.expire_time = expire_time
        self.lock_value = str(uuid.uuid4())  # 唯一标识当前持有者

    def acquire(self, retry_count=3, retry_delay=0.1):
        '''获取锁'''
        for i in range(retry_count):
            # SET key value NX EX(原子操作)
            result = self.redis.set(
                self.lock_key,
                self.lock_value,
                nx=True,
                ex=self.expire_time
            )
            if result:
                return True
            time.sleep(retry_delay)
        return False

    def release(self):
        '''释放锁(Lua脚本保证原子性)'''
        # 只有锁的持有者才能释放,避免误删
        lua_script = '''
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        '''
        result = self.redis.eval(lua_script, 1, self.lock_key, self.lock_value)
        return result == 1

    def renew(self):
        '''续期锁(防止业务执行时间超过锁过期时间)'''
        lua_script = '''
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("expire", KEYS[1], ARGV[2])
        else
            return 0
        end
        '''
        result = self.redis.eval(
            lua_script, 1, self.lock_key,
            self.lock_value, self.expire_time
        )
        return result == 1

# 使用示例
r = redis.Redis(host='localhost', port=6379, db=0)
lock = RedisDistributedLock(r, 'inventory_lock:product_123', expire_time=10)

if lock.acquire():
    try:
        # 执行业务逻辑
        deduct_inventory('product_123', 1)
        # 如果业务执行时间不确定,启动看门狗续期
        # watchdog = Thread(target=auto_renew, args=(lock,))
        # watchdog.daemon = True
        # watchdog.start()
    finally:
        lock.release()
else:
    print('获取锁失败,请稍后重试')

锁续期(看门狗机制)是生产环境中必须考虑的问题。如果业务执行时间超过锁的过期时间,锁会被自动释放,此时其他客户端可能获取到锁,导致互斥失效。Redisson的看门狗默认每10秒续期一次(锁过期时间30秒),如果续期失败会停止续期并让锁自然过期。

RedLock算法原理与多节点容错实现

单节点Redis锁存在主从切换时锁丢失的风险:主节点加锁后尚未同步到从节点就宕机,哨兵将从节点提升为主节点,新主节点上没有锁记录,其他客户端可以重复加锁。RedLock算法通过在多个独立的Redis实例上同时加锁来解决这个问题。

import redis
import time
import uuid

class RedLock:
    def __init__(self, redis_nodes, lock_key, expire_time=10):
        '''
        redis_nodes: Redis实例列表 [(host, port), ...]
        至少5个独立实例才能保证容错性
        '''
        self.clients = [
            redis.Redis(host=h, port=p, db=0)
            for h, p in redis_nodes
        ]
        self.lock_key = lock_key
        self.expire_time = expire_time  # 毫秒
        self.lock_value = str(uuid.uuid4())
        self.quorum = len(self.clients) // 2 + 1  # 多数派
        self.retry_count = 3
        self.retry_delay = 200  # 毫秒

    def acquire_instance(self, client):
        '''在单个实例上加锁'''
        try:
            return client.set(
                self.lock_key, self.lock_value,
                nx=True, px=self.expire_time
            )
        except Exception:
            return False

    def release_instance(self, client):
        '''在单个实例上释放锁'''
        lua_script = '''
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        '''
        try:
            return client.eval(lua_script, 1, self.lock_key, self.lock_value)
        except Exception:
            return 0

    def acquire(self):
        '''RedLock获取锁'''
        for attempt in range(self.retry_count):
            success_count = 0
            start_time = time.time()

            # 在所有节点上尝试加锁
            for client in self.clients:
                if self.acquire_instance(client):
                    success_count += 1

            # 计算获取锁花费的时间
            elapsed = int((time.time() - start_time) * 1000)

            # 多数派成功且剩余时间充足
            if success_count >= self.quorum and elapsed < self.expire_time:
                # 更新锁的实际有效时间
                self.expire_time -= elapsed
                return True
            else:
                # 加锁失败,释放已获取的锁
                for client in self.clients:
                    self.release_instance(client)

                time.sleep(self.retry_delay / 1000)

        return False

    def release(self):
        '''释放所有节点上的锁'''
        for client in self.clients:
            self.release_instance(client)

# 使用示例
nodes = [
    ('redis1.example.com', 6379),
    ('redis2.example.com', 6379),
    ('redis3.example.com', 6379),
    ('redis4.example.com', 6379),
    ('redis5.example.com', 6379),
]

lock = RedLock(nodes, 'order_lock:user_456', expire_time=10000)
if lock.acquire():
    try:
        process_order('user_456')
    finally:
        lock.release()

RedLock算法的有效性依赖一个前提:各个Redis实例之间是独立的,不存在主从复制关系。如果使用云服务商提供的Redis集群,底层可能共享物理机或网络,在极端情况下可能同时故障。对于绝大多数业务场景,单节点Redis锁配合合理的过期时间已经足够,RedLock适用于对一致性要求极高的金融场景。

ZooKeeper临时顺序节点分布式锁

ZooKeeper通过临时顺序节点实现分布式锁,天然具备锁的可重入性和公平性。临时节点在客户端会话断开时自动删除,解决了持锁者宕机死锁的问题。顺序节点保证了锁的公平获取顺序,先请求的客户端先获得锁。

from kazoo.client import KazooClient
from kazoo.exceptions import NodeExistsError
import time

class ZooKeeperLock:
    def __init__(self, hosts, lock_path, timeout=10):
        self.zk = KazooClient(hosts=hosts, timeout=timeout)
        self.lock_path = lock_path
        self.lock_node = None
        self.locked = False

    def connect(self):
        self.zk.start()
        # 确保锁路径存在
        self.zk.ensure_path(self.lock_path)

    def acquire(self, timeout=None):
        '''获取锁(公平锁)'''
        # 创建临时顺序节点
        self.lock_node = self.zk.create(
            f'{self.lock_path}/lock_',
            ephemeral=True,
            sequence=True
        )
        node_name = self.lock_node.split('/')[-1]

        while True:
            # 获取所有子节点并排序
            children = self.zk.get_children(self.lock_path)
            children.sort()

            # 如果自己是最小节点,获取到锁
            if node_name == children[0]:
                self.locked = True
                return True

            # 否则监听前一个节点
            idx = children.index(node_name)
            prev_node = f'{self.lock_path}/{children[idx - 1]}'

            # 检查前一个节点是否存在
            if self.zk.exists(prev_node, watch=self._watch_handler):
                # 前一个节点存在,等待其删除
                event = self.zk.handler.event_object()
                self._watch_event = event
                event.wait(timeout=timeout)
                if not event.is_set():
                    # 超时
                    self.zk.delete(self.lock_node)
                    return False
            # 前一个节点已删除,重新检查

    def _watch_handler(self, event):
        '''节点删除回调'''
        if hasattr(self, '_watch_event'):
            self._watch_event.set()

    def release(self):
        '''释放锁'''
        if self.locked and self.lock_node:
            self.zk.delete(self.lock_node)
            self.locked = False

    def disconnect(self):
        '''关闭连接'''
        if self.locked:
            self.release()
        self.zk.stop()

# 使用示例
zk_lock = ZooKeeperLock('zk1:2181,zk2:2181,zk3:2181', '/locks/order')
zk_lock.connect()

if zk_lock.acquire(timeout=30):
    try:
        process_order()
    finally:
        zk_lock.release()
zk_lock.disconnect()

# 使用Curator Framework(Java生态推荐)
# InterProcessMutex lock = new InterProcessMutex(curatorClient, "/locks/order");
# if (lock.acquire(30, TimeUnit.SECONDS)) {
#     try {
#         processOrder();
#     } finally {
#         lock.release();
#     }
# }

ZooKeeper锁的优势在于其一致性保证:ZAB协议确保所有事务按顺序持久化,客户端会话超时后临时节点自动清理。劣势是性能低于Redis:每次加锁需要创建节点和设置watch,网络往返开销更大。在锁竞争激烈的场景下,大量watch事件可能造成羊群效应,通过顺序节点监听前一个节点的方式可以缓解。

三种方案对比与生产环境选型建议

Redis单节点锁、RedLock和ZooKeeper锁各有优劣,选型需要根据业务场景的CAP要求决定。

# 方案对比表
'''
| 维度        | Redis单节点锁 | RedLock      | ZooKeeper锁  |
|------------|--------------|-------------|-------------|
| 性能(TPS)  | 10万+        | 1万+        | 1千-5千     |
| 延迟       | ~1ms         | ~5ms        | ~10-50ms    |
| 一致性     | AP(最终一致)| CP+AP混合   | CP(强一致) |
| 可用性     | 高           | 中高        | 中          |
| 复杂度     | 低           | 中          | 中高        |
| 死锁风险   | 依赖过期时间  | 依赖过期时间 | 无(会话超时)|
| 公平性     | 非公平       | 非公平      | 公平        |
| 可重入性   | 需自行实现    | 需自行实现  | 原生支持    |
'''

# 工厂模式:根据场景选择锁实现
class LockFactory:
    @staticmethod
    def create(lock_type, config):
        if lock_type == 'redis':
            return RedisDistributedLock(
                redis.Redis(host=config['host'], port=config['port']),
                config['key'],
                expire_time=config.get('ttl', 30)
            )
        elif lock_type == 'redlock':
            return RedLock(
                config['nodes'],
                config['key'],
                expire_time=config.get('ttl', 10000)
            )
        elif lock_type == 'zookeeper':
            lock = ZooKeeperLock(
                config['hosts'],
                config['path'],
                timeout=config.get('timeout', 10)
            )
            lock.connect()
            return lock

# 选型建议:
# 1. 电商库存扣减、秒杀 -> Redis单节点锁(高并发,允许极小概率的不一致)
# 2. 金融转账、支付 -> ZooKeeper锁(强一致,可接受性能损失)
# 3. 定时任务去重 -> Redis单节点锁(低频,简单可靠)
# 4. 跨数据中心分布式锁 -> RedLock(多节点容错,但需独立部署Redis)

消息中间件配合分布式锁使用可以进一步提升系统的可靠性。将锁获取和业务执行封装为消息消费过程,利用消息队列的重试和死信机制处理锁获取失败的请求。这种模式在业务中台建设中广泛应用,将分布式锁的复杂性封装在中间件层,业务代码保持简洁。

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

赞 (0)
小编小编
上一篇 2026年9月4日
下一篇 2026年9月4日

相关推荐