一、基于数据库
1. 基于唯一索引(乐观锁)
原理:利用数据库表的唯一约束,插入成功即获取锁,删除即释放锁。
CREATE TABLE distributed_lock (
lock_key VARCHAR(64) PRIMARY KEY,
holder VARCHAR(64) NOT NULL,
expire_at DATETIME NOT NULL,
INDEX idx_expire (expire_at)
);
加锁:INSERT INTO distributed_lock VALUES ('order:1001', 'node-1', NOW() + INTERVAL 30 SECOND) → 插入成功=获取锁,唯一键冲突=锁被占用。
释放锁:DELETE FROM distributed_lock WHERE lock_key = 'order:1001' AND holder = 'node-1'
缺陷:没有内置过期机制,需要定时任务清理过期锁;数据库是单点故障。
2. 基于行锁(悲观锁)
SELECT * FROM distributed_lock WHERE lock_key = 'xxx' FOR UPDATE;
事务提交后释放行锁。需要关闭自动提交(autocommit=0),否则 FOR UPDATE 不生效。
缺陷:数据库连接数瓶颈;MySQL 5.7 前 FOR UPDATE 可能导致间隙锁死锁。
数据库方案总结
- 优点:实现简单,无需额外组件
- 缺点:性能差(DB 瓶颈),无自动过期,单点故障,不适合高并发
二、基于 Redis
1. 单节点 Redis 锁
加锁 —— SET NX + EX 原子命令(Redis 2.6.12+):
SET lock_key unique_value NX EX 30
NX:仅 key 不存在时设置(Not eXists)EX 30:30 秒自动过期,防止死锁unique_value:用 UUID + 线程ID,释放锁时校验身份
释放锁 —— Lua 脚本保证原子性(先查再删,避免误删他人锁):
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Java 示例(Jedis):
String lockKey = "order:1001";
String requestId = UUID.randomUUID() + ":" + Thread.currentThread().getId();
// 加锁
String result = jedis.set(lockKey, requestId, new SetParams().nx().ex(30));
boolean locked = "OK".equals(result);
// 释放锁
String script = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
2. Redisson 看门狗机制
Redisson 封装了 Redis 锁,核心特性:
- 自动续期(Watchdog):加锁时不设过期时间,后台定时线程每 10 秒续期到 30 秒,锁释放后停止续期
- 可重入:内部用 hash 记录重入次数
- 自旋等待:默认自旋获取,支持设置等待超时
RLock lock = redisson.getLock("order:1001");
try {
lock.lock(); // 阻塞直到获取锁,Watchdog 自动续期
// 业务逻辑
} finally {
lock.unlock();
}
也支持指定过期时间(此时不触发 Watchdog):
lock.lock(10, TimeUnit.SECONDS); // 10 秒后自动释放,不会续期
3. Redlock 算法(多节点)
背景:单节点 Redis 锁在 master 宕机后可能丢失(主从异步复制,已获取的锁未同步到 slave)。
Redlock 算法(Redis 作者 antirez 提出):
- 获取当前毫秒时间戳
- 依次向 N 个独立 Redis 节点请求锁,每个节点超时时间远小于锁 TTL
- 只有当超过半数(N/2+1)节点加锁成功,且总耗时 < 锁 TTL,才算获取成功
- 如果失败,向所有节点发送释放命令
- 锁有效时间 = TTL - 总耗时,到期后需续期
Java 实现:
RedissonRedLock redLock = redisson.getRedLock(
redisson.getLock("lock1"),
redisson.getLock("lock2"),
redisson.getLock("lock3")
);
redLock.lock();
注意:Redlock 存在争议(Martin Kleppmann 认为缺乏 fencing token 不安全),高安全性场景建议用 ZooKeeper/Etcd。
Redis 方案总结
- 优点:高性能(单机 10w+ QPS),天然过期机制,Redisson 开箱即用
- 缺点:单节点有丢失风险;Redlock 实现复杂且有争议;不保证强一致性
三、基于 ZooKeeper
核心原理
利用 ZK 的临时顺序节点 + Watch 机制实现:
- 所有请求者在
/lock下创建临时顺序节点(EPHEMERAL_SEQUENTIAL),如/lock/000000001 - 获取
/lock下所有子节点,按序号排序 - 如果自己是最小序号节点 → 获取锁
- 如果不是最小 → 对前一个节点(比自己小且序号最接近的)注册 Watch
- 前一个节点释放后,Watch 通知自己,重复步骤 2
为什么是安全的
- 临时节点:客户端断开连接(宕机、网络中断)后 ZK 自动删除节点,避免死锁
- 顺序节点:严格按创建顺序排队,公平锁
- Watch 通知:只唤醒下一个排队者,避免惊群效应
命令交互示意
Client A: create -e -s /lock/request → /lock/request_0001
Client B: create -e -s /lock/request → /lock/request_0002
Client C: create -e -s /lock/request → /lock/request_0003
A 最小 → A 获取锁
B watch A
C watch B
A 释放 → ZK 通知 B → B 获取锁 → C watch B
Curator 框架
InterProcessMutex lock = new InterProcessMutex(curatorClient, "/lock");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
lock.release();
}
Curator 封装了顺序节点创建 + Watch + 重试逻辑,一行代码即可使用。
ZK 方案总结
- 优点:强一致性(ZAB 协议),自动释放(临时节点),公平锁(顺序节点),无惊群效应(Watch 链)
- 缺点:性能低于 Redis(选举+写磁盘),部署维护成本高,适合一致性要求高于性能的场景
四、基于 Etcd
与 ZK 同属 CP 系统(Raft 协议),类似机制:
- 利用 Lease(租约) 实现自动过期
- 利用 Revision(全局递增版本号) 实现顺序排队
- 利用 Watch 实现等待通知
// Jetcd 示例
Lease lease = client.getLeaseClient();
long leaseId = lease.grant(30).get().getID();
Lock lock = new Lock(client, leaseId); // 注意是 etcd Lock,非 JUC Lock
lock.lock();
优点:Raft 协议比 ZAB 更简洁;Lease 机制比临时节点更灵活;社区活跃 缺点:和 ZK 一样性能不如 Redis;Go 生态原生,Java 封装不如 Curator 成熟
五、方案对比与选型
| 维度 | 数据库 | Redis | ZooKeeper | Etcd |
|---|---|---|---|---|
| 性能 | 低 | 高(10w+ QPS) | 中 | 中 |
| 一致性 | 取决于DB | AP(可能丢失) | CP(强一致) | CP(强一致) |
| 自动释放 | 需定时清理 | TTL 自动过期 | 临时节点自动删 | Lease 自动过期 |
| 公平性 | 手动实现 | 手动实现 | 天然支持 | 天然支持 |
| 可重入 | 手动实现 | Redis Hash | 手动实现 | 手动实现 |
| 运维成本 | 低(复用DB) | 中 | 中-高 | 中 |
| 适用场景 | 低并发/无额外组件 | 高并发/性能优先 | 强一致性/金融 | 云原生/K8s |
选型建议:
- 高并发 + 允许偶尔锁丢失 → Redis(Redisson)
- 强一致性 + 金融级可靠性 → ZooKeeper / Etcd
- 快速原型 + 已有数据库 → 数据库唯一索引
- K8s 环境 → Etcd(天然集成)
六、常见面试追问点
- Redis 锁过期了但业务还没执行完怎么办? → Redisson Watchdog 自动续期;或业务内异步定时续期
- 主从切换导致锁丢失? → Redlock 多节点投票;或 ZooKeeper CP 方案
- ZooKeeper 羊群效应(惊群)怎么解决? → 只 Watch 前一个节点,不 Watch 全部子节点
- ZK 和 Redis 混合方案什么时候用? → 用 ZK 选主(强一致),Redis 分片锁(高性能)
- fencing token 是什么? → 递增令牌,锁持有者携带 token 操作共享资源,资源端校验 token 防止过期锁误操作