在分布式系统中,我们经常会遇到多个进程同时访问同一个共享资源的场景,比如库存扣减、订单创建、定时任务执行等。在单体应用中,我们可以使用 Java 的synchronized关键字或者ReentrantLock来保证线程安全。但在分布式环境下,这些本地锁完全失效,因为多个进程运行在不同的 JVM 中,无法共享同一个锁对象。
为了解决这个问题,我们需要分布式锁------ 一种跨进程、跨机器的互斥锁机制。而 Redis 凭借其高性能、高可用的特性,成为了实现分布式锁的首选方案。
面试时,Redis 分布式锁更是 100% 的必考题:
- 如何用 Redis 实现一个分布式锁?
- 原生 Redis 分布式锁有哪些缺陷?
- 什么是锁的可重入性?怎么实现?
- 什么是看门狗机制?它解决了什么问题?
- Redisson 的分布式锁是怎么实现的?
- 主从切换会导致锁丢失吗?怎么解决?
这篇文章,我们就从分布式锁的本质出发,一步步拆解原生 Redis 分布式锁的演进过程,分析每个版本的缺陷和优化方案,然后深入讲解 Redisson 分布式锁的底层原理和实现细节,最后给出线上最佳实践和避坑指南。看完这篇,你不仅能轻松应对所有相关面试题,更能在实际项目中正确使用分布式锁。

一、先搞懂:什么是分布式锁?
1. 为什么需要分布式锁?
在单体应用中,多个线程访问同一个共享资源时,我们可以使用本地锁来保证同一时间只有一个线程访问资源。但在分布式系统中,多个服务实例运行在不同的机器上,本地锁无法跨机器生效,这就会导致多个实例同时访问同一个资源,引发数据不一致的问题。
最典型的场景就是库存扣减:如果有 100 个商品库存,同时有 1000 个请求过来扣减库存,如果没有分布式锁,就会出现超卖的问题,最终卖出 1000 个商品,导致系统亏损。

2. 分布式锁应该满足哪些特性?
一个合格的分布式锁,必须满足以下四个核心特性:
- 互斥性:同一时间只能有一个客户端持有锁
- 防死锁:即使持有锁的客户端崩溃或网络中断,锁也能被最终释放,不会导致其他客户端永远无法获取锁
- 可重入性:同一个客户端可以多次获取同一把锁
- 高可用:加锁和解锁的操作必须高可用,且性能要好
二、原生 Redis 分布式锁的演进之路
很多人以为 Redis 分布式锁就是一个SETNX命令这么简单,其实不然。原生 Redis 分布式锁的实现经历了多个版本的演进,每个版本都解决了上一个版本的缺陷,但又引入了新的问题。
版本 1:最简单的实现(SETNX)
最原始的分布式锁实现,使用 Redis 的SETNX(SET if Not eXists)命令:
java
public boolean tryLock(String key, long expireTime) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(result);
}
public void unlock(String key) {
redisTemplate.delete(key);
}
原理:如果 key 不存在,就设置 key 的值并返回 true,表示加锁成功;如果 key 已经存在,返回 false,表示加锁失败。

致命缺陷:
- 没有唯一标识:客户端 A 加锁成功,执行时间过长导致锁过期自动释放,客户端 B 获取到锁;此时客户端 A 执行完成,删除了客户端 B 的锁,导致锁失效。

- 不可重入:同一个客户端无法多次获取同一把锁。
- 没有自动续期:如果业务执行时间超过了锁的过期时间,锁会自动释放,导致多个客户端同时持有锁。
版本 2:增加唯一标识,解决误删锁问题
为了解决误删锁的问题,我们给每个锁的值设置一个唯一标识,只有持有锁的客户端才能删除自己的锁:
java
public String tryLock(String key, long expireTime) {
String lockValue = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, lockValue, expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(result) ? lockValue : null;
}
public void unlock(String key, String lockValue) {
// 只有当锁的值等于自己的唯一标识时,才删除锁
String currentValue = redisTemplate.opsForValue().get(key);
if (lockValue.equals(currentValue)) {
redisTemplate.delete(key);
}
}
改进:每个锁都有一个唯一的 UUID,只有持有这个 UUID 的客户端才能删除锁,解决了误删锁的问题。
新的缺陷:
- 解锁操作不是原子的。在
get和delete之间,锁可能已经过期并被其他客户端获取,导致误删。

- 仍然不可重入,没有自动续期。
版本 3:使用 Lua 脚本保证解锁原子性
为了解决解锁操作的原子性问题,我们使用 Lua 脚本来执行解锁操作。Redis 会将整个 Lua 脚本作为一个原子命令执行,中间不会被其他命令打断:
java
public void unlock(String key, String lockValue) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key), lockValue);
}
改进:解锁操作变成了原子操作,彻底解决了误删锁的问题。
仍然存在的问题:
- 不可重入:同一个客户端无法多次获取同一把锁
- 没有自动续期:业务执行时间超过锁过期时间时,锁会自动释放
- 主从切换导致锁丢失:如果 Redis 主库在加锁成功后还没同步到从库就宕机了,从库提升为主库后,锁就丢失了
原生 Redis 分布式锁的总结
原生 Redis 分布式锁经过三次演进,解决了互斥性、防死锁和误删锁的问题,但仍然存在三个无法解决的致命缺陷:
- 不可重入
- 没有自动续期机制
- 主从切换导致锁丢失
这些缺陷在生产环境中是不可接受的。为了解决这些问题,我们需要一个更成熟、更完善的分布式锁解决方案 ------Redisson。
三、Redisson 分布式锁:工业级解决方案
Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式 Java 常用对象,还提供了丰富的分布式锁实现。Redisson 的分布式锁是目前最成熟、最稳定的 Redis 分布式锁实现,也是互联网公司的标准选择。
1. Redisson 的优势
Redisson 完美解决了原生 Redis 分布式锁的所有缺陷:
- ✅ 可重入性:支持同一个客户端多次获取同一把锁
- ✅ 自动续期:内置看门狗机制,自动延长锁的过期时间
- ✅ 原子操作:所有加锁解锁操作都使用 Lua 脚本保证原子性
- ✅ 多种锁类型:支持可重入锁、公平锁、读写锁、联锁、红锁等
- ✅ 高可用:支持 Redis 集群、哨兵模式,解决主从切换锁丢失问题
- ✅ 简单易用 :API 和 Java 的
Lock接口完全一致,学习成本极低
2. Redisson 可重入锁的核心原理
Redisson 的可重入锁(RLock)是最常用的分布式锁,它的实现原理是整个 Redisson 分布式锁的核心。
核心数据结构
Redisson 使用 Redis 的Hash 数据结构来实现可重入锁:
- Hash 的 key 是锁的名称
- Hash 的 field 是客户端的唯一标识(
UUID:threadId) - Hash 的 value 是重入次数
例如,客户端a1b2c3的线程 1 获取了锁order:123,重入了 2 次,那么 Redis 中的数据结构是:
java
HGETALL order:123
1) "a1b2c3:1"
2) "2"
加锁流程(面试必问)
Redisson 的加锁操作完全通过 Lua 脚本实现,保证原子性。完整的加锁流程如下:
- 客户端调用
lock()方法,尝试获取锁 - 执行 Lua 脚本,检查锁是否存在:
- 如果锁不存在(Hash 为空),则创建 Hash,设置重入次数为 1,设置过期时间(默认 30 秒),返回加锁成功
- 如果锁存在,且 field 是当前客户端的唯一标识,则将重入次数加 1,重置过期时间,返回加锁成功
- 如果锁存在,但 field 不是当前客户端的唯一标识,则返回锁的剩余过期时间,表示加锁失败
- 如果加锁失败,客户端会订阅锁的释放消息,然后进入循环等待,每隔一段时间重试获取锁
- 加锁成功后,启动看门狗线程,每隔 10 秒(过期时间的 1/3)检查一次,如果客户端仍然持有锁,就将锁的过期时间重置为 30 秒

看门狗机制(Watch Dog)
看门狗是 Redisson 最核心的设计之一,它解决了 "业务执行时间超过锁过期时间导致锁自动释放" 的问题。
工作原理:
- 当客户端加锁成功后,会启动一个后台线程(看门狗)
- 看门狗每隔
internalLockLeaseTime / 3时间(默认 30 秒 / 3=10 秒)执行一次 - 如果客户端仍然持有锁,就将锁的过期时间重置为
internalLockLeaseTime(默认 30 秒) - 如果客户端崩溃或网络中断,看门狗无法续期,锁会在 30 秒后自动释放
注意:只有当没有指定锁的过期时间时,看门狗才会生效。如果手动指定了过期时间,看门狗不会启动,锁会在指定时间后自动释放。
解锁流程
解锁操作同样通过 Lua 脚本实现,保证原子性:
- 客户端调用
unlock()方法 - 执行 Lua 脚本,检查锁是否存在:
- 如果锁不存在,返回 null,表示锁已经被释放
- 如果锁存在,但 field 不是当前客户端的唯一标识,返回 null,表示不是自己的锁
- 如果锁存在,且 field 是当前客户端的唯一标识,则将重入次数减 1
- 如果重入次数减到 0,则删除锁,发布锁释放消息,返回 1
- 如果重入次数大于 0,则重置过期时间,返回 0
- 解锁成功后,停止看门狗线程

3. Redisson 支持的其他锁类型
除了可重入锁,Redisson 还提供了多种其他类型的锁,适应不同的业务场景:
(1)公平锁(FairLock)
保证多个客户端按照请求的顺序获取锁,先到先得,避免饥饿问题。底层通过 Redis 的有序集合实现。
java
RLock fairLock = redisson.getFairLock("fairLock");
fairLock.lock();
try {
// 执行业务逻辑
} finally {
fairLock.unlock();
}
(2)读写锁(ReadWriteLock)
允许多个客户端同时获取读锁,但只能有一个客户端获取写锁。读锁和写锁互斥,读锁之间不互斥。适合读多写少的场景,可以大幅提升并发性能。
java
RReadWriteLock rwLock = redisson.getReadWriteLock("rwLock");
// 获取读锁
rwLock.readLock().lock();
try {
// 执行读操作
} finally {
rwLock.readLock().unlock();
}
// 获取写锁
rwLock.writeLock().lock();
try {
// 执行写操作
} finally {
rwLock.writeLock().unlock();
}
(3)联锁(MultiLock)
可以同时获取多个锁,只有当所有锁都获取成功时,才返回加锁成功。适用于需要同时锁定多个资源的场景。
java
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");
RLock multiLock = redisson.getMultiLock(lock1, lock2, lock3);
multiLock.lock();
try {
// 执行业务逻辑
} finally {
multiLock.unlock();
}
(4)红锁(RedLock)
为了解决 Redis 主从切换导致的锁丢失问题,Redisson 提供了红锁算法。红锁需要向多个独立的 Redis 节点请求加锁,只有当超过半数的节点加锁成功时,才认为加锁成功。
注意:红锁虽然解决了主从切换锁丢失的问题,但也带来了更高的复杂度和性能开销。绝大多数业务场景下,普通的可重入锁已经足够,不需要使用红锁。
四、Spring Boot 整合 Redisson 实战
下面我们通过一个完整的示例,演示如何在 Spring Boot 项目中整合 Redisson,使用分布式锁解决库存扣减的超卖问题。
1. 引入依赖
XML
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.25.0</version>
</dependency>
2. 配置 Redisson
在application.yml中配置 Redis 连接信息:
bash
spring:
redis:
host: localhost
port: 6379
password: yourpassword
database: 0
Redisson 会自动根据 Spring Boot 的 Redis 配置创建RedissonClient实例,不需要额外配置。
3. 库存扣减示例
java
@Service
public class StockService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 扣减库存
* @param productId 商品ID
* @param quantity 扣减数量
* @return 是否扣减成功
*/
public boolean deductStock(Long productId, int quantity) {
String lockKey = "stock:lock:" + productId;
String stockKey = "stock:" + productId;
// 获取分布式锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 加锁,最多等待10秒,锁的过期时间30秒(看门狗自动续期)
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!locked) {
// 获取锁失败,返回扣减失败
return false;
}
// 获取当前库存
String stockStr = redisTemplate.opsForValue().get(stockKey);
if (stockStr == null) {
return false;
}
int stock = Integer.parseInt(stockStr);
if (stock < quantity) {
// 库存不足
return false;
}
// 扣减库存
redisTemplate.opsForValue().set(stockKey, String.valueOf(stock - quantity));
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
4. 测试
我们可以使用 JMeter 模拟 1000 个并发请求,扣减 100 个库存。如果没有分布式锁,会出现超卖;使用 Redisson 分布式锁后,最终库存会准确变为 0,不会出现超卖。
五、线上最佳实践与避坑指南
1. 锁的粒度要尽可能小
锁的粒度越小,并发性能越好。尽量不要在锁中执行耗时的操作,比如调用第三方接口、复杂的数据库查询等。应该只把需要互斥执行的核心代码放在锁中。
错误示例:
java
lock.lock();
try {
// 调用第三方接口,耗时1秒
thirdPartyService.call();
// 数据库查询,耗时500毫秒
userMapper.selectById(userId);
// 核心业务逻辑,耗时10毫秒
doBusiness();
} finally {
lock.unlock();
}
正确示例:
java
// 把耗时操作放在锁外面
thirdPartyService.call();
User user = userMapper.selectById(userId);
lock.lock();
try {
// 只把核心业务逻辑放在锁中
doBusiness(user);
} finally {
lock.unlock();
}
2. 合理设置锁的等待时间和过期时间
- 等待时间:不要设置过长的等待时间,否则会导致大量线程阻塞。一般设置为 1-10 秒,根据业务场景调整。
- 过期时间:如果业务执行时间比较固定,可以手动指定过期时间,关闭看门狗,减少不必要的性能开销。如果业务执行时间不确定,不要指定过期时间,让看门狗自动续期。
3. 锁必须在 finally 块中释放
无论业务逻辑是否抛出异常,都必须在 finally 块中释放锁,否则会导致锁永远无法释放,其他客户端永远无法获取锁。
4. 避免死锁
- 按照相同的顺序获取多个锁
- 给锁设置超时时间
- 避免在一个锁中嵌套另一个锁
5. 不要使用红锁
绝大多数业务场景下,普通的可重入锁已经足够。红锁虽然解决了主从切换锁丢失的问题,但也带来了更高的复杂度和性能开销,而且需要部署多个独立的 Redis 节点,维护成本很高。
6. 监控锁的使用情况
监控以下指标,及时发现问题:
- 加锁成功率
- 平均加锁等待时间
- 锁的持有时间
- 死锁次数
六、常见误区纠正
-
误区 :Redis 分布式锁是绝对安全的。 纠正:没有绝对安全的分布式锁。Redis 分布式锁在极端情况下(比如主从同时宕机)仍然可能出现锁丢失的问题,但对于绝大多数业务场景来说,这个概率极低,完全可以接受。
-
误区 :看门狗会无限续期。 纠正:只有当客户端正常运行时,看门狗才会续期。如果客户端崩溃或网络中断,看门狗会停止运行,锁会在 30 秒后自动释放。
-
误区 :Redisson 的可重入锁是基于线程的。 纠正:Redisson 的可重入锁是基于客户端 + 线程的。不同客户端的不同线程,即使是同一个线程 ID,也不能重入对方的锁。
-
误区 :分布式锁可以解决所有并发问题。 纠正:分布式锁只能解决跨进程的互斥问题,不能解决数据库层面的并发问题。比如库存扣减,即使加了分布式锁,也需要在数据库层面使用乐观锁或悲观锁做兜底。
七、高频面试题解答
-
问:如何用 Redis 实现一个分布式锁? 答:使用
SET key value NX EX expire_time命令加锁,使用 Lua 脚本解锁,保证原子性。但原生实现存在不可重入、没有自动续期、主从切换锁丢失等问题,推荐使用 Redisson。 -
问:Redisson 的分布式锁是怎么实现的? 答:Redisson 使用 Hash 数据结构实现可重入锁,所有加锁解锁操作都通过 Lua 脚本保证原子性。内置看门狗机制,自动延长锁的过期时间。支持可重入锁、公平锁、读写锁等多种锁类型。
-
问:什么是看门狗机制?它解决了什么问题? 答:看门狗是 Redisson 的一个后台线程,每隔 10 秒检查一次,如果客户端仍然持有锁,就将锁的过期时间重置为 30 秒。它解决了 "业务执行时间超过锁过期时间导致锁自动释放" 的问题。
-
问:Redis 主从切换会导致锁丢失吗?怎么解决? 答:会。如果主库在加锁成功后还没同步到从库就宕机了,从库提升为主库后,锁就丢失了。可以使用 Redisson 的红锁算法解决,但绝大多数场景下不需要。
-
问:Redisson 的可重入性是怎么实现的? 答:Redisson 使用 Hash 数据结构,field 是客户端的唯一标识(UUID:threadId),value 是重入次数。每次加锁时,如果是同一个客户端,就将重入次数加 1;每次解锁时,将重入次数减 1,减到 0 时删除锁。
-
问:分布式锁和本地锁有什么区别? 答:本地锁只能保证同一个 JVM 内的线程安全,分布式锁可以保证跨进程、跨机器的线程安全。本地锁性能更高,分布式锁性能稍低,但适用范围更广。
八、总结
分布式锁是分布式系统中最基础也是最重要的组件之一。原生 Redis 分布式锁虽然简单,但存在很多缺陷,不适合在生产环境中使用。Redisson 作为工业级的分布式锁解决方案,完美解决了原生实现的所有问题,是目前最成熟、最稳定的选择。
回顾一下全文的核心内容:
- 分布式锁必须满足互斥性、防死锁、可重入性、高可用四个特性
- 原生 Redis 分布式锁经过三次演进,解决了互斥性、防死锁和误删锁的问题,但仍然存在不可重入、没有自动续期、主从切换锁丢失等缺陷
- Redisson 使用 Hash 数据结构实现可重入锁,通过 Lua 脚本保证原子性,内置看门狗机制自动续期
- Redisson 支持可重入锁、公平锁、读写锁、联锁、红锁等多种锁类型,适应不同的业务场景
- 线上使用时要注意锁的粒度、等待时间和过期时间,必须在 finally 块中释放锁
掌握了 Redis 分布式锁和 Redisson 的原理,你就能在实际项目中正确使用分布式锁,解决各种并发问题,同时轻松应对所有相关的面试题。