什么是分布式锁?
理解为什么需要分布式锁。

在单机应用中,如果多个线程需要访问同一个共享资源(比如一个计数器),我们可以使用 Java 自带的 synchronized 或 ReentrantLock 来保证线程安全,确保同一时间只有一个线程能访问该资源。
在分布式系统中,应用可能部署在多个不同的服务器上。synchronized 等单机锁就失效了,因为它只能锁住当前 JVM 进程内的线程,一个服务器上的线程无法阻止另一个服务器上的线程。
分布式锁 就是为了解决这个问题而生的,它是一种跨进程、跨机器的锁,可以保证在分布式系统中的多个节点,对于同一个共享资源,同一时间只有一个节点能对其进行操作。
分布式锁的核心需求
一个可靠的分布式锁实现,通常需要满足以下几个核心条件:

- 互斥性:在任意时刻,只有一个客户端能持有锁,这是最基本的要求。
- 不会发生死锁:如果一个客户端在获取锁后崩溃或网络异常,没有能主动释放锁,那么这个锁将永远被占用,导致其他客户端永远无法获取,锁必须有自动过期机制。
- 容错性:只要大部分 Redis 节点是正常的,客户端就能获取和释放锁,这要求 Redis 集群是高可用的。
- 可重入性:同一个客户端在持有锁的情况下,可以多次获取该锁而不会造成死锁,这类似于 Java 的
ReentrantLock。 - 安全性:锁的获取和释放必须是原子操作,不能出现“获取了锁但没设置成功”或“释放了锁但没清除成功”的情况。
Redis 实现分布式锁的方案
Redis 提供了多种数据结构,可以用来实现分布式锁,最主流的方案有两种:
基于 SET 命令的简单实现 (NX/XX, EX/PX)
这是 Redis 官方推荐的简单实现方式,适用于大多数场景。
核心命令:SET key value [NX|XX] [EX seconds|PX milliseconds]
key: �的名字。value: 锁的值。非常重要,它必须是唯一的,通常是一个随机字符串(UUID)或客户端 ID。NX: 表示 "Not eXists",只有当 key 不存在时,才设置成功,这保证了互斥性。EX seconds: 设置 key 的过期时间,单位是秒。PX milliseconds: 设置 key 的过期时间,单位是毫秒。
加锁过程:

- 客户端 A 生成一个唯一的随机字符串
value(uuid)。 - 客户端 A 执行命令:
SET lock_key my_unique_value NX EX 30。lock_key不存在,命令执行成功,客户端 A 成功获取锁,锁会在 30 秒后自动过期。lock_key已存在,命令执行失败,客户端 A 获取锁失败。
解锁过程:
解锁的关键在于,只能由加锁的客户端来解锁,如果直接用 DEL lock_key 命令,任何客户端都可以删除锁,这会破坏安全性(客户端 A 的锁快过期了,客户端 B 获取了锁,此时客户端 A 的操作完成,执行 DEL 命令误删了客户端 B 的锁)。
Redis 的 Lua 脚本可以保证原子性,解锁的 Lua 脚本逻辑是:
lock_key 的值等于 my_unique_value,那么就执行 DEL 命令。
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
客户端在执行解锁脚本时,会传入 lock_key 和当初加锁时生成的 my_unique_value,只有当两者匹配时,才会删除锁。
Redlock (Redisson 的实现)
对于需要极高可靠性的场景,比如多个客户端同时竞争锁,Redis 是主从架构时,简单的 SET NX 方案存在一个主从切换问题:
- 客户端 A 在主节点上成功获取了锁。
- 主节点还未将锁的同步到从节点,就发生了故障。
- 从节点被提升为主节点,此时这个锁在新的主节点上是不存在的。
- 客户端 B 可以在新的主节点上再次获取到同一个锁,这就破坏了互斥性。
为了解决这个问题,Redis 的作者 Antirez 提出了 Redlock 算法,Redlock 的思想是,使用多个(通常是 5 个)独立的 Redis 节点,客户端只有在大多数(3 个或以上)节点上成功获取锁,才认为锁获取成功。
Redlock 算法流程:
- 客户端获取当前时间(毫秒级)。
- 依次向 5 个 Redis 节点尝试获取锁,使用相同的 key、随机 value 和较短的过期时间(例如几十毫秒)。
SET lock_key my_unique_value NX EX 100。 - 客户端在所有 5 个节点上获取锁,计算总耗时,如果客户端在至少 3 个节点上获取了锁,并且总耗时小于锁的过期时间,那么就认为获取锁成功。
- 如果获取锁成功,锁的有效时间是初始设定的过期时间减去总耗时。
- 如果获取锁失败,客户端会在所有能获取锁的节点上执行解锁(即使没有获取成功,也要尝试删除,避免脏数据)。
Redlock 的实现库:
手动实现 Redlock 算法非常复杂,容易出错,强烈建议使用成熟的客户端库,Redisson,Redisson 提供了开箱即用的 RLock 对象,其底层就是基于 Redlock 算法实现的。
Java 代码实现
下面我们通过 Java 代码来演示这两种方案。
准备工作
你需要添加 Redis 客户端的依赖,这里我们使用 jedis,它是 Redis 的官方 Java 客户端。
Maven 依赖 (pom.xml):
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version> <!-- 使用较新版本 -->
</dependency>
基于 SET 命令的简单实现
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.UUID;
public class SimpleRedisLock {
private final Jedis jedis;
private final String lockKey;
private final String lockValue; // 用于标识是哪个客户端加的锁
private final long expireTime; // 锁的过期时间(毫秒)
public SimpleRedisLock(Jedis jedis, String lockKey, long expireTime) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockValue = UUID.randomUUID().toString();
this.expireTime = expireTime;
}
/**
* 尝试获取锁
* @return true: 获取成功, false: 获取失败
*/
public boolean tryLock() {
// 使用 SET 命令的 NX 和 PX 选项
SetParams params = SetParams.setParams().nx().px(expireTime);
String result = jedis.set(lockKey, lockValue, params);
// "OK" 表示设置成功
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() 会自动将 KEYS 和 ARGV 正确地传递给 Lua 脚本
jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
}
}
使用示例:
public class SimpleLockDemo {
public static void main(String[] args) throws InterruptedException {
Jedis jedis = new Jedis("localhost", 6379);
jedis.flushAll(); // 清空数据库,方便测试
SimpleRedisLock lock = new SimpleRedisLock(jedis, "my_resource_lock", 10000); // 10秒过期
// 模拟两个客户端
Thread client1 = new Thread(() -> {
if (lock.tryLock()) {
System.out.println("Client 1: Lock acquired!");
try {
// 模拟业务处理
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Client 1: Releasing lock...");
lock.unlock();
}
} else {
System.out.println("Client 1: Failed to acquire lock.");
}
});
Thread client2 = new Thread(() -> {
// 让 client2 稍后启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (lock.tryLock()) {
System.out.println("Client 2: Lock acquired!");
try {
// 模拟业务处理
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Client 2: Releasing lock...");
lock.unlock();
}
} else {
System.out.println("Client 2: Failed to acquire lock.");
}
});
client1.start();
client2.start();
client1.join();
client2.join();
jedis.close();
}
}
预期输出:
Client 1: Lock acquired!
Client 2: Failed to acquire lock.
Client 1: Releasing lock...
可以看到,Client 1 成功获取锁,Client 2 获取失败,当 Client 1 释放锁后,Client 2 的逻辑已经执行完毕,所以不会有输出。
使用 Redisson 实现 Redlock
这是生产环境中最推荐的方式。
Maven 依赖 (pom.xml):
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.4</version> <!-- 使用较新版本 -->
</dependency>
代码实现:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RedissonLockDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 配置 Redisson 客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);
// 2. 获取锁
// lockKey 是锁的名字
RLock lock = redisson.getLock("my_resource_redisson_lock");
// 3. 尝试获取锁
// 参数:waitTime(等待获取锁的时间),leaseTime(锁的持有时间),timeUnit(时间单位)
// waitTime 为 -1,则无限期等待直到获取锁
// leaseTime 为 -1,则锁不会自动过期,需要手动 unlock
boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (isLocked) {
try {
System.out.println("Thread " + Thread.currentThread().getName() + ": Lock acquired!");
// 模拟业务处理
Thread.sleep(15000);
} finally {
System.out.println("Thread " + Thread.currentThread().getName() + ": Releasing lock...");
// 4. 释放锁
lock.unlock();
}
} else {
System.out.println("Thread " + Thread.currentThread().getName() + ": Failed to acquire lock.");
}
// 关闭 Redisson 客户端
redisson.shutdown();
}
}
多线程测试:
// 在 main 方法中启动多个线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
RLock lock = redisson.getLock("my_resource_redisson_lock");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + ": Lock acquired!");
Thread.sleep(5000);
} finally {
System.out.println(Thread.currentThread().getName() + ": Releasing lock...");
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + ": Failed to acquire lock.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread-" + i).start();
}
预期输出: 只有一个线程会输出 "Lock acquired!",其他线程会输出 "Failed to acquire lock.",获取锁的线程在 5 秒后释放锁,其他线程可以立即尝试获取。
最佳实践和注意事项
- 锁的 value 必须唯一:这是实现安全释放锁的关键。
UUID是一个很好的选择。 - 设置合理的过期时间:过期时间需要根据业务逻辑的执行时间来设定,既要保证业务能完成,又要防止因异常导致锁长期不释放。
- 使用
tryLock而非阻塞式lock:在分布式系统中,直接阻塞等待获取锁可能会导致资源耗尽。tryLock可以设置超时时间,失败后可以快速失败,进行重试或降级处理。 - 锁的粒度要小:只锁定必要的资源,不要用一个锁来保护所有业务逻辑。
- 优先使用成熟的库:除非有特殊需求,否则强烈建议使用 Redisson,它已经解决了 Redlock 算法、锁续期(看门狗机制)、可重入性等复杂问题,稳定性和可靠性远高于自己实现的简单版本。
- 处理异常:在
finally块中释放锁,确保即使业务代码抛出异常,锁也能被正确释放。 - 监控:对锁的获取成功率、持有时间等指标进行监控,以便及时发现系统中的瓶颈和问题。
| 特性 | 简单 SET NX 实现 |
Redisson (Redlock) 实现 |
|---|---|---|
| 实现复杂度 | 低,几行代码即可 | 高,但封装在库中,使用简单 |
| 可靠性 | 较高,但在主从切换场景下有风险 | 非常高,通过多节点解决了主从切换问题 |
| 可重入性 | 不支持,需要额外实现 | 支持,RLock 是可重入锁 |
| 锁续期 | 不支持,锁一旦设置过期时间就不会变 | 支持,看门狗机制在锁快过期时自动续期 |
| 适用场景 | 简单业务,对可靠性要求不是极致的场景 | 关键业务,高并发,需要极高可靠性的场景 |
对于绝大多数 Java 应用,直接使用 Redisson 是最省心、最可靠的选择,如果你只是想快速实现一个简单的功能,并且对主从切换问题不敏感,SET NX 的方案也是一个可行的选择。
