一、前言:为什么单机锁在分布式系统中失效?
在单体应用中,我们常用 synchronized 或 ReentrantLock 保证线程安全。
但当系统拆分为多个服务实例(如 10 台服务器同时运行订单服务),本地锁无法跨 JVM 生效!
此时,必须引入 分布式锁(Distributed Lock) ------ 一种跨进程、跨机器的互斥机制。
本文将带你深入理解分布式锁的核心要求,并对比 Redis、ZooKeeper、数据库 三种主流实现方案的优劣。
二、分布式锁的三大核心要求
一个合格的分布式锁,必须满足:
✅ 1. 互斥性(Mutual Exclusion)
- 同一时刻,只能有一个客户端持有锁
✅ 2. 避免死锁(Deadlock Free)
- 即使持有锁的节点宕机,锁也能自动释放(通过超时机制)
✅ 3. 高可用 & 容错性
- 锁服务本身不能成为单点故障
⚠️ 额外加分项:可重入性 、公平性 、高性能
三、实现方式一:基于数据库(最简单,但性能差)
原理:
利用数据库 唯一索引 或 排他锁(FOR UPDATE) 实现互斥。
方案 A:唯一索引
sql
-- 创建锁表
CREATE TABLE distributed_lock (
lock_name VARCHAR(64) PRIMARY KEY,
expire_time BIGINT
);
-- 获取锁:插入成功即获得锁
INSERT INTO distributed_lock (lock_name, expire_time)
VALUES ('order_lock', 1712345678);
-- 失败则抛出唯一索引冲突
方案 B:SELECT FOR UPDATE
java
// 事务中执行
SELECT * FROM distributed_lock WHERE lock_name = 'order_lock' FOR UPDATE;
// 执行业务逻辑...
✅ 优点:
- 实现简单,无需额外中间件
- 强一致性(依赖 DB ACID)
❌ 缺点:
- 性能极差:DB 成为瓶颈
- 无自动过期:需额外定时任务清理
- 主从切换可能丢锁
📌 适用场景:低并发、已有 DB 且无 Redis/ZK 的遗留系统
四、实现方式二:基于 Redis(高性能,但需注意细节)
基础实现(错误示范!):
bash
# ❌ 错误:非原子操作
GET lock_key
# 若不存在
SET lock_key "client_1"
EXPIRE lock_key 30
问题 :
SET和EXPIRE不是原子的,若服务在 SET 后 crash,锁永不过期!
✅ 正确实现:使用 SET key value NX EX seconds
bash
# 原子获取锁(Redis 2.6.12+)
SET lock_key "client_1" NX EX 30
NX:仅当 key 不存在时设置EX 30:30 秒后自动过期
释放锁(需校验 owner):
Lua
-- unlock.lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
防止 A 释放了 B 的锁!
高级方案:RedLock(Redis 官方推荐)
- 向 N 个独立 Redis 节点 请求锁
- 超过半数成功 + 总耗时 < 锁有效期 → 获取成功
✅ 优点:
- 性能极高(微秒级)
- 自动过期防死锁
- 客户端实现简单
❌ 缺点:
- CP or AP?:Redis 主从异步复制,主挂可能导致锁丢失(不满足强一致性)
- RedLock 实现复杂,争议较大(Martin Kleppmann 曾质疑其安全性)
📌 适用场景:高并发、允许短暂不一致的业务(如秒杀、缓存重建)
五、实现方式三:基于 ZooKeeper(强一致,但性能较低)
原理:
利用 ZK 的 临时顺序节点(Ephemeral Sequential Node) 实现公平锁。
流程:
- 所有客户端在
/locks下创建临时顺序节点(如/locks/lock-0000000001) - 客户端检查自己是否是最小序号节点
- 是 → 获得锁
- 否 → 监听前一个节点的删除事件(Watcher)
- 业务执行完,删除自身节点(或会话断开自动删除)
✅ 优点:
- 强一致性(ZAB 协议保证)
- 天然支持公平锁 & 可重入
- 无死锁风险(临时节点随会话销毁)
❌ 缺点:
- 性能较低:频繁创建/删除节点 + 网络往返
- 运维复杂:需维护 ZK 集群
- 羊群效应:大量 Watcher 可能引发性能抖动(可通过"只监听前驱"优化)
📌 适用场景:对一致性要求极高、并发不极端的场景(如配置管理、选主)
六、三大方案对比总结
| 维度 | 数据库 | Redis | ZooKeeper |
|---|---|---|---|
| 实现复杂度 | ⭐ | ⭐⭐ | ⭐⭐⭐ |
| 性能 | ❌ 极低 | ✅ 极高 | ⚠️ 中等 |
| 一致性 | ✅ 强(ACID) | ⚠️ 最终一致(主从异步) | ✅ 强(ZAB) |
| 自动过期 | ❌ 需手动 | ✅ 支持 | ✅ 临时节点 |
| 公平性 | ❌ | ❌ | ✅ 支持 |
| 高可用 | 依赖 DB HA | Redis Cluster | ZK 集群(≥3 节点) |
| 典型场景 | 低并发遗留系统 | 秒杀、缓存重建 | 分布式协调、选主 |
💡 一句话选型建议:
- 要性能 → 选 Redis
- 要强一致 → 选 ZooKeeper
- 已有 DB 且并发低 → 用数据库
七、避坑指南:常见错误实践
❌ 错误 1:Redis 锁不设超时
后果 :服务 crash 后锁永远不释放
正解 :必须用EX设置 TTL
❌ 错误 2:释放锁时不校验 value
风险 :A 释放了 B 的锁,导致并发安全问题
正解 :Lua 脚本比对 owner
❌ 错误 3:ZooKeeper 使用永久节点
后果 :客户端宕机后锁残留
正解 :必须用临时节点(Ephemeral)
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!