分布式锁是分布式系统中非常重要的一个组件,它用于控制多个节点对共享资源的互斥访问,Redis 因其高性能、原子性和丰富的命令,是实现分布式锁的常用选择。

为什么需要分布式锁?
在单机应用中,我们可以使用 synchronized 或 ReentrantLock 等锁机制来保证线程安全,但在分布式系统中,多个服务实例可能部署在不同的机器上,它们无法共享 JVM 内的锁,当一个服务实例需要修改共享数据(如库存、订单状态)时,必须有一种机制来确保同一时间只有一个实例在执行该操作,这就是分布式锁要解决的问题。
Redis 分布式锁的基本原理
最基础的 Redis 分布式锁利用了 Redis 的 SET 命令,并结合其一些选项来实现原子性。
核心命令
SET key value [NX] [EX] seconds
key: 锁的名称,lock:product:123。value: 锁的“值”,通常是一个唯一的标识(如 UUID 或请求 ID),用于标识是哪个客户端获取了锁。NX: Not eXists,只有当 key 不存在时,才设置成功,这是实现“互斥”的关键。EX: 设置 key 的过期时间(秒),防止客户端宕机后锁无法释放,导致死锁。
加锁流程
- 客户端 A 尝试执行
SET lock:product:123 unique_value NX EX 10。 - 如果返回
OK:表示客户端 A 成功获取到锁,可以执行业务逻辑。 - 如果返回
nil:表示锁已被其他客户端持有,获取失败,客户端 A 可以选择重试或直接放弃。
解锁流程
解锁不能简单地使用 DEL lock:product:123,因为可能会误删其他客户端持有的锁。

- 客户端 A 获取了锁。
- 客户端 A 的业务逻辑执行时间较长,超过了锁的过期时间(10秒),锁自动释放。
- 客户端 B 获取了同一个锁。
- 客户端 A 的业务逻辑执行完毕,尝试执行
DEL命令,结果错误地删除了客户端 B 的锁。
解锁必须是有条件的:只有当锁的值还是当初自己设置的值时,才允许删除。
正确的解锁流程是使用 Lua 脚本,因为它能保证原子性(GET 和 DEL 两个操作不会被分开执行):
-- KEYS[1]: 锁的 key,"lock:product:123"
-- ARGV[1]: 锁的值,"unique_value"
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
只有当 GET 的结果和 ARGV[1] 相同时,才会执行 DEL。
Java 实现(使用 Jedis/Lettuce)
下面我们用 Java 代码来实现这个逻辑,这里我们使用 Jedis 作为 Redis 客户端,因为它对 Lua 脚本的支持非常直观。

1 添加依赖
在你的 pom.xml 中添加 Jedis 依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version> <!-- 使用较新版本 -->
</dependency>
2 实现加锁与解锁
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.UUID;
public class RedisDistributedLock {
private final Jedis jedis;
private final String lockKey;
private final String lockValue; // 使用 UUID 保证唯一性
private final long expireTime; // 锁的过期时间(毫秒)
public RedisDistributedLock(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() {
// 使用 SetParams 来构建 SET 命令的参数
SetParams params = SetParams.setParams().nx().px(expireTime);
String result = jedis.set(lockKey, lockValue, params);
// SET 命令在 NX 模式下,成功返回 OK,失败返回 nil
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() 会将脚本发送到 Redis 服务器执行
jedis.eval(luaScript, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
}
}
3 使用示例
public class Main {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String lockKey = "my_resource_lock";
// 创建锁实例,设置过期时间为 10 秒
RedisDistributedLock lock = new RedisDistributedLock(jedis, lockKey, 10000);
try {
// 尝试获取锁
if (lock.tryLock()) {
System.out.println("获取锁成功,开始执行业务逻辑...");
// 模拟业务逻辑执行
Thread.sleep(5000);
System.out.println("业务逻辑执行完毕。");
} else {
System.out.println("获取锁失败,可能是其他客户端正在持有锁。");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 在 finally 块中释放锁,确保锁一定会被释放
// 在实际使用前,最好先判断一下当前线程是否持有锁,避免误删
// 这里为了简化示例,直接调用
lock.unlock();
System.out.println("锁已释放。");
jedis.close();
}
}
}
更健壮的实现:Redisson
虽然上面的实现已经可以工作,但它还有一些问题需要考虑,
- 锁续期:如果一个任务执行时间超过了锁的初始过期时间,锁会自动释放,导致其他任务进入,造成数据不一致,我们需要一个机制,在任务执行期间,定期“续期”锁的过期时间。
- 可重入性:一个线程在持有锁的情况下,可以再次获取同一把锁,而不会造成死锁,上面的实现是不可重入的。
- 代码易用性:手动管理
tryLock和unlock容易出错,最好能使用类似synchronized的代码块。
Redisson 是一个强大的 Java Redis 客户端,它已经为我们解决了上述所有问题,提供了功能完善的分布式锁实现。
1 添加 Redisson 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.4</version> <!-- 使用较新版本 -->
</dependency>
2 使用 Redisson 实现分布式锁
Redisson 提供了 RLock 接口,用法非常简单。
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 RedissonLockExample {
public static void main(String[] args) {
// 1. 配置 Redisson 客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);
// 2. 获取锁对象
// 锁的名称是全局唯一的
RLock lock = redisson.getLock("my_resource_lock");
try {
// 3. 尝试获取锁
// waitTime: 等待获取锁的最大时间,超过则放弃
// leaseTime: 锁的自动释放时间(看门狗会自动续期)
// TimeUnit: 时间单位
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
System.out.println("Redisson 获取锁成功,开始执行业务逻辑...");
// 模拟业务逻辑执行时间很长
Thread.sleep(15000);
System.out.println("业务逻辑执行完毕。");
} else {
System.out.println("Redisson 获取锁失败。");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 4. 释放锁
// 只有在锁是当前线程持有的时候,才会释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println("Redisson 锁已释放。");
}
}
// 关闭 Redisson 客户端
redisson.shutdown();
}
}
Redisson 的核心优势
- 看门狗机制:当线程获取锁后,Redisson 会启动一个后台线程(看门狗),定期检查锁是否还存在,如果存在就延长其过期时间,这解决了任务执行时间超长导致锁过期的问题。
- 可重入锁:
RLock是可重入的,同一线程可以多次调用lock()方法,而不会阻塞自己。 - 锁续期:
tryLock(leaseTime)方法的leaseTime参数只是初始的租约时间,看门狗会在此基础上进行续期,直到你主动unlock()。 - 优雅的 API:提供了类似
synchronized的lock.lock()和lock.unlock(),以及带有超时和租约时间的lock.tryLock(),使用非常方便。 - 公平锁/非公平锁:Redisson 还支持公平锁,按照请求的顺序来获取锁,避免“饥饿”问题。
总结与最佳实践
| 特性 | 手动实现 (Jedis/Lettuce) | Redisson |
|---|---|---|
| 实现复杂度 | 较高,需要自己处理 Lua 脚本、锁续期等 | 低,开箱即用 |
| 锁续期 | 需要自己实现看门狗逻辑 | 内置看门狗机制,自动续期 |
| 可重入性 | 不可重入,需要额外代码实现 | 支持可重入锁 |
| 公平锁 | 不支持 | 支持公平锁和非公平锁 |
| 易用性 | 较低,容易出错 | 高,API 设计优雅 |
| 性能 | 直接操作 Redis,性能高 | 性能稍好于手动实现,因为内部有优化和连接池管理 |
最佳实践建议:
- 优先使用 Redisson:在绝大多数生产环境中,强烈推荐使用 Redisson,它是一个成熟、稳定、功能完备的解决方案,能让你专注于业务逻辑,而不是底层的锁实现细节。
- 设置合理的锁超时时间:即使有看门狗,也应该为锁设置一个合理的初始超时时间(
leaseTime),作为兜底,防止极端情况下看门狗失效。 - 在
finally块中释放锁:确保锁一定会被释放,避免死锁。 - 保证
value的唯一性:在手动实现中,value必须是唯一的,通常使用 UUID,在 Redisson 中,这个值由客户端内部生成和管理。 - 处理锁获取失败:当获取锁失败时,应该有相应的重试策略或直接降级处理(如返回错误、返回默认数据等),而不是无限循环等待。
