在单体应用中,我们用本地锁(如 Java 的 synchronized、Python 的 threading.Lock)就能解决并发问题。但在分布式系统中,多个服务实例共享同一资源(如库存、订单号、分布式任务),本地锁完全失效 ------ 因为本地锁只能控制单个进程内的线程,无法跨机器、跨进程同步。此时,分布式锁就成了解决分布式并发问题的核心工具。
但分布式锁的使用门槛远高于本地锁:选型错误会导致性能瓶颈,实现不当会引发死锁、锁失效、数据不一致等严重问题,甚至比不加锁更糟。很多程序员对分布式锁的认知停留在「用 Redis 加个 SETNX 就行」,却忽视了锁的自动释放、重入性、公平性、高可用等关键特性,最终在生产环境踩坑。本文结合 Redis、ZooKeeper、数据库三大主流实现方案,拆解分布式锁的核心原理、选型依据、实战落地步骤与避坑指南,帮你彻底掌握分布式锁的正确用法。
一、分布式锁的核心需求:不止于「互斥」
一个合格的分布式锁,必须满足以下核心特性,缺少任何一个都可能导致线上故障。这些特性是设计和使用分布式锁的根本原则,也是区分「能用」和「好用」的关键。
- 互斥性:最核心的需求,同一时刻只能有一个服务实例持有锁,确保并发操作的原子性。
- 高可用:锁服务本身必须高可用,不能因为锁服务宕机导致整个系统无法运行。比如 Redis 分布式锁,必须避免单点故障,采用主从集群或哨兵模式。
- 自动释放:持有锁的实例如果宕机、网络中断,无法主动释放锁,必须有自动释放机制(如过期时间),避免死锁。
- 可重入性:同一个实例在持有锁的情况下,再次请求锁时能成功获取,避免自身死锁。比如一个方法调用另一个需要同一把锁的方法,可重入锁能避免锁冲突。
- 公平性:可选需求,按请求顺序获取锁,避免某些实例长期获取不到锁(饥饿问题)。但公平性通常会牺牲性能,需根据业务场景权衡。
- 高性能:加锁和释放锁的操作必须高效,不能成为系统的性能瓶颈。分布式锁的性能直接影响整个业务的吞吐量。
二、三大主流分布式锁实现方案:对比与选型
分布式锁的实现方案有很多,最主流的是 Redis 分布式锁 、ZooKeeper 分布式锁 、数据库分布式锁。三种方案各有优劣,适用于不同的业务场景,选型的核心是「匹配业务需求」,而非「追求最优性能」。
1. Redis 分布式锁:高性能首选,适合大部分业务场景
核心实现原理 :利用 Redis 的 SETNX 命令(SET if Not Exists),只有当键不存在时才会设置成功,返回 1 表示获取锁成功;返回 0 表示锁已被持有。同时为键设置过期时间,避免死锁。核心命令(Redis 2.6.12 及以上版本推荐使用):
plaintext
# SET key value NX PX expire_time
SET lock:order:123 unique_value NX PX 30000
NX:只有键不存在时才设置,确保互斥性。PX 30000:设置过期时间为 30 秒,自动释放锁。unique_value:唯一值(如 UUID + 线程 ID),用于释放锁时验证身份,避免误删其他实例的锁。
释放锁的正确方式:必须使用 Lua 脚本原子执行「判断唯一值 + 删除键」,避免因为网络延迟导致的误删锁问题。
lua
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
优点 :性能极高,加锁和释放锁的操作都是 O (1);支持过期时间自动释放;生态完善,有成熟的客户端(如 Redisson)支持。缺点 :锁的过期时间难以设置 ------ 设置太短,业务没执行完锁就释放了;设置太长,死锁时影响范围大;Redis 主从切换时,可能出现锁丢失(主库写入锁后宕机,未同步到从库,从库升级为主库后,其他实例可重新获取锁)。适用场景:大部分分布式业务场景,尤其是高并发、高性能要求的场景(如库存扣减、订单号生成、秒杀活动)。
2. ZooKeeper 分布式锁:高可靠首选,适合强一致性需求
核心实现原理:利用 ZooKeeper 的临时有序节点特性。多个实例竞争锁时,在指定节点下创建临时有序子节点;判断自己的节点是否是当前最小的节点,若是则获取锁成功;若不是,则监听前一个节点的删除事件,当前一个节点释放锁时,被唤醒再次判断。
核心特性:
- 临时节点:客户端与 ZooKeeper 断开连接时,临时节点自动删除,实现锁的自动释放,避免死锁。
- 有序节点:确保锁的公平性,按请求顺序获取锁。
- 监听机制:实现锁的高效等待,无需轮询,性能优于数据库锁。
优点 :高可靠性,不存在锁丢失问题;天然支持公平锁;自动释放锁,无需设置过期时间;支持可重入锁。缺点 :性能低于 Redis,加锁和释放锁需要创建和删除节点,涉及网络通信;部署和维护成本高,需要搭建 ZooKeeper 集群。适用场景:强一致性、高可靠性要求的场景(如分布式事务、主从切换、配置中心),并发量适中的业务。
3. 数据库分布式锁:最易实现,适合低并发场景
核心实现原理 :利用数据库的唯一索引特性。创建一张锁表,包含 lock_key(锁标识)、owner(持有锁的实例标识)、expire_time(过期时间)等字段;通过 INSERT 语句插入锁记录,唯一索引确保同一时刻只能插入一条记录,插入成功表示获取锁成功;释放锁时删除记录,或通过定时任务清理过期锁。
核心 SQL:
sql
-- 创建锁表
CREATE TABLE `distributed_lock` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`lock_key` VARCHAR(64) NOT NULL COMMENT '锁标识',
`owner` VARCHAR(64) NOT NULL COMMENT '持有锁的实例标识',
`expire_time` DATETIME NOT NULL COMMENT '过期时间',
UNIQUE KEY `uk_lock_key` (`lock_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁表';
-- 获取锁:插入记录,唯一索引确保互斥性
INSERT INTO distributed_lock (lock_key, owner, expire_time)
VALUES ('order_123', 'instance_1', DATE_ADD(NOW(), INTERVAL 30 SECOND));
-- 释放锁:删除记录
DELETE FROM distributed_lock WHERE lock_key = 'order_123' AND owner = 'instance_1';
优点 :实现最简单,无需额外依赖中间件;开发成本低,熟悉数据库操作的程序员就能实现。缺点 :性能最差,数据库的插入和删除操作涉及磁盘 IO,并发量高时会成为性能瓶颈;可靠性差,数据库宕机会导致锁服务不可用;容易出现死锁,需要定时任务清理过期锁;不支持可重入锁。适用场景:低并发、简单业务场景,快速原型开发,不需要高性能和高可用的场景。
三、分布式锁选型对比表:一眼选对方案
| 特性 | Redis 分布式锁 | ZooKeeper 分布式锁 | 数据库分布式锁 |
|---|---|---|---|
| 性能 | 极高(O (1)) | 中等(O (logn)) | 极低(O (n)) |
| 可靠性 | 高(需集群) | 极高(天然集群) | 低(单点风险) |
| 自动释放 | 支持(过期时间) | 支持(临时节点) | 支持(定时清理) |
| 可重入性 | 支持(Redisson) | 支持 | 不支持 |
| 公平性 | 不支持(可扩展) | 支持(有序节点) | 不支持 |
| 部署成本 | 低(单节点即可,集群推荐) | 高(需搭建集群) | 低(复用现有数据库) |
| 适用并发量 | 高并发(10w+ QPS) | 中并发(1w+ QPS) | 低并发(1k QPS 以下) |
| 典型场景 | 秒杀、库存、订单号 | 分布式事务、配置中心 | 小型业务、原型开发 |
四、分布式锁的 5 个高频坑点,90% 的人都踩过
分布式锁的坑点大多源于对「分布式场景的复杂性」认识不足,这些坑点隐蔽性强,开发环境难以复现,一旦出现就会导致线上故障。必须重点规避。
坑点 1:释放锁时不验证身份,导致误删其他实例的锁
这是最常见的坑点。比如实例 A 获取锁后,业务执行时间超过锁的过期时间,锁被自动释放;此时实例 B 获取到锁,开始执行业务;实例 A 业务执行完毕后,直接删除锁,导致实例 B 的锁被误删;随后实例 C 又能获取到锁,出现多个实例同时持有锁的情况,引发数据不一致。
解决方案 :加锁时设置唯一标识(如 UUID + 线程 ID),释放锁时必须验证该标识,只有属于自己的锁才能删除。Redis 中用 Lua 脚本原子执行验证和删除操作,ZooKeeper 中通过节点路径验证,数据库中通过 owner 字段验证。
坑点 2:锁的过期时间设置不合理,导致业务执行中断或死锁
过期时间设置太短,业务没执行完锁就释放了,导致并发问题;设置太长,一旦实例宕机,锁长时间无法释放,引发死锁,影响后续业务。
解决方案:
- 预估合理的过期时间:根据业务平均执行时间,设置 2-3 倍的过期时间,预留足够的缓冲。
- 实现锁的自动续期:使用「看门狗」机制,在业务执行过程中,定期检查锁是否即将过期,若业务还在执行,则自动延长锁的过期时间。Redis 的 Redisson 客户端已内置该机制,ZooKeeper 可通过延长会话超时时间实现。
坑点 3:忽视分布式锁的高可用,单点故障导致锁服务不可用
很多人在测试环境用单节点 Redis 实现分布式锁,生产环境也直接复用,一旦 Redis 节点宕机,整个分布式锁服务就会不可用,导致业务无法运行。
解决方案:
- Redis 分布式锁:采用主从集群 + 哨兵模式,或 Redis Cluster 集群,确保即使主节点宕机,从节点能快速切换,提供高可用服务;也可使用 Redlock 算法,部署多个独立的 Redis 节点,只有在大多数节点获取锁成功,才认为锁获取成功,进一步提高可靠性。
- ZooKeeper 分布式锁:搭建 3 节点或 5 节点的 ZooKeeper 集群,利用其天然的高可用特性。
- 数据库分布式锁:采用主从复制 + 读写分离,确保数据库的高可用。
坑点 4:滥用分布式锁,过度设计导致性能下降
分布式锁的性能远低于本地锁,且存在网络开销。很多程序员在不需要分布式锁的场景下滥用,比如:
- 单体应用中使用分布式锁,而非本地锁。
- 分布式系统中,资源不需要跨实例共享,却使用分布式锁。
- 高频读写的场景中,使用分布式锁,导致性能瓶颈。
解决方案 :能不用分布式锁,就不用;能用本地锁,就用本地锁。只有在资源需要跨实例共享的分布式场景下,才使用分布式锁。同时,尽量减少持有锁的时间,将锁的粒度控制在最小范围,比如只在修改数据的核心逻辑上加锁,而非整个业务流程。
坑点 5:不考虑锁的重入性,导致自身死锁
在同一个实例中,一个方法获取锁后,调用另一个需要同一把锁的方法,如果锁不支持重入性,会导致自身死锁 ------ 第二个方法无法获取锁,一直等待,而第一个方法持有锁无法释放。
解决方案:
- 选择支持可重入性的分布式锁实现方案,如 Redis 的 Redisson 客户端、ZooKeeper 的 Curator 客户端。
- 自定义实现可重入锁:在锁中记录持有锁的实例标识和重入次数,获取锁时,若为同一实例则增加重入次数,释放锁时减少重入次数,只有重入次数为 0 时才真正释放锁。
五、分布式锁实战落地:Redis 分布式锁(Redisson)最佳实践
Redis 分布式锁是大部分业务的首选方案,而 Redisson 是 Redis 官方推荐的 Java 客户端,提供了丰富的分布式锁实现(可重入锁、公平锁、读写锁、红锁等),内置看门狗机制,自动续期锁的过期时间,完美解决了分布式锁的大部分坑点。以下是 Redisson 分布式锁的实战落地步骤。
1. 引入依赖
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.5</version>
</dependency>
2. 配置 Redisson
yaml
spring:
redis:
host: localhost
port: 6379
password: 123456
database: 0
3. 实战使用可重入锁
java
运行
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public void createOrder(Long orderId) {
// 1. 获取锁对象:锁的标识为 "lock:order:" + orderId
RLock lock = redissonClient.getLock("lock:order:" + orderId);
try {
// 2. 获取锁:等待时间 10 秒,锁过期时间 30 秒
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("获取锁失败,请稍后重试");
}
// 3. 核心业务逻辑:扣减库存、生成订单
doCreateOrder(orderId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SystemException("获取锁被中断");
} finally {
// 4. 释放锁:必须在 finally 块中释放,确保锁一定会被释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private void doCreateOrder(Long orderId) {
// 业务逻辑
}
}
4. 核心特性说明
- 可重入性 :Redisson 的
RLock实现了java.util.concurrent.locks.Lock接口,支持可重入锁。 - 自动续期:默认开启看门狗机制,每隔 10 秒检查一次业务是否还在执行,若在执行则将锁的过期时间延长 30 秒,避免锁过期释放。
- 非阻塞获取锁 :使用
tryLock()方法,设置等待时间,避免线程无限期等待。 - 安全释放锁 :通过
isHeldByCurrentThread()验证当前线程是否持有锁,避免误删其他线程的锁。
六、分布式锁终极总结:简单的东西,更要用到极致
分布式锁的核心原理并不复杂,但落地时需要考虑的细节极多。很多线上故障不是因为技术方案不对,而是因为忽视了「高可用」「自动释放」「身份验证」等关键细节。
关于分布式锁的使用,最后分享三个核心原则:
- 选型匹配场景:高并发选 Redis,强一致性选 ZooKeeper,低并发选数据库,不要盲目追求高性能或高可用。
- 优先使用成熟客户端:不要重复造轮子,Redis 用 Redisson,ZooKeeper 用 Curator,这些客户端已解决了大部分坑点。
- 最小化锁粒度:减少持有锁的时间,只在核心逻辑上加锁,避免锁成为性能瓶颈。
记住:分布式锁是解决分布式并发问题的工具,不是银弹。在使用之前,先思考「是否真的需要分布式锁」,很多场景下,通过优化业务逻辑、使用分布式事务、或借助中间件的特性,就能避免并发问题。只有在必须使用的场景下,再谨慎落地分布式锁,才能发挥其最大价值。