分布式锁实现实战:Redis与Zookeeper锁方案对比与代码实现

分布式锁的核心需求与挑战

分布式锁是分布式系统中协调多节点对共享资源互斥访问的基础组件。单机环境下通过语言内置锁(如Java synchronized、Go mutex)即可实现互斥,但跨进程、跨服务器的场景需要借助外部存储实现锁协调。分布式锁需满足四个核心需求:互斥性(同一时刻仅一个客户端持有锁)、可重入性(同一客户端可多次获取同一锁)、防死锁(锁持有者宕机后锁能自动释放)和公平性(按请求顺序获取锁)。

实现分布式锁的常见方案包括基于Redis的内存锁、基于Zookeeper的共识锁和基于数据库的乐观锁。Redis锁性能最高但存在极端情况下的一致性风险,Zookeeper锁强一致但性能较低,数据库锁实现简单但扩展性差。选择方案需根据业务对一致性和性能的要求权衡。

Redis分布式锁的SET NX实现

Redis实现分布式锁的基础是SET key value NX PX命令。NX保证只有key不存在时才能设置成功,PX设置过期时间防止死锁。value设置为客户端唯一标识,用于释放锁时验证所有权。

import redis
import uuid
import time

class RedisDistributedLock:
    def __init__(self, redis_client, lock_name, expire_ms=30000):
        self.redis = redis_client
        self.lock_name = f"lock:{lock_name}"
        self.expire_ms = expire_ms
        self.lock_value = str(uuid.uuid4())
        self._locked = False
    
    def acquire(self, retry_count=3, retry_delay=200):
        '''获取分布式锁'''
        for i in range(retry_count):
            # SET key value NX PX expire_ms
            result = self.redis.set(
                self.lock_name,
                self.lock_value,
                nx=True,  # 仅当key不存在时设置
                px=self.expire_ms  # 毫秒级过期
            )
            if result:
                self._locked = True
                return True
            time.sleep(retry_delay / 1000)
        return False
    
    def release(self):
        '''释放锁 - 使用Lua脚本保证原子性'''
        if not self._locked:
            return False
        
        # Lua脚本:先检查value再删除,保证不会误删别人的锁
        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_name, self.lock_value)
        self._locked = False
        return result == 1
    
    def __enter__(self):
        self.acquire()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()

释放锁必须使用Lua脚本保证GET和DEL的原子性。若先GET判断再DEL,中间存在时间窗口:客户端A获取锁后超时自动释放,客户端B获取锁,客户端A执行DEL删除了客户端B的锁。Lua脚本在Redis单线程中原子执行,避免竞态条件。

Redis RedLock算法与多实例容错

单节点Redis锁在主从切换时存在锁丢失风险:master节点宕机,slave晋升为master但尚未同步锁数据,另一个客户端获取到相同的锁。RedLock算法通过多个独立Redis实例投票解决此问题。

import time
import uuid

class RedLock:
    '''RedLock算法实现 - 多Redis实例投票'''
    
    def __init__(self, redis_nodes, lock_name, expire_ms=30000, quorum=None):
        '''
        redis_nodes: Redis实例列表 [(host, port), ...]
        lock_name: 锁名称
        expire_ms: 锁过期时间(毫秒)
        quorum: 法定票数,默认为 N//2 + 1
        '''
        self.nodes = [redis.Redis(host=h, port=p) for h, p in redis_nodes]
        self.lock_name = f"redlock:{lock_name}"
        self.expire_ms = expire_ms
        self.quorum = quorum or (len(redis_nodes) // 2 + 1)
        self.lock_value = str(uuid.uuid4())
        self._acquired_nodes = []
    
    def acquire(self, retry_count=3, retry_delay=200):
        '''获取锁 - 需要多数节点同意'''
        for attempt in range(retry_count):
            success_count = 0
            start_time = time.time()
            self._acquired_nodes = []
            
            for node in self.nodes:
                try:
                    result = node.set(
                        self.lock_name,
                        self.lock_value,
                        nx=True,
                        px=self.expire_ms
                    )
                    if result:
                        success_count += 1
                        self._acquired_nodes.append(node)
                except Exception:
                    continue
            
            # 检查是否获得多数节点同意
            elapsed_ms = (time.time() - start_time) * 1000
            if success_count >= self.quorum and elapsed_ms < self.expire_ms:
                return True
            else:
                # 未获得足够票数,释放已获取的锁
                self._release_acquired()
                time.sleep(retry_delay / 1000)
        
        return False
    
    def _release_acquired(self):
        '''释放已获取锁的节点'''
        lua_script = '''
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
        '''
        for node in self._acquired_nodes:
            try:
                node.eval(lua_script, 1, self.lock_name, self.lock_value)
            except Exception:
                continue
    
    def release(self):
        '''释放所有节点上的锁'''
        self._release_acquired()

RedLock在N个独立Redis实例上同时获取锁,当且仅当在超过半数实例上获取成功且耗时未超过锁过期时间时,认为锁获取成功。该算法由Redis作者Antirez提出,但在分布式系统社区存在争议。Martin Kleppmann指出RedLock在时钟漂移和GC暂停场景下仍存在安全性问题。对一致性要求极高的场景建议使用Zookeeper或etcd。

Zookeeper分布式锁实现

Zookeeper通过临时顺序节点(Ephemeral Sequential Node)实现分布式锁。每个客户端在锁节点下创建临时顺序子节点,序号最小的客户端获得锁,其他客户端监听前一个节点的删除事件。

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

class ZkDistributedLock:
    def __init__(self, zk_hosts, lock_path, timeout=30):
        self.zk = KazooClient(hosts=zk_hosts, timeout=timeout)
        self.lock_path = f"/locks/{lock_path}"
        self.node_path = None
        self._locked = False
    
    def acquire(self, retry_count=3, retry_delay=500):
        '''获取Zookeeper分布式锁'''
        self.zk.start(timeout=self.zk.timeout)
        
        for attempt in range(retry_count):
            try:
                # 确保锁根节点存在
                self.zk.ensure_path(self.lock_path)
                
                # 创建临时顺序节点
                self.node_path = self.zk.create(
                    f"{self.lock_path}/lock-",
                    ephemeral=True,
                    sequence=True
                )
                
                # 获取锁节点下的所有子节点并排序
                children = self.zk.get_children(self.lock_path)
                children.sort()
                
                # 检查自己是否是序号最小的节点
                my_node = self.node_path.split('/')[-1]
                my_index = children.index(my_node)
                
                if my_index == 0:
                    # 序号最小,获取锁成功
                    self._locked = True
                    return True
                else:
                    # 监听前一个节点
                    prev_node = f"{self.lock_path}/{children[my_index - 1]}"
                    lock_event = self.zk.handler.event_object()
                    
                    # 创建一次性监听
                    @self.zk.DataWatch(prev_node)
                    def watch_prev(data, stat, event):
                        if event and event.type == 'DELETED':
                            lock_event.set()
                    
                    # 等待前一个节点被删除
                    if lock_event.wait(timeout=30):
                        # 重新检查自己是否成为最小节点
                        children = self.zk.get_children(self.lock_path)
                        children.sort()
                        my_index = children.index(my_node)
                        if my_index == 0:
                            self._locked = True
                            return True
                    
            except Exception as e:
                if attempt < retry_count - 1:
                    time.sleep(retry_delay / 1000)
                    continue
        
        self.zk.stop()
        return False
    
    def release(self):
        '''释放锁 - 删除临时节点'''
        if self.node_path and self._locked:
            try:
                self.zk.delete(self.node_path)
            except Exception:
                pass
            self._locked = False
        self.zk.stop()
    
    def __enter__(self):
        self.acquire()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()

Zookeeper锁的优势在于客户端宕机时临时节点自动删除,无需设置过期时间,避免了Redis锁的续期问题。监听前一个节点而非所有节点(羊群效应避免),减少通知风暴。Zookeeper的ZAB协议保证强一致性,锁状态在所有节点上同步。

Redis锁与Zookeeper锁对比与选型

两种方案的对比维度包括性能、一致性、可用性和复杂度:

# 对比总结
'''
维度          Redis SET NX        RedLock             Zookeeper
性能          最高(10K+ QPS)       高(5K+ QPS)         中(1K QPS)
一致性        弱(主从异步)         中(多实例投票)       强(ZAB共识)
可用性        高(主从切换)         高(多实例冗余)       高(集群选举)
防死锁        过期时间兜底          过期时间兜底          临时节点自动删除
可重入        需自行实现           需自行实现           需自行实现
锁续期        需WatchDog机制       需WatchDog机制       不需要(临时节点)
运维成本      低                   中(多实例)           高(ZK集群)
'''

选型建议:高并发、对极端情况下的短暂不一致可容忍的场景选Redis单节点锁(如库存扣减、限流)。对一致性要求严格、可接受较低性能的场景选Zookeeper锁(如分布式事务协调、选主)。中间场景可考虑Redisson的RLock实现,内置看门狗(WatchDog)自动续期和可重入支持:

# Redisson分布式锁(生产推荐)
from redisson import Redisson

redisson = Redisson(config={
    'host': '127.0.0.1',
    'port': 6379
})

lock = redisson.get_lock("order_lock:1001")

try:
    # 尝试获取锁,等待10秒,锁30秒后自动释放
    if lock.try_lock(wait_time=10, lease_time=30):
        try:
            # 执行业务逻辑
            process_order()
        finally:
            lock.unlock()
    else:
        print("获取锁失败")
except Exception as e:
    print(f"异常: {e}")
    if lock.is_locked():
        lock.unlock()

Redisson的RLock实现了可重入锁,同一线程可多次获取同一把锁,内部通过Hash结构记录重入次数。看门狗机制默认每10秒续期一次,锁持有者活跃时锁不会过期。生产环境中分布式锁的监控指标包括锁等待时间、持有时间、获取失败率和锁续期次数,异常指标持续走高时需排查锁竞争热点和业务逻辑。

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

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

相关推荐