分布式锁
问题
为什么需要分布式锁?Redis、ZooKeeper、MySQL 各如何实现分布式锁?各方案的优缺点是什么?
答案
为什么需要分布式锁
单机环境中 synchronized / ReentrantLock 可以保证线程安全,但在分布式部署下多个 JVM 进程无法共享同一把锁。分布式锁通过外部存储(Redis、ZooKeeper、MySQL)来实现跨进程互斥。
分布式锁的核心要求
| 要求 | 说明 |
|---|---|
| 互斥性 | 同一时刻只有一个客户端持有锁 |
| 防死锁 | 持锁客户端崩溃后,锁能自动释放(超时机制) |
| 可重入 | 同一客户端可重复加锁(选项) |
| 高可用 | 锁服务本身要高可用 |
| 安全释放 | 只有持锁者能释放自己的锁 |
方案一:Redis 分布式锁
基础版:SETNX + 超时
// 加锁:SET key value NX EX seconds(原子操作)
String lockKey = "lock:order:" + orderId;
String requestId = UUID.randomUUID().toString();
Boolean acquired = redis.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(acquired)) {
try {
// 执行业务逻辑
doBusiness();
} finally {
// 释放锁:Lua 脚本保证原子性(只释放自己的锁)
String script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
redis.execute(new DefaultRedisScript<>(script, Long.class),
List.of(lockKey), requestId);
}
}
Redisson(推荐生产使用):
Redisson 分布式锁
RLock lock = redissonClient.getLock("lock:order:" + orderId);
try {
// 尝试加锁,最多等待 10 秒,锁自动过期 30 秒
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
try {
doBusiness();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Redisson 内部通过 Watchdog 看门狗 自动续期(默认每 10 秒续期到 30 秒),解决了锁超时问题。详见 Redis 分布式锁。
方案二:ZooKeeper 分布式锁
基于 ZooKeeper 的 临时顺序节点 实现:
Curator 框架实现
InterProcessMutex lock = new InterProcessMutex(curatorClient, "/locks/order");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
doBusiness();
} finally {
lock.release();
}
}
} catch (Exception e) {
log.error("获取锁失败", e);
}
优点:
- 临时节点天然防死锁(客户端断开连接,节点自动删除)
- 监听机制避免轮询(惊群问题通过顺序节点解决)
- 强一致性(ZAB 协议保证)
方案三:MySQL 分布式锁
基于唯一索引
-- 创建锁表
CREATE TABLE distributed_lock (
lock_key VARCHAR(64) PRIMARY KEY,
request_id VARCHAR(64) NOT NULL,
expire_time DATETIME NOT NULL
);
-- 加锁:插入成功则获取锁
INSERT INTO distributed_lock (lock_key, request_id, expire_time)
VALUES ('order_lock', 'uuid-xxx', NOW() + INTERVAL 30 SECOND);
-- 释放锁:删除自己的记录
DELETE FROM distributed_lock WHERE lock_key = 'order_lock' AND request_id = 'uuid-xxx';
一般不推荐用 MySQL 实现分布式锁(性能差、实现复杂),通常作为兜底方案。
三种方案对比
| 维度 | Redis | ZooKeeper | MySQL |
|---|---|---|---|
| 性能 | 最高 | 中等 | 最低 |
| 一致性 | AP(主从可能丢锁) | CP(强一致) | 强一致 |
| 可靠性 | 中等(RedLock 提升) | 高 | 高 |
| 实现复杂度 | 低(Redisson) | 低(Curator) | 中 |
| 自动续期 | Watchdog | Session 超时自动释放 | 需定时任务 |
| 适用场景 | 高并发、允许极端情况失锁 | 强一致性要求 | 已有 MySQL 无额外组件 |
常见面试问题
Q1: Redis 和 ZooKeeper 分布式锁怎么选?
答案:
- Redis:性能高(10万+ QPS),适合高并发场景。缺点是 Redis 主从切换时可能丢锁(Master 宕机,锁未同步到 Slave)
- ZooKeeper:强一致(CP),锁更可靠。缺点是性能较 Redis 低,适合对一致性要求高的场景
大多数互联网项目选 Redis(Redisson),金融等强一致性场景选 ZooKeeper(Curator)。
Q2: Redis 分布式锁的主从问题怎么解决?
答案:
问题:客户端在 Master 加锁成功,Master 宕机,锁还未同步到 Slave,Slave 晋升为新 Master 后另一个客户端也能加锁 → 两个客户端同时持锁。
解决方案:
- RedLock 算法:向 5 个独立 Redis 实例加锁,过半成功才算加锁成功。但有争议(Martin Kleppmann 指出时钟漂移问题)
- Redisson 的 RedLock 实现已被弃用
- 实际方案:接受极端情况下的不一致,业务层做兜底(如数据库乐观锁)
Q3: 如何防止锁过期但业务未完成?
答案:
- Watchdog 自动续期:Redisson 默认开启看门狗,每
leaseTime / 3自动续期 - 合理设置超时时间:根据业务处理时间设置足够长的锁超时
- 兜底机制:业务层加乐观锁(版本号),即使锁失效也能防止并发问题
Q4: 分布式锁是不是越用越好?
答案:
不是。分布式锁引入了额外的依赖和延迟,应该尽量减少使用:
- 优先考虑 数据库唯一约束、乐观锁 等无锁方案
- 分布式锁的粒度要细(按 orderId 加锁,不要全局锁)
- 持锁时间要短(只锁关键操作)
- 考虑是否真的需要互斥(有些场景幂等处理就够了)