分布式锁是什么实现的
1️⃣ 考察意图
面试官想考察你对分布式锁实现原理的深度理解和工程选型能力,而非简单背诵方案。这是典型的“系统设计+工程取舍”题,刁钻点在于:能否说清不同实现(Redis/ZK/DB)在CAP理论下的本质差异,以及实际落地中如何避免死锁、锁续期、脑裂等坑。答好了能展示你对一致性、可用性、性能的权衡判断,以及处理分布式并发问题的实战经验。
2️⃣ 标准答
分布式锁的核心要求:互斥性(同一时刻只有一个客户端持有)、高可用(锁服务不单点)、死锁避免(锁自动释放)、可重入(同一线程可重复获取)。实现方案主要有三种,各有取舍:
- 基于 Redis 实现(高性能,弱一致性)
- 基础方案:用
SET key value NX PX 30000原子命令,NX 保证互斥,PX 设置过期时间防死锁。释放锁用 Lua 脚本if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end,确保只释放自己的锁(通过 value 存唯一 ID)。 - 坑:业务执行超时导致锁自动释放,其他线程获取锁后原线程又释放别人的锁。解法:引入看门狗(Watchdog)机制,如 Redisson 的
lock()默认每 10 秒续期一次,续期失败则自动释放。 - Redlock 算法:在 N/2+1 个 Redis 节点上同时加锁,解决主从切换时的锁丢失问题。但工程上争议大(Martin Kleppmann 指出其依赖时钟同步,存在脑裂风险),实际更推荐用 ZK 或 etcd 做强一致锁。
- Trade-off:Redis 锁性能极高(单节点 QPS 10万+),但一致性弱(主从异步复制可能丢锁),适合允许短暂不一致的场景(如秒杀库存扣减)。
- 基于 ZooKeeper 实现(强一致性,低性能)
- 原理:利用 ZK 的临时顺序节点(EPHEMERAL_SEQUENTIAL)。客户端在锁目录下创建节点,获取所有子节点列表,若自己是最小编号则获得锁;否则监听前一个节点的删除事件,阻塞等待。
- 优点:临时节点特性保证客户端崩溃后锁自动释放(无死锁);顺序节点天然避免羊群效应(只唤醒下一个等待者)。
- 坑:ZK 的会话超时(Session Expired)可能导致锁提前释放。解法:设置合理的 session timeout(如 15 秒),并在业务代码中处理
ConnectionLossException重试逻辑。 - Trade-off:ZK 写性能约 1万 QPS(受 ZAB 协议限制),且加锁过程需多次网络交互(创建节点+监听+通知),延迟较高。适合强一致性要求高的场景(如分布式任务调度、配置更新)。
- 基于数据库实现(简单,低吞吐)
- 悲观锁:
SELECT ... FOR UPDATE利用行锁互斥,事务提交后自动释放。坑:数据库单点故障、行锁可能升级为表锁(无索引时)、死锁检测开销大。 - 乐观锁:用版本号字段
UPDATE table SET version=version+1 WHERE id=1 AND version=old_version,适合读多写少场景。坑:高并发下大量重试,性能急剧下降。 - 适用场景:已有数据库且并发量低(<100 QPS)的遗留系统,避免引入新中间件。
总结:选型时遵循“一致性要求 > 性能要求”原则。强一致选 ZK/etcd,高性能选 Redis,简单场景选 DB。实际落地中,推荐 Redisson(Redis)或 Curator(ZK) 这类成熟客户端,避免重复造轮子。
3️⃣ 答题模板(30 秒电梯版)
“这个问题我从三个主流实现层面回答:第一,Redis 方案用 SETNX+Lua 保证原子性,配合看门狗续期解决超时问题,但主从切换可能丢锁;第二,ZK 方案用临时顺序节点实现强一致,通过监听前一个节点避免羊群效应,但性能较低;第三,数据库方案用 SELECT FOR UPDATE 或版本号,简单但吞吐有限。总结一句:选型核心是权衡一致性、性能和复杂度,强一致选 ZK,高性能选 Redis,简单场景选 DB。”
4️⃣ 高频追问 & 应对
追问 1:Redlock 算法有什么缺陷?为什么很多专家不推荐?
应对策略:指出 Martin Kleppmann 在《分布式系统设计》中的批评:Redlock 依赖时钟同步,若节点时钟发生跳跃(如 NTP 同步),锁可能提前过期或被多个客户端同时持有。此外,Redlock 的脑裂问题:客户端加锁成功后,在写入数据前发生 GC pause,锁过期后其他客户端获取锁,导致数据不一致。工程替代方案:用 ZK 或 etcd 的租约(Lease)机制,或使用 Redis 的 WAIT 命令等待主从同步(牺牲性能换一致性)。
追问 2:如何实现分布式锁的可重入?
应对策略:用线程标识+计数器。Redis 方案:value 存
{threadId:count},加锁时先判断 value 中的 threadId 是否等于当前线程,是则 count+1 并重置过期时间;释放锁时 count-1,减到 0 才真正删除 key。ZK 方案:在临时节点中存储线程 ID,同一线程重复创建节点时直接返回成功。注意:可重入会增加锁的复杂度,非必要不实现(如 Redisson 默认支持,但需额外内存)。
追问 3:如果 Redis 主节点宕机,锁丢失怎么办?
应对策略:分场景处理。场景一:允许短暂不一致(如秒杀),直接接受丢锁,用业务幂等性兜底(如数据库唯一索引)。场景二:要求强一致,用 Redlock 或切换到 ZK。场景三:使用 Redis 哨兵模式,主从切换后新主节点可能没有锁数据,解法:在锁 key 上设置足够长的过期时间(如 30 秒),并配合业务超时检测(如用 TTL 监控锁是否被异常释放)。
5️⃣ 避坑 · 常见错误答法
- ❌ 只提 Redis 的 SETNX 命令,不说 Lua 脚本释放锁的原子性 → ✅ 必须强调“用 Lua 脚本保证检查+删除的原子性,避免误删其他线程的锁”
- ❌ 说 ZK 锁性能高,适合所有场景 → ✅ 明确 ZK 写性能约 1万 QPS,且加锁延迟高,只适合强一致场景
- ❌ 推荐自己实现分布式锁组件 → ✅ 强调“用 Redisson/Curator 等成熟客户端,避免处理边界情况(如网络分区、时钟跳跃)”
6️⃣ 简历呼应
- 如果你有 Redis 项目:从“Redisson 看门狗续期机制”切入,结合你项目中遇到的锁超时问题,展示你如何用 Lua 脚本优化原子性。
- 如果你有 ZK 项目:从“临时顺序节点避免羊群效应”切入,对比你项目中 ZK 和 Redis 锁的选型决策,强调一致性优先原则。
- 如果你是校招无项目:聚焦“分布式锁的 CAP 权衡”,引用 Redlock 争议和 ZK 的 ZAB 协议,展示你对理论的理解深度,并提一个简单的 Redis 锁 demo(如 Spring Boot 集成)。
- 《Redis 深度历险》- 分布式锁章节(Redlock 实现细节)
- 《ZooKeeper:分布式过程协同技术详解》- 临时节点与 Watcher 机制
- Martin Kleppmann - “How to do distributed locking”(Redlock 批判论文)
- Redisson 官方文档 - Lock 与 RLock 接口设计
- etcd 分布式锁实现(基于 Raft 的 Lease 机制)