如何实现分布式锁

分布式锁是分布式系统中协调多个节点对共享资源访问的关键机制。我来从原理到实现,系统性地讲解。


核心要求

一个可靠的分布式锁必须满足:

特性 说明
互斥性 同一时间只有一个客户端能持有锁
防死锁 锁必须有过期机制,避免客户端崩溃后锁永远不被释放
可重入性(可选) 同一客户端可以多次获取同一把锁
容错性 大部分节点存活时,锁服务仍能正常工作

方案一:基于 Redis(最常用)

1. 基础版(SETNX + EXPIRE)

bash 复制代码
SETNX lock:resource "client_id"    # 如果不存在则设置
EXPIRE lock:resource 30            # 设置30秒过期

问题:SETNX 和 EXPIRE 不是原子操作,如果中间崩溃会导致死锁。

2. 改进版(Redis 2.6+ 原子命令)

bash 复制代码
SET lock:resource "client_id" NX EX 30
# NX = Not eXists(不存在才设置)
# EX = EXpire(秒级过期)

3. 生产级实现(Redisson 原理)

python 复制代码
import redis
import uuid
import threading

class RedisDistributedLock:
    def __init__(self, redis_client, lock_key, expire=30):
        self.redis = redis_client
        self.lock_key = f"lock:{lock_key}"
        self.expire = expire
        self.identifier = str(uuid.uuid4())  # 唯一标识,防止误删他人锁
    
    def acquire(self):
        """获取锁"""
        # 原子性设置锁
        result = self.redis.set(
            self.lock_key, 
            self.identifier,
            nx=True,      # 不存在才设置
            ex=self.expire  # 过期时间
        )
        if result:
            # 启动看门狗线程,自动续期
            self._start_watchdog()
            return True
        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
        """
        # 只有锁的持有者才能删除
        self.redis.eval(lua_script, 1, self.lock_key, self.identifier)
        self._stop_watchdog()
    
    def _start_watchdog(self):
        """看门狗:自动续期,防止业务执行超时"""
        def renew():
            while self._running:
                time.sleep(self.expire / 3)
                # 如果锁还存在,续期
                lua_script = """
                if redis.call("get", KEYS[1]) == ARGV[1] then
                    return redis.call("expire", KEYS[1], ARGV[2])
                end
                return 0
                """
                self.redis.eval(lua_script, 1, self.lock_key, 
                              self.identifier, self.expire)
        
        self._running = True
        self._watchdog = threading.Thread(target=renew)
        self._watchdog.daemon = True
        self._watchdog.start()
    
    def _stop_watchdog(self):
        self._running = False

# 使用
redis_client = redis.Redis()
lock = RedisDistributedLock(redis_client, "order:123")

if lock.acquire():
    try:
        # 执行业务逻辑
        process_order()
    finally:
        lock.release()

4. RedLock 算法(Redis 作者提出,多主节点)

解决单点 Redis 故障问题,需要奇数个独立 Redis 节点(如 5 个):

python 复制代码
class RedLock:
    def __init__(self, redis_nodes, lock_key, expire=30):
        self.nodes = [redis.Redis(**node) for node in redis_nodes]
        self.lock_key = lock_key
        self.expire = expire
        self.quorum = len(redis_nodes) // 2 + 1  # 多数派
    
    def acquire(self):
        identifier = str(uuid.uuid4())
        locked_nodes = 0
        
        for node in self.nodes:
            try:
                if node.set(self.lock_key, identifier, nx=True, ex=self.expire):
                    locked_nodes += 1
            except:
                continue
        
        # 获取多数派且总耗时小于锁过期时间
        if locked_nodes >= self.quorum:
            return True
        
        # 失败,释放已获取的锁
        self.release(identifier)
        return False

方案二:基于 ZooKeeper / etcd

更适合强一致性场景,利用临时顺序节点实现:

ZooKeeper 实现原理

markdown 复制代码
1. 客户端在 /locks/resource 下创建临时顺序节点:/locks/resource/lock-00000001
2. 检查自己是否是最小编号:
   - 是:获得锁
   - 否:监听前一个节点(Watcher),等待其删除
3. 业务完成后删除节点,自动唤醒下一个等待者
4. 客户端崩溃 → 会话超时 → 临时节点自动删除 → 锁释放
python 复制代码
from kazoo.client import KazooClient

class ZkDistributedLock:
    def __init__(self, zk_hosts, lock_path):
        self.zk = KazooClient(hosts=zk_hosts)
        self.zk.start()
        self.lock_path = lock_path
        self.lock = self.zk.Lock(lock_path, "my_identifier")
    
    def acquire(self, timeout=None):
        return self.lock.acquire(blocking=True, timeout=timeout)
    
    def release(self):
        self.lock.release()

优势

  • 天然防死锁(临时节点会话断开自动删除)
  • 可重入(同一会话可多次获取)
  • 强一致性(ZAB 协议)

方案三:基于数据库

MySQL 实现

sql 复制代码
-- 建表
CREATE TABLE distributed_lock (
    lock_name VARCHAR(64) PRIMARY KEY,
    identifier VARCHAR(64) NOT NULL,
    expire_time TIMESTAMP NOT NULL
);

-- 获取锁(插入,利用唯一索引)
INSERT INTO distributed_lock (lock_name, identifier, expire_time)
VALUES ('order:123', 'uuid-xxx', DATE_ADD(NOW(), INTERVAL 30 SECOND));

-- 释放锁
DELETE FROM distributed_lock WHERE lock_name = 'order:123' AND identifier = 'uuid-xxx';

-- 清理过期锁(定时任务)
DELETE FROM distributed_lock WHERE expire_time < NOW();

缺点:性能差,无自动续期,仅适合低频场景。


方案对比

方案 优点 缺点 适用场景
Redis 性能极高(10w+ QPS),实现简单 单点故障(除非 RedLock),时钟漂移问题 高并发、允许短暂不一致
ZooKeeper 强一致性,自动故障恢复 性能较低(写操作需半数确认),部署复杂 强一致性要求(如金融)
etcd 类似 ZK,但基于 Raft 更易用 性能一般 K8s 生态、服务发现
MySQL 无需额外组件 性能差,单点瓶颈 低频、简单场景

关键问题与解决方案

1. 锁续期(看门狗机制)

业务执行时间超过锁过期时间时,需要自动续期:

python 复制代码
# 独立线程定时检查,剩余时间 < 1/3 时续期
if ttl < expire_time / 3:
    redis.expire(lock_key, expire_time)

2. 主从延迟问题(Redis)

主节点宕机,从节点晋升为主,但锁数据可能未同步 → RedLock 或 Redisson 的 WAIT 命令

3. 可重入实现

python 复制代码
# 锁 value 记录:client_id + 重入次数
"client_abc:3"  # 表示 client_abc 第 3 次重入

# 释放时递减,为 0 时才删除

4. 公平锁 vs 非公平锁

  • 非公平锁:所有客户端同时抢,谁快谁得(Redis 默认)
  • 公平锁:按请求顺序排队(ZK 临时顺序节点天然支持)

生产建议

场景 推荐方案
一般互联网应用 Redisson(Redis 单节点 + 看门狗)
要求强一致性(库存扣减) ZooKeeperetcd
极高可用要求 RedLock(5 个 Redis 节点)
已有 K8s 集群 etcd(原生支持)
相关推荐
白露与泡影4 小时前
Java面试题库及答案解析(2026版)
java·开发语言·面试
June bug4 小时前
全链路测试
功能测试·面试·职场和发展
AI成长日志6 小时前
【笔面试算法学习专栏】哈希表基础:两数之和与字母异位词分组
学习·算法·面试
ShineWinsu6 小时前
对于Linux:文件操作以及文件IO的解析
linux·c++·面试·笔试·io·shell·文件操作
U盘失踪了7 小时前
面试题:你在测试工作中有使用过AI吗?具体是怎么用的?
面试
Baihai_IDP8 小时前
微软多模态推理模型 Phi-4-reasoning-vision 训练经验分享
人工智能·面试·llm
前端Hardy8 小时前
前端开发效率翻倍:15个超级实用的工具函数,直接复制进项目(建议收藏)
前端·javascript·面试
a里啊里啊8 小时前
常见面试题目集合
linux·数据库·c++·面试·职场和发展·操作系统
indexsunny9 小时前
互联网大厂Java面试实战:从Spring Boot到微服务架构的技术问答
java·spring boot·redis·微服务·面试·kafka·spring security