分布式锁是分布式系统中协调多个节点对共享资源访问的关键机制。我来从原理到实现,系统性地讲解。
核心要求
一个可靠的分布式锁必须满足:
| 特性 | 说明 |
|---|---|
| 互斥性 | 同一时间只有一个客户端能持有锁 |
| 防死锁 | 锁必须有过期机制,避免客户端崩溃后锁永远不被释放 |
| 可重入性(可选) | 同一客户端可以多次获取同一把锁 |
| 容错性 | 大部分节点存活时,锁服务仍能正常工作 |
方案一:基于 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 单节点 + 看门狗) |
| 要求强一致性(库存扣减) | ZooKeeper 或 etcd |
| 极高可用要求 | RedLock(5 个 Redis 节点) |
| 已有 K8s 集群 | etcd(原生支持) |