分布式锁是分布式系统中协调多个节点对共享资源访问的关键机制。我来从原理到实现,系统性地讲解。
核心要求
一个可靠的分布式锁必须满足:
| 特性 | 说明 |
|---|---|
| 互斥性 | 同一时间只有一个客户端能持有锁 |
| 防死锁 | 锁必须有过期机制,避免客户端崩溃后锁永远不被释放 |
| 可重入性(可选) | 同一客户端可以多次获取同一把锁 |
| 容错性 | 大部分节点存活时,锁服务仍能正常工作 |
方案一:基于 Redis(最常用)
1. 基础版(SETNX + EXPIRE)
1SETNX lock:resource "client_id" # 如果不存在则设置 2EXPIRE lock:resource 30 # 设置30秒过期 3
问题:SETNX 和 EXPIRE 不是原子操作,如果中间崩溃会导致死锁。
2. 改进版(Redis 2.6+ 原子命令)
1SET lock:resource "client_id" NX EX 30 2# NX = Not eXists(不存在才设置) 3# EX = EXpire(秒级过期) 4
3. 生产级实现(Redisson 原理)
1import redis 2import uuid 3import threading 4 5class RedisDistributedLock: 6 def __init__(self, redis_client, lock_key, expire=30): 7 self.redis = redis_client 8 self.lock_key = f"lock:{lock_key}" 9 self.expire = expire 10 self.identifier = str(uuid.uuid4()) # 唯一标识,防止误删他人锁 11 12 def acquire(self): 13 """获取锁""" 14 # 原子性设置锁 15 result = self.redis.set( 16 self.lock_key, 17 self.identifier, 18 nx=True, # 不存在才设置 19 ex=self.expire # 过期时间 20 ) 21 if result: 22 # 启动看门狗线程,自动续期 23 self._start_watchdog() 24 return True 25 return False 26 27 def release(self): 28 """释放锁 - 使用 Lua 脚本保证原子性""" 29 lua_script = """ 30 if redis.call("get", KEYS[1]) == ARGV[1] then 31 return redis.call("del", KEYS[1]) 32 else 33 return 0 34 end 35 """ 36 # 只有锁的持有者才能删除 37 self.redis.eval(lua_script, 1, self.lock_key, self.identifier) 38 self._stop_watchdog() 39 40 def _start_watchdog(self): 41 """看门狗:自动续期,防止业务执行超时""" 42 def renew(): 43 while self._running: 44 time.sleep(self.expire / 3) 45 # 如果锁还存在,续期 46 lua_script = """ 47 if redis.call("get", KEYS[1]) == ARGV[1] then 48 return redis.call("expire", KEYS[1], ARGV[2]) 49 end 50 return 0 51 """ 52 self.redis.eval(lua_script, 1, self.lock_key, 53 self.identifier, self.expire) 54 55 self._running = True 56 self._watchdog = threading.Thread(target=renew) 57 self._watchdog.daemon = True 58 self._watchdog.start() 59 60 def _stop_watchdog(self): 61 self._running = False 62 63# 使用 64redis_client = redis.Redis() 65lock = RedisDistributedLock(redis_client, "order:123") 66 67if lock.acquire(): 68 try: 69 # 执行业务逻辑 70 process_order() 71 finally: 72 lock.release() 73
4. RedLock 算法(Redis 作者提出,多主节点)
解决单点 Redis 故障问题,需要奇数个独立 Redis 节点(如 5 个):
1class RedLock: 2 def __init__(self, redis_nodes, lock_key, expire=30): 3 self.nodes = [redis.Redis(**node) for node in redis_nodes] 4 self.lock_key = lock_key 5 self.expire = expire 6 self.quorum = len(redis_nodes) // 2 + 1 # 多数派 7 8 def acquire(self): 9 identifier = str(uuid.uuid4()) 10 locked_nodes = 0 11 12 for node in self.nodes: 13 try: 14 if node.set(self.lock_key, identifier, nx=True, ex=self.expire): 15 locked_nodes += 1 16 except: 17 continue 18 19 # 获取多数派且总耗时小于锁过期时间 20 if locked_nodes >= self.quorum: 21 return True 22 23 # 失败,释放已获取的锁 24 self.release(identifier) 25 return False 26
方案二:基于 ZooKeeper / etcd
更适合强一致性场景,利用临时顺序节点实现:
ZooKeeper 实现原理
11. 客户端在 /locks/resource 下创建临时顺序节点:/locks/resource/lock-00000001 22. 检查自己是否是最小编号: 3 - 是:获得锁 4 - 否:监听前一个节点(Watcher),等待其删除 53. 业务完成后删除节点,自动唤醒下一个等待者 64. 客户端崩溃 → 会话超时 → 临时节点自动删除 → 锁释放 7
1from kazoo.client import KazooClient 2 3class ZkDistributedLock: 4 def __init__(self, zk_hosts, lock_path): 5 self.zk = KazooClient(hosts=zk_hosts) 6 self.zk.start() 7 self.lock_path = lock_path 8 self.lock = self.zk.Lock(lock_path, "my_identifier") 9 10 def acquire(self, timeout=None): 11 return self.lock.acquire(blocking=True, timeout=timeout) 12 13 def release(self): 14 self.lock.release() 15
优势:
- 天然防死锁(临时节点会话断开自动删除)
- 可重入(同一会话可多次获取)
- 强一致性(ZAB 协议)
方案三:基于数据库
MySQL 实现
1-- 建表 2CREATE TABLE distributed_lock ( 3 lock_name VARCHAR(64) PRIMARY KEY, 4 identifier VARCHAR(64) NOT NULL, 5 expire_time TIMESTAMP NOT NULL 6); 7 8-- 获取锁(插入,利用唯一索引) 9INSERT INTO distributed_lock (lock_name, identifier, expire_time) 10VALUES ('order:123', 'uuid-xxx', DATE_ADD(NOW(), INTERVAL 30 SECOND)); 11 12-- 释放锁 13DELETE FROM distributed_lock WHERE lock_name = 'order:123' AND identifier = 'uuid-xxx'; 14 15-- 清理过期锁(定时任务) 16DELETE FROM distributed_lock WHERE expire_time < NOW(); 17
缺点:性能差,无自动续期,仅适合低频场景。
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis | 性能极高(10w+ QPS),实现简单 | 单点故障(除非 RedLock),时钟漂移问题 | 高并发、允许短暂不一致 |
| ZooKeeper | 强一致性,自动故障恢复 | 性能较低(写操作需半数确认),部署复杂 | 强一致性要求(如金融) |
| etcd | 类似 ZK,但基于 Raft 更易用 | 性能一般 | K8s 生态、服务发现 |
| MySQL | 无需额外组件 | 性能差,单点瓶颈 | 低频、简单场景 |
关键问题与解决方案
1. 锁续期(看门狗机制)
业务执行时间超过锁过期时间时,需要自动续期:
1# 独立线程定时检查,剩余时间 < 1/3 时续期 2if ttl < expire_time / 3: 3 redis.expire(lock_key, expire_time) 4
2. 主从延迟问题(Redis)
主节点宕机,从节点晋升为主,但锁数据可能未同步 → RedLock 或 Redisson 的 WAIT 命令
3. 可重入实现
1# 锁 value 记录:client_id + 重入次数 2"client_abc:3" # 表示 client_abc 第 3 次重入 3 4# 释放时递减,为 0 时才删除 5
4. 公平锁 vs 非公平锁
- 非公平锁:所有客户端同时抢,谁快谁得(Redis 默认)
- 公平锁:按请求顺序排队(ZK 临时顺序节点天然支持)
生产建议
| 场景 | 推荐方案 |
|---|---|
| 一般互联网应用 | Redisson(Redis 单节点 + 看门狗) |
| 要求强一致性(库存扣减) | ZooKeeper 或 etcd |
| 极高可用要求 | RedLock(5 个 Redis 节点) |
| 已有 K8s 集群 | etcd(原生支持) |