前言:从"会用"到"懂选型"的阵痛
昨天面试,聊到分布式锁,我本以为 SET NX PX 和 Lua 脚本已经够用了。结果面试官追问了一句:"那如果让你在 Redis、ZooKeeper 和 etcd 里选一个做分布式锁,你会怎么选?为什么金融级场景不推荐 RedLock?
那一刻,我意识到自己只懂了"应用",没懂"架构权衡"。今天,我把这些关于硬件成本、性能和高可靠性的深度对比整理出来,希望能避开我踩过的坑。
一、使用场景:别只说"库存扣减"
面试官问"在哪用",他其实想听的是:你懂不懂业务边界?
1. 防并发写(数据一致性)
比如下单扣库存。
"我会用分布式锁保证同一时间只有一个线程能扣减某个商品的库存,防止超卖。"
2. 防重复执行(幂等性)
这是进阶加分项 。比如支付回调。
"支付渠道可能会发三次回调。我会用订单号作为锁的 Key,保证同一订单在同一时间只被处理一次,防止重复加积分。"
3. 分布式任务调度
比如每天凌晨的定时任务。
"三台机器部署了任务,但只希望一台跑。我会用分布式锁抢锁,抢到的那台才执行。"
二、具体怎么用:从"SETNX"到"看门狗"
1. 加锁:一句话背后的细节
"我一般用 Redis 的
SET key random_value NX PX 30000。
random_value是为了防止误删别人的锁;PX是为了防止死锁。"
2. 解锁:Lua 脚本是底线
"解锁必须用 Lua 脚本 。因为如果直接
DEL,万一我的业务还没跑完锁就过期了,我可能就把别人刚加的锁给删了。Lua 能保证'判断是不是我的锁'和'删除锁'这两个动作是原子的。"
3. 锁续期:看门狗机制
"如果业务执行时间不确定,用 Redisson 的看门狗。它会开个后台线程定时给锁续期,防止业务没跑完锁就没了。"
三、RedLock 的"罗生门":这才是面试深水区
1. 为什么要有 RedLock?
"单机 Redis 有个问题:如果 Master 刚加了锁就挂了,还没同步给 Slave,Slave 升级成 Master 后,别的线程又能加锁了,这就导致锁失效。
RedLock 就是为了解决这个问题。它不像主从复制,而是同时向 N 个独立的 Redis 节点申请锁 ,只有**过半节点(N/2+1)**加锁成功,才算真的拿到锁。"
2. 剑桥大神 Martin Kleppmann 的"致命一击"
"但后来 Martin Kleppmann(剑桥教授,《DDIA》作者)发文章怼了 RedLock。
他的核心观点是:RedLock 依赖系统时钟 。如果服务器发生时钟跳跃(比如 NTP 强制同步),锁的有效期计算就错了,可能导致两个客户端同时持有锁。
而且,Redis 没有 Fencing Token (栅栏令牌)。就算锁没过期,如果客户端发生 GC 停顿,恢复后拿着旧锁去写数据,还是会出问题。他认为,分布式锁的本质是共识问题,应该用 ZooKeeper 或 etcd 这种基于 Raft 算法的系统。"
3. Redis 之父 Antirez 的反击
"Antirez(Redis 作者)也回应了。他说 Martin 把问题理想化了。
他认为,任何分布式系统都无法完全避免时钟问题 ,RedLock 在计算锁有效性的时候已经考虑了网络延迟。而且,他建议资源层(比如数据库)应该自己做幂等校验,不能完全依赖锁服务。
但他也承认,如果业务需要绝对的强一致性,确实应该考虑 etcd。"
四、深度对决:Redis vs ZooKeeper vs etcd
我从硬件成本、性能、高可靠性三个维度整理好了:
| 维度 | Redis (RedLock) | ZooKeeper | etcd |
|---|---|---|---|
| 一致性协议 | 无(基于时钟和多数派) | **ZAB (Paxos 变种)** | Raft |
| 共识保证 | 弱(依赖时钟,无 Fencing) | 强(顺序节点,有 Ephemeral 节点) | 强(Lease 机制,有 Revision) |
| 性能 | 极高(内存操作,~1ms) | 中等(磁盘序写,~10ms) | 较高(内存 + WAL,~5ms) |
| 硬件成本 | 低(普通服务器即可,5节点) | 高(需要 SSD,3~5 节点) | 中高(需要 SSD,3~5 节点) |
| 运维复杂度 | 低(部署简单) | 高(调优复杂,JVM 依赖) | 中(云原生友好,API 简单) |
| 典型场景 | 防重复提交、防缓存击穿 | 金融级核心协调、命名服务 | Kubernetes 底座、强一致锁 |
1. 硬件成本:Redis 是"经济适用男"
"Redis 对硬件要求最低,普通虚拟机甚至容器就能跑,5 个节点成本很低。而 ZooKeeper 和 etcd 为了保证数据不丢,必须依赖 SSD 磁盘做 WAL(预写日志),而且 etcd 对内存和 CPU 也有一定要求,成本明显高一个档次。"
2. 性能:Redis 遥遥领先
"如果你的业务是秒杀,对延迟极其敏感,Redis 是首选,QPS 轻松过 10 万。ZooKeeper 和 etcd 每次加锁都需要写磁盘(WAL),虽然 etcd 比 ZK 快,但比起纯内存的 Redis,还是慢了一个数量级。"
3. 高可靠性:ZK/etcd 才是"定海神针"
"这是最关键的区别。Redis 的 RedLock 没有 Fencing Token。如果客户端 GC 了 30 秒,锁过期了,它醒过来还是可能去改数据。
而 ZooKeeper 的临时顺序节点 和 etcd 的 Revision(版本号) 天然就是 Fencing Token。数据库那边只要记录一下'当前处理的最大版本号',就能拒绝旧请求。所以,涉及钱的核心链路,绝对不用 Redis,只用 ZK 或 etcd。"
五、最后的防线:数据库层面的兜底策略
面试官问:"如果以上中间件全挂了,你怎么保证数据不错?" 这才是架构师的试金石。
"分布式锁是'防并发'的手段,但不是数据一致性的唯一真理 。我必须在数据库层 留有最后的兜底手段,遵循**'防御性编程'**原则。"
1. 唯一约束(Unique Constraint):最后的铁闸
"这是最硬的底牌。无论锁怎么失效,只要请求到了数据库,我就靠唯一索引来保证。
比如订单号 或支付流水号 ,我会在数据库里建唯一索引。即使分布式锁失效导致两个请求同时进来,数据库也会直接抛
DuplicateKeyException,事务回滚,数据绝对不会重复。"
2. 乐观锁(Optimistic Locking):无锁化的并发控制
"对于更新操作,比如库存扣减 ,用乐观锁。
表里加个
version字段,更新时带上版本号:
UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE product_id = ? AND version = ? AND stock > 0;如果更新返回 0 行,说明在我操作期间数据被改了,我就重试或失败。这种方式不依赖外部锁服务,性能极高,是数据库层面的自愈能力。"
3. 悲观锁(Pessimistic Locking):核心资金链路
"如果是账户余额扣减 这种绝对不能错的场景,直接用数据库的悲观锁 (
SELECT ... FOR UPDATE)。虽然性能差一点,但它利用数据库本身的行锁机制,保证了在事务提交前,这行数据不会被别人动。这是最可靠的兜底,比任何中间件都稳。"
4. 中间件挂了怎么办?(降级策略)
"如果 Redis/ZK/etcd 真的全挂了,启动降级预案:
- 限流:直接拒绝新的下单请求,提示'系统繁忙'。
- 本地锁 :如果还有部分实例存活,用 JVM 的
synchronized或ReentrantLock做单机防护(虽然集群下不完美,但能拦住单机内的并发)。- 异步化处理 :把请求放入本地队列,等中间件恢复后再处理,或者人工介入对账。
但无论如何,数据库的唯一索引和乐观锁永远是最后一道防线,确保即使最坏情况发生,数据也不会乱。"
六、面试时的"黄金结构"
下次被问到,试着按这个逻辑说,直接封神:
- 先定场景:"我主要在防超卖和防重复处理时用锁。"
- 再说实现 :"核心是用
SET NX PX和 Lua 脚本,业务耗时不确定时用看门狗。" - 接着谈争论:"RedLock 解决了主从切换问题,但 Martin Kleppmann 指出了它对时钟的依赖风险,缺乏 Fencing 机制。"
- 然后选型对比 :"所以选型要看业务:追求极致性能 用 Redis;金融级强一致用 ZK 或 etcd。"
- 最后升华(兜底) :"但任何中间件都不是 100% 可靠的 。我必须在数据库层留兜底:用唯一索引 防重复,用乐观锁 做并发控制,用悲观锁保核心资金。这样即使锁服务全挂了,业务数据依然不会错。"
这段话的逻辑是:业务 → 技术 → 理论争议 → 架构权衡 → 容灾兜底。 面试官会瞬间觉得你不仅有技术深度,更有工程落地的安全意识。
"注:本文讨论的分布式锁方案,主要针对高并发下的业务协调场景。对于涉及资金最终清算、账务核心等强监管领域,建议直接依赖数据库的单点强一致性或成熟的商业分布式事务解决方案(如 TCC),而非自研锁方案。"
七、写在最后
这次面试虽然因为没答好"兜底策略"而遗憾,但也逼着我把这块硬骨头啃了下来。
空窗期很难熬,但只要每天能把一个"不知道"变成"能讲清楚",心里就踏实一点。Martin 和 Antirez 的争论,以及数据库兜底策略,本质上是在提醒我们:架构师的价值不在于用了多炫的技术,而在于在极端情况下,依然能保证系统的正确性和数据的完整性。