下面是一个关于 Java 分布式锁工具类 DistributedLockUtils
的完整实现及详细解释。分布式锁在分布式系统中非常重要,可以防止多个进程或服务同时访问共享资源,导致数据不一致或冲突。常见的实现方式包括基于数据库、Redis、ZooKeeper 等。
1. 分布式锁的基本概念
分布式锁的核心是确保在分布式环境下,多个进程/线程可以协调对共享资源的访问。实现一个可靠的分布式锁通常需要考虑以下几个方面:
- 原子性:获取和释放锁的操作必须是原子的。
- 过期时间:防止因服务异常未释放锁而导致死锁问题。
- 锁的唯一性:确保每个锁具有唯一标识。
- 可重入性:如果需要同一进程多次获取锁,还需要考虑可重入性。
2. Redis 实现分布式锁
我们将基于 Redis 来实现分布式锁,利用 Redis 的 SETNX
命令(“set if not exists”)来确保锁的原子性,再结合锁的过期时间,避免死锁。
Redis 相关命令
- SETNX key value:如果
key
不存在,则设置key
的值为value
,并返回 1;如果key
已存在,则返回 0。 - EXPIRE key seconds:为
key
设置过期时间,过期后 Redis 会自动删除key
。 - DEL key:删除
key
,释放锁。
3. DistributedLockUtils 工具类实现
DistributedLockUtils 工具类代码
import redis.clients.jedis.Jedis;
import java.util.Collections;
/**
* DistributedLockUtils - 基于 Redis 实现的分布式锁工具类
*/
public class DistributedLockUtils {
// Lua脚本,确保原子性释放锁
private static final String LUA_SCRIPT_UNLOCK =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
/**
* 获取分布式锁
*
* @param jedis Redis客户端实例
* @param lockKey 锁的键
* @param requestId 请求ID,用于标识请求,防止误删他人的锁
* @param expireTime 锁的过期时间,单位秒
* @return 是否成功获取锁
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
// 尝试获取锁
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
return "OK".equals(result);
}
/**
* 释放分布式锁
*
* @param jedis Redis客户端实例
* @param lockKey 锁的键
* @param requestId 请求ID,确保只能释放自己持有的锁
* @return 是否成功释放锁
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
Object result = jedis.eval(LUA_SCRIPT_UNLOCK, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return result != null && "1".equals(result.toString());
}
/**
* 续期锁(延长锁的过期时间)
*
* @param jedis Redis客户端实例
* @param lockKey 锁的键
* @param requestId 请求ID,确保是自己持有的锁
* @param expireTime 新的过期时间,单位秒
* @return 是否成功续期
*/
public static boolean renewLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String currentLockOwner = jedis.get(lockKey);
if (requestId.equals(currentLockOwner)) {
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
}
4. 主要功能详解
4.1. 获取分布式锁 (tryGetDistributedLock
)
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
return "OK".equals(result);
}
-
参数说明:
jedis
:Redis 客户端实例。lockKey
:锁的键,通常与共享资源关联,如某个业务场景的标识。requestId
:请求的唯一标识,可以使用 UUID 确保每次请求的唯一性。expireTime
:锁的过期时间,防止服务异常导致锁一直存在。
-
核心逻辑:
- 使用
SET key value NX EX expireTime
命令尝试获取锁:NX
:只在key
不存在时才设置key
。EX expireTime
:设置过期时间,单位为秒。
- 如果获取锁成功,Redis 会返回
OK
,否则返回null
。
- 使用
4.2. 释放分布式锁 (releaseDistributedLock
)
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
Object result = jedis.eval(LUA_SCRIPT_UNLOCK, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return result != null && "1".equals(result.toString());
}
-
参数说明:
jedis
:Redis 客户端实例。lockKey
:锁的键。requestId
:请求的唯一标识,用于确保只能释放自己持有的锁。
-
核心逻辑:
- 使用 Lua 脚本来保证原子性:在获取到
lockKey
的value
后,判断是否与requestId
相同,若相同则删除key
以释放锁。 - 使用
jedis.eval()
方法执行 Lua 脚本,确保锁的释放过程是原子操作,避免竞争条件。
- 使用 Lua 脚本来保证原子性:在获取到
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
该 Lua 脚本首先通过 GET
操作检查 lockKey
是否为当前请求的 requestId
,如果是,则调用 DEL
删除锁;否则返回 0,表示释放失败。
4.3. 续期分布式锁 (renewLock
)
public static boolean renewLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String currentLockOwner = jedis.get(lockKey);
if (requestId.equals(currentLockOwner)) {
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
-
参数说明:
jedis
:Redis 客户端实例。lockKey
:锁的键。requestId
:请求的唯一标识,用于确认是持有锁的请求在续期。expireTime
:新的锁的过期时间。
-
核心逻辑:
- 首先通过
jedis.get(lockKey)
获取当前锁的持有者。 - 如果持有者与当前请求的
requestId
一致,则调用expire
方法延长锁的过期时间。
- 首先通过
5. 代码示例
以下是如何使用 DistributedLockUtils
工具类的示例:
import redis.clients.jedis.Jedis;
import java.util.UUID;
public class DistributedLockDemo {
public static void main(String[] args) {
// 连接到 Redis 服务器
Jedis jedis = new Jedis("localhost", 6379);
// 生成请求ID
String requestId = UUID.randomUUID().toString();
// 定义锁的键和过期时间
String lockKey = "myLock";
int expireTime = 10; // 10 秒过期时间
// 尝试获取分布式锁
boolean lockAcquired = DistributedLockUtils.tryGetDistributedLock(jedis, lockKey, requestId, expireTime);
if (lockAcquired) {
System.out.println("成功获取到锁!");
// 执行一些需要同步的业务逻辑
try {
Thread.sleep(5000); // 模拟业务操作
} catch (InterruptedException e) {
e.printStackTrace();
}
// 释放锁
boolean lockReleased = DistributedLockUtils.releaseDistributedLock(jedis, lockKey, requestId);
if (lockReleased) {
System.out.println("成功释放锁!");
} else {
System.out.println("释放
锁失败!");
}
} else {
System.out.println("获取锁失败!");
}
// 关闭 Redis 连接
jedis.close();
}
}
6. 关键点总结
- 锁的唯一性:
requestId
确保每个请求对锁的唯一标识,避免锁被其他请求误释放。 - 过期时间:使用过期时间 (
EX
) 防止死锁。 - 原子性:通过 Redis 的 Lua 脚本保证锁的获取和释放操作是原子的,避免竞态条件。
- 可扩展性:此工具类适合使用 Redis 部署的分布式系统,适用于多节点并发访问的场景,如订单处理、库存扣减等。
7. 扩展功能
- 锁续期机制:可以在锁快到期时自动续期,防止长时间任务执行时锁意外过期。
- 可重入性支持:可以扩展为可重入锁,即同一请求可以多次获取锁,不需要重复释放。
- 超时机制:可以为业务操作设置超时,防止获取锁后由于系统故障未能及时释放锁。
8. 总结
DistributedLockUtils
是一个基于 Redis 的轻量级分布式锁实现,适用于 Java 应用中需要处理多进程或多服务并发访问共享资源的场景。通过 Redis 的 SETNX
和 EXPIRE
操作,确保锁的获取、续期和释放的原子性和可靠性。