Java 并发进阶:分布式锁 (Distributed Lock) vs 本地锁 (Synchronized)
1. 什么是分布式锁?
在微服务或分布式系统环境中,当系统由多个独立的进程或节点组成时,如果这些不同的进程需要协调对同一个共享资源(如数据库中的某条记录、共享文件、或某个唯一的业务 ID)的访问,传统的本地锁(如 synchronized 或 ReentrantLock)就无法生效了。
分布式锁 (Distributed Lock) 就是为了解决这个问题而生。它是一种在分布式系统中,用于控制多个进程对共享资源进行并发访问的机制,以确保同一时刻只有一个进程(或线程)能够获得资源的使用权。
- 核心思想:让处于不同 JVM 进程中的线程,在访问同一个共享资源时,也能实现互斥。
2. 分布式锁与 synchronized (本地锁) 的核心区别
synchronized 和 ReentrantLock 都是本地锁 (Local Lock),它们的作用范围仅限于单个 JVM 进程内部的多个线程。而分布式锁则突破了这个限制。
| 特性 | 本地锁 (synchronized / ReentrantLock) |
分布式锁 (Distributed Lock) |
|---|---|---|
| 作用范围 | 单个 JVM 进程内部的多个线程 | 多个 JVM 进程/节点之间的协调 |
| 锁的粒度 | 通常是对象实例或类 | 通常是某个资源 ID (String) |
| 实现方式 | JVM 层面关键字或 JUC API,操作本地内存中的 Monitor 或 AQS |
依赖外部协调服务 (如 Redis, ZooKeeper, Etcd) |
| 互斥性保证 | JVM 层面,操作系统保证 | 通过外部服务保证,需要考虑网络、服务宕机等复杂情况 |
| 并发性能 | 性能高,无网络开销 | 性能相对较低,有网络通信开销 |
| 复杂性 | 较低,JVM 自动管理或 JUC API 提供 | 较高,需要考虑一致性、容错性、死锁、锁续期等 |
| 解决问题 | 单个进程内的线程安全 | 多个进程间的资源竞争 |
3. 分布式锁的常见实现方式
分布式锁通常基于以下几种外部协调服务实现:
-
基于 Redis (Redis Distributed Lock)
- 核心命令:
SET key value NX PX milliseconds(NX:只在 key 不存在时设置;PX:设置过期时间,单位毫秒)。 - 实现原理:利用 Redis 的
SET NX PX命令的原子性。当一个客户端成功设置了 key,就获得了锁,并设置过期时间防止死锁。释放锁时,通过 Lua 脚本原子性地判断 value 是否为自己的,然后删除 key。 - 优点:实现相对简单,性能高(Redis 是内存数据库)。
- 缺点:
- 单点故障:如果只用一个 Redis 实例,该实例宕机锁就失效。
- 可靠性问题:在主从切换、网络分区等复杂场景下,经典的 Redlock 算法虽然尝试解决,但仍存在争议和潜在问题。
- 核心命令:
-
基于 ZooKeeper (ZooKeeper Distributed Lock)
- 核心机制:利用 ZooKeeper 的临时顺序节点 (Ephemeral Sequential Nodes) 特性。
- 实现原理:
- 客户端尝试在 ZooKeeper 上创建一个临时顺序节点 (例如
/locks/lock_node/lock-00000001)。 - 客户端获取
/locks/lock_node下的所有子节点,判断自己创建的是否是序号最小的节点。 - 如果是,则获得锁。
- 如果不是,则监听比自己序号小的前一个节点。当前一个节点被删除(锁释放)时,自己就会被唤醒。
- 客户端尝试在 ZooKeeper 上创建一个临时顺序节点 (例如
- 优点:强一致性(基于 Zab 协议),可靠性高,具备自动重入和死锁避免(临时节点断开连接自动删除)。
- 缺点:性能相对较低(网络延迟和 ZooKeeper 本身的处理速度),实现相对复杂。
-
基于 Etcd (Etcd Distributed Lock)
- 核心机制:类似 ZooKeeper,利用其强一致性、租约 (Lease) 和事务 (Transaction) 特性。
- 实现原理:创建带有租约的 key,然后通过事务检查是否是最小的 key。
- 优点:与 ZooKeeper 类似,但 API 更现代化,常用于 Kubernetes 等云原生环境中。
4. 实现分布式锁需要考虑的关键问题
设计和实现一个可靠的分布式锁,需要全面考虑以下几点:
- 互斥性 (Mutual Exclusion):在任何时刻,只能有一个客户端持有锁。这是锁最基本的保证。
- 死锁 (Deadlock):客户端崩溃或异常时,锁必须能够被正确释放,避免其他客户端永远无法获取锁。通常通过设置锁的过期时间或心跳续期机制来解决。
- 重入性 (Reentrancy):同一个客户端(线程)在持有锁的情况下,能否再次获取该锁?
- 锁续期 (Lease Renewal / Fencing):如果业务处理时间超出了锁的过期时间,锁可能被其他客户端获取,导致数据不一致。需要看门狗 (Watchdog) 机制来自动续期。
- 性能 (Performance):锁的获取和释放速度是否满足业务需求。
- 高可用和可靠性 (High Availability & Reliability):在协调服务(Redis, ZK)集群部分节点宕机或网络分区时,锁服务是否还能正常提供。
总结:分布式锁的实现远比本地锁复杂,需要综合考虑一致性、可用性、分区容错性等多个方面,且没有银弹。选择何种实现方式,取决于具体业务对性能、可靠性和复杂性的权衡。