目录
- 什么是分布式锁?
- 为什么需要分布式锁?
- Redis 实现分布式锁的基本原理
- 简单实现(不推荐)
- 代码示例
- 严重缺陷: 不具备锁的可靠性
- Redis 官方推荐的 Redlock 算法
- 算法流程
- 严重缺陷: 实现复杂,性能开销大,争议多
- Redisson(业界最佳实践)
- 什么是 Redisson?
- Redisson 如何实现分布式锁?
- 可重入性
- 锁续期(Watchdog 机制)
- 锁的释放
- 代码示例
- 总结与对比
- 最佳实践建议
什么是分布式锁?
在单机应用中,我们可以使用 synchronized 或 ReentrantLock 等锁机制来保证共享资源在同一时间只被一个线程访问,但在分布式系统中,多个服务实例部署在不同的机器上,它们拥有各自的 JVM,这些 JVM 内置的锁机制无法跨进程、跨机器生效。

分布式锁就是一种用来控制分布式系统之间访问共享资源的机制,它能够保证在分布式场景下的多个节点,对于同一个共享资源,在同一时间只有一个节点能访问它。
为什么需要分布式锁?
当多个服务实例需要同时修改一个共享资源时(库存扣减、订单创建、秒杀活动),如果不加锁,就会发生并发问题,导致数据不一致。
场景示例: 一个秒杀活动,库存只剩 1 件,两个用户几乎同时下单。
- 服务 A 查询库存,发现为 1。
- 服务 B 也查询库存,发现为 1。
- 服务 A 扣减库存,库存变为 0。
- 服务 B 也执行扣减库存,库存变为 -1。 结果就是库存出现负数,超卖了。
使用分布式锁,可以确保在服务 A 完成扣减库存并释放锁之前,服务 B 无法获取到锁,从而避免了超卖。

Redis 实现分布式锁的基本原理
利用 Redis 的 SET 命令,并结合一些其他命令和选项,可以实现一个基础的分布式锁。
核心思想是:
- 获取锁: 尝试使用
SET命令向一个 key(lock:product:123)设置一个唯一值(UUID),如果设置成功,则表示获取锁成功。 - 释放锁: 执行
DEL命令删除这个 key,表示释放锁。
但仅仅这样做是远远不够的,必须考虑各种异常情况。
方案一:简单实现(不推荐)
这是最 naive 的实现方式,能理解基本逻辑,但绝对不能用于生产环境。

代码示例
import redis.clients.jedis.Jedis;
public class SimpleRedisLock {
private Jedis jedis;
private String lockKey;
private String requestId; // 请求的唯一标识
public SimpleRedisLock(Jedis jedis, String lockKey, String requestId) {
this.jedis = jedis;
this.lockKey = lockKey;
this.requestId = requestId;
}
// 获取锁
public boolean tryLock() {
// SET key value NX PX 30000
// NX: Not eXist, 只有当 key 不存在时才设置
// PX: 毫秒级过期时间
String result = jedis.set(lockKey, requestId, "NX", "PX", 30000);
return "OK".equals(result);
}
// 释放锁
public void unlock() {
// 使用 Lua 脚本来保证原子性
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(luaScript, 1, lockKey, requestId);
}
}
严重缺陷:不具备锁的可靠性
-
锁的误释放:
- 问题: 如果客户端 A 获取了锁,但在执行业务逻辑时,因为某些原因(如 GC 停顿、网络延迟)导致锁超时自动释放了,客户端 B 获取到了锁,随后,客户端 A 的业务逻辑执行完毕,调用
unlock()方法,由于unlock()只判断了 key 的 value 是否和自己当初设置的一样,而客户端 A 的 value 仍然存在,所以它会误删除客户端 B 的锁。 - 解决方案: 释放锁时,必须验证 value 是否为自己设置的值,这需要原子操作,上面的
unlock()方法使用了 Lua 脚本来保证get和del的原子性。
- 问题: 如果客户端 A 获取了锁,但在执行业务逻辑时,因为某些原因(如 GC 停顿、网络延迟)导致锁超时自动释放了,客户端 B 获取到了锁,随后,客户端 A 的业务逻辑执行完毕,调用
-
锁续期问题:
- 问题: 一个业务逻辑的执行时间可能超过了锁的默认过期时间(30 秒),如果业务还没执行完,锁就自动过期了,其他客户端就可以获取到锁,导致并发问题。
- 解决方案: 需要一个机制在业务执行期间,自动续期锁的过期时间,启动一个后台线程,每隔 10 秒就去检查一下,如果当前线程还持有锁,就重置过期时间为 30 秒,这就是所谓的 Watchdog 机制。
-
单点故障问题:
- 问题: Redis 实例发生故障(主从切换、宕机),可能会导致锁服务不可用,或者锁在切换过程中丢失。
- 解决方案: 使用 Redis 集群来提高可用性,但这也引出了更复杂的 Redlock 算法。
方案二:Redis 官方推荐的 Redlock 算法
为了解决单点故障问题,Redis 官方提出了 Redlock 算法,它不再依赖单个 Redis 节点,而是使用多个独立的 Redis 节点(5 个)。
算法流程
- 获取当前时间戳(
start_time)。 - 使用相同的 key 和 value,依次向这 5 个 Redis 节点尝试获取锁,获取锁的方式和之前一样(
SET key value NX PX 30000)。 - 客户端计算获取锁所花费的时间(
time_elapsed = current_time - start_time)。 - 只有当满足以下两个条件时,才认为锁获取成功:
- 客户端在大多数(N/2 + 1,这里是 3 个)节点上成功获取了锁。
- 获取锁的总耗时
time_elapsed小于锁的过期时间。
- 如果锁获取成功,锁的有效时间是原始过期时间减去
time_elapsed。 - 如果获取锁失败,客户端会向所有节点发送释放锁的命令。
严重缺陷:实现复杂,争议多
Redlock 算法虽然解决了单点问题,但带来了新的复杂性:
- 性能开销大: 需要和多个节点通信,网络延迟较高。
- 实现复杂: 代码逻辑繁琐,容易出错。
- 理论争议: 很多分布式系统专家(如 Martin Kleppmann)对 Redlock 的必要性提出了质疑,认为在大多数场景下,一个高可用的 Redis 集群(使用主从复制)已经足够,Redlock 带来的额外复杂性可能不值得。
除非你的业务场景对锁的极端容错性有非常高的要求,否则一般不推荐自己实现 Redlock。
方案三:Redisson(业界最佳实践)
Redisson 是一个在 Java 生态中非常流行的 Redis 客户端,它不仅提供了丰富的 Redis 数据结构的操作,更重要的是,它封装好了分布式锁的实现,解决了我们前面提到的所有问题,是生产环境中的首选。
什么是 Redisson?
Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid),它极大地简化了 Redis 在 Java 项目中的使用,特别是分布式对象和服务(如分布式锁、分布式集合、分布式服务)。
Redisson 如何实现分布式锁?
Redisson 的分布式锁 RLock 是基于 Redis 的 hash 数据结构实现的,并且完美地解决了我们之前讨论的缺陷。
-
可重入性
- 问题: 同一个线程可以多次获取同一个锁,如果不可重入,会导致自己死锁。
- 解决方案: Redisson 在 key 对应的
hash中,存储了key -> (线程ID, 重入计数)的映射,当同一个线程再次获取锁时,它会检查hash中是否已有该线程的记录,如果有,则增加计数,而不是重新设置 key。
-
锁续期(Watchdog 机制)
- 问题: 业务执行时间超过锁的默认过期时间。
- 解决方案: 当客户端通过
lock()方法成功获取锁后,Redisson 会启动一个后台线程(Watchdog),这个线程会定期(例如每 10 秒)检查当前线程是否还持有锁,如果持有,就为这个 key 续期(重置过期时间为 30 秒),这个后台线程会一直运行,直到锁被手动释放或者客户端关闭。
-
锁的释放
- 问题: 释放锁的原子性,以及只释放自己持有的锁。
- 解决方案: Redisson 的
unlock()方法同样通过执行 Lua 脚本来实现,确保只有锁的持有者才能释放锁,并且释放过程是原子的。
代码示例
在 pom.xml 中添加 Redisson 依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.4</version> <!-- 请使用最新版本 -->
</dependency>
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class ProductService {
@Autowired
private RedissonClient redissonClient;
public void deductStock(String productId) {
// 1. 定义锁的名称,通常与业务资源相关
String lockKey = "lock:product:" + productId;
// 2. 获取锁对象
RLock lock = redissonClient.getLock(lockKey);
try {
// 3. 尝试获取锁
// - 第一个参数:等待时间,最多等待 10 秒
// - 第二个参数:锁的自动释放时间,30 秒后自动释放(Watchdog 会续期)
// - 第三个参数:时间单位
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
System.out.println(Thread.currentThread().getName() + " 成功获取到锁,开始处理业务...");
// --- 模拟业务逻辑 ---
Thread.sleep(15000); // 假设业务逻辑需要 15 秒
// --- 业务逻辑结束 ---
System.out.println(Thread.currentThread().getName() + " 业务处理完成,准备释放锁。");
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁失败,可能已被其他线程占用。");
}
} catch (InterruptedException e) {
e.printStackTrace();
// 如果线程在等待锁时被中断,需要中断状态
Thread.currentThread().interrupt();
} finally {
// 4. 确保锁一定会被释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 锁已释放。");
}
}
}
}
总结与对比
| 特性 | 简单实现 | Redlock 算法 | Redisson |
|---|---|---|---|
| 实现复杂度 | 低 | 极高 | 低 |
| 可靠性 | 差 | 高 | 高 |
| 可重入性 | 无 | 无 | 有 |
| 锁续期 | 无 | 无 | 有 (Watchdog) |
| 原子性释放 | 需手动实现 (Lua) | 需手动实现 (Lua) | 内置 |
| 容错性 | 差 | 高 | 高 |
| 适用场景 | 学习、演示 | 极端苛刻的业务场景 | 生产环境首选 |
| 推荐度 | ⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
最佳实践建议
- 首选 Redisson:在绝大多数 Java 项目中,直接使用 Redisson 是最简单、最可靠、最高效的选择,它为你处理了所有复杂的细节。
- 锁的粒度要小:锁的范围应该尽可能小,只锁定必要的代码段(通常是修改共享资源的那部分),不要将整个方法都用
synchronized或lock包裹起来,以减少锁的持有时间。 - 设置合理的超时时间:
tryLock的等待时间和锁的过期时间需要根据业务逻辑的执行时间来设定,太短可能导致业务未完成就释放,太长可能导致资源被长时间占用。 - 在
finally块中释放锁:这是必须遵守的原则,确保无论业务逻辑是否抛出异常,锁都能被正确释放,避免死锁。 - 避免长时间持有锁:在持有锁期间,不要执行耗时操作(如网络请求、复杂计算),这会严重影响系统性能和并发能力,可以将耗时操作放到锁外执行。
- 注意锁的 Key 设计:Key 应该具有唯一性,通常与业务资源 ID 相关,
lock:order:1001,避免使用全局锁(如lock:global)。
