为什么需要分布式锁?本地锁为什么不够用?
简化版
本地锁(synchronized、ReentrantLock)只在单个 JVM 进程内有效——它锁的是本进程内存里的对象。一旦服务部署了多个实例(集群),不同实例是不同进程、不同内存,本地锁互相看不见,多个实例可以同时进入「临界区」,本地锁就失效了。分布式锁是把锁放到一个所有实例都能访问的外部存储(Redis、ZooKeeper、数据库)上,让跨进程、跨机器的多个实例竞争同一把锁,从而在分布式环境下保证「同一时刻只有一个实例能操作共享资源」。
详细版
本地锁的作用范围:synchronized 锁的是 JVM 里的对象监视器,ReentrantLock 靠 AQS 的内存状态——它们的「锁」都存在当前进程的内存里,只能保证同一个 JVM 内的多个线程互斥。
为什么集群下失效:假设订单服务部署了 3 个实例做负载均衡。用户重复提交,请求被分到实例 A 和实例 B。A 的 synchronized 只挡得住 A 内部的线程,挡不住 B——A 和 B 是两个独立进程、各自有各自的锁对象,它们同时都能进入「扣库存」逻辑,导致超卖。
分布式锁的本质:把「谁持有锁」这个状态外置到一个所有实例都能访问的公共组件上(Redis 的一个 key、ZooKeeper 的一个节点、数据库的一行),大家去抢这个公共资源,抢到的才算持锁。这样跨进程也能互斥。
分布式锁必须满足的特性:
- 互斥:任意时刻只有一个客户端持锁。
- 防死锁:持锁者宕机也能自动释放(靠过期时间/临时节点)。
- 谁加锁谁释放:不能误删别人的锁。
- 高可用、高性能:加解锁要快、锁服务不能单点。
完整版教学
一、一个超卖的例子
商品库存 100,两个用户同时下单,请求分别打到集群里的实例 A、B。理想流程是「读库存→判断>0→扣减」这三步要原子。但:
- 实例 A 用了
synchronized,实例 B 也用了synchronized。 - A 的线程和 B 的线程同时读到库存 = 1,都判断 > 0,都执行扣减,库存变成 -1,超卖。
因为 A、B 是两个 JVM,synchronized 各锁各的,无法跨进程互斥。这时就需要一把「A 和 B 都认」的锁——分布式锁。
二、从单机锁到分布式锁的演进
- 单线程:不用锁。
- 单进程多线程:用本地锁(
synchronized/Lock)就够,因为锁状态在共享的进程内存里,所有线程都看得见。 - 多进程/集群:进程之间不共享内存,本地锁失效,必须把锁状态放到进程之外的共享存储——这就是分布式锁。
关键转变是「锁状态的存放位置」:从进程内存 → 外部共享组件。谁能原子地在这个共享组件上「占坑」,谁就持锁。
记忆点:本地锁锁的是「本进程内存里的对象」,分布式锁锁的是「所有进程都能看到的外部资源」。集群 = 多进程 = 本地锁失效 = 需要分布式锁。
三、分布式锁的三种主流实现
- 基于 Redis:用
SET key value NX EX原子地占坑,最常用、性能高。配合 Redisson 的看门狗自动续期、Lua 脚本保证解锁原子性。 - 基于 ZooKeeper:用临时顺序节点,天然有序、天然防死锁(会话断连节点自动删)、可 watch 前驱实现公平锁与阻塞等待。一致性强,但性能不如 Redis。
- 基于数据库:用唯一索引/
SELECT ... FOR UPDATE/乐观锁版本号实现。简单但性能差、易有单点,一般作为轻量兜底。
四、不是所有并发都要分布式锁
分布式锁有成本(网络往返、锁服务压力、复杂度),能不用尽量不用:
- 能用数据库原子操作解决的,优先用它:如
update stock set count=count-1 where id=? and count>0,靠数据库行锁 + 条件更新就能防超卖,比引入分布式锁更简单可靠。 - 能用乐观锁(版本号)解决的,用乐观锁:冲突不激烈时性能更好。
- 确实需要跨多步骤、跨多资源保证互斥时,才上分布式锁。
滥用分布式锁会把「本可以由数据库原子性解决的问题」复杂化,还引入锁服务的可用性依赖。
五、分布式锁落地自检
判断是否真的需要分布式锁,可以问 3 个问题:是否多实例同时竞争同一资源、是否数据库唯一约束或乐观锁已经足够、是否允许把操作串行化到 MQ。比如防止定时任务多实例重复跑适合分布式锁;而创建唯一订单更适合唯一索引加幂等,不一定要先抢锁。
六、常见误区与追问
这道题不能只背概念,要把「为什么需要分布式锁」放回真实分布式系统里解释:谁发起、谁协调、状态如何变化、失败后怎么兜底,以及它和性能、可用性、一致性的取舍。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 分布式锁用于多节点共同竞争同一临界资源时保证互斥,单机 synchronized 只在一个 JVM 内有效 | 不要停在名词解释 |
| 流程机制 | 多个实例同时请求资源 -> 本地锁只能约束单进程 -> 竞争同一外部资源 -> 通过 Redis/ZooKeeper 获取全局锁 -> 执行业务后安全释放 | 说明触发方、参与方、状态变化和兜底 |
| 工程取舍 | 库存服务部署 3 个实例时,本地锁只能锁住各自 JVM,仍可能 3 台同时扣同一商品库存 | 分布式锁只能控制临界区进入,不能替代业务幂等和状态校验 |
为什么需要分布式锁 面试拆解:
1. 多个实例同时请求资源
2. 本地锁只能约束单进程
3. 竞争同一外部资源
4. 通过 Redis/ZooKeeper 获取全局锁
5. 执行业务后安全释放
记忆钩子:先说为什么单机锁失效,再说明加锁、过期、续期、解锁、防误删和故障恢复;回答时要紧扣「为什么需要分布式锁」这道题,不要把相邻概念混成一段泛泛的分布式套话。
- 误区:用了 synchronized 就能防并发。 它只在单 JVM 生效,多实例部署后每个进程都有自己的锁。
- 误区:分布式锁能保证业务一定正确。 锁只保证互斥,业务还要校验库存、状态和幂等。
- 误区:所有并发问题都上分布式锁。 很多场景可用数据库唯一约束、乐观锁、队列串行化替代。
- 追问:分布式锁适合什么? 定时任务防重复、库存热点保护、资源互斥操作。
- 追问:锁的核心要求有哪些? 互斥、防死锁、唯一持有者释放、可重入可选、自动过期和高可用。
- 追问:锁粒度怎么选? 粒度太大影响吞吐,太小保护不足,要按业务资源 ID 加锁。
七、加强记忆
本地锁(synchronized/ReentrantLock)锁的是本进程内存里的对象,只在单 JVM 内互斥;服务集群化后变成多进程多内存,本地锁互相看不见、失效,会导致超卖等问题。分布式锁把「锁状态」外置到所有实例都能访问的共享组件(Redis/ZooKeeper/数据库),跨进程竞争同一把锁实现互斥。它必须满足互斥、防死锁、谁加谁解、高可用。但别滥用——能用数据库原子更新或乐观锁解决的就别上分布式锁。