Redis实现分布式锁的方法示例
在分布式系统中,多个服务实例同时操作共享资源时,容易出现数据不一致的问题,分布式锁就是解决这类并发问题的常用方案。Redis凭借高性能、支持原子操作的特性,成为实现分布式锁的常用工具。本文将介绍两种常见的Redis分布式锁实现方式,并给出对应的代码示例。
一、基于SETNX命令的基础实现
Redis的SETNX命令(SET if Not eXists)是早期实现分布式锁的常用命令,它的作用是当指定的key不存在时,才设置key的值,若key已存在则不做任何操作。利用这个特性,我们可以让第一个成功设置key的客户端获得锁,其他客户端则获取锁失败。
1. 基础加锁逻辑
加锁时需要设置锁的过期时间,避免客户端获取到锁之后崩溃,导致锁永远无法释放。注意SET命令支持同时设置过期时间和判断key是否存在,可替代SETNX实现原子操作。
以下是使用Jedis客户端的基础加锁示例:
import redis.clients.jedis.Jedis;
public class RedisDistributedLock {
private static final String LOCK_KEY = "distributed_lockorder";
// 锁的过期时间,单位毫秒
private static final int LOCK_EXPIRE_TIME = 10000;
/**
* 尝试获取分布式锁
* @param jedis Redis客户端实例
* @param requestId 客户端唯一标识,用于后续释放锁时校验身份
* @return 是否获取锁成功
*/
public static boolean tryLock(Jedis jedis, String requestId) {
// 使用SET命令的NX(不存在才设置)、PX(设置过期时间,单位毫秒)参数,保证原子操作
String result = jedis.set(LOCK_KEY, requestId, "NX", "PX", LOCK_EXPIRE_TIME);
// 返回OK表示设置成功,即获取锁成功
return "OK".equals(result);
}
}2. 基础释放锁逻辑
释放锁时不能直接使用DEL命令删除key,否则可能出现以下问题:客户端A获取锁之后,锁过期自动释放,此时客户端B获取到锁,客户端A此时执行完业务逻辑后调用释放锁的方法,就会误删客户端B的锁。因此释放锁时需要先校验锁的持有者身份,也就是判断当前锁的value是否和自己的requestId一致,一致才删除。
这个校验和删除的操作也需要保证原子性,否则可能出现校验通过之后、删除之前锁过期,其他客户端获取锁,依然会误删的问题。我们可以使用Lua脚本实现原子操作:
-- 释放锁的Lua脚本
-- KEYS[1]:锁的key
-- ARGV[1]:客户端的requestId
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end对应的Java释放锁实现:
import redis.clients.jedis.Jedis;
public class RedisDistributedLock {
private static final String LOCK_KEY = "distributed_lock:order";
// 释放锁的Lua脚本
private static final String RELEASE_LOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
/**
* 释放分布式锁
* @param jedis Redis客户端实例
* @param requestId 客户端唯一标识
* @return 是否释放成功
*/
public static boolean releaseLock(Jedis jedis, String requestId) {
// 执行Lua脚本,保证原子性
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, 1, LOCK_KEY, requestId);
// 返回1表示删除成功,即释放锁成功
return Long.valueOf(1).equals(result);
}
}二、基于Redisson的可重入分布式锁实现
基础的分布式锁实现存在功能上的不足,比如不支持可重入、没有自动续期机制,如果业务执行时间超过过期时间,锁会自动释放,依然可能出现并发问题。Redisson是Redis官方推荐的Java驻内存数据网格,它封装了多种分布式锁的实现,包括可重入锁、公平锁、读写锁等,使用起来更加方便,也解决了基础实现的诸多问题。
1. Redisson可重入锁的使用示例
首先需要引入Redisson的依赖,以Maven为例:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.23.3</version> </dependency>
初始化Redisson客户端,连接Redis服务:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonConfig {
public static RedissonClient getRedissonClient() {
Config config = new Config();
// 单机模式配置,替换为实际的Redis地址,示例地址为https://www.ipipp.com
config.useSingleServer().setAddress("redis://https://www.ipipp.com:6379");
return Redisson.create(config);
}
}使用Redisson的可重入锁实现分布式锁的业务逻辑:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
public class RedissonDistributedLockExample {
private static final String LOCK_KEY = "distributed_lock:order";
// 锁的过期时间,单位毫秒,Redisson会自动续期,无需担心业务执行时间超过过期时间
private static final long LOCK_LEASE_TIME = 10000;
public static void main(String[] args) {
RedissonClient redissonClient = RedissonConfig.getRedissonClient();
RLock lock = redissonClient.getLock(LOCK_KEY);
try {
// 尝试获取锁,最多等待5秒,锁的过期时间为10秒
boolean isLocked = lock.tryLock(5, LOCK_LEASE_TIME / 1000, java.util.concurrent.TimeUnit.SECONDS);
if (isLocked) {
// 获取锁成功,执行业务逻辑
System.out.println("获取分布式锁成功,执行业务逻辑");
// 模拟业务执行耗时
Thread.sleep(3000);
// 可重入特性:同一个线程可以多次获取锁,不会死锁
lock.lock();
try {
System.out.println("重入锁获取成功,执行嵌套业务逻辑");
} finally {
lock.unlock();
}
} else {
System.out.println("获取分布式锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
} finally {
// 判断当前线程是否持有锁,避免未获取锁却调用unlock导致异常
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
// 关闭Redisson客户端
redissonClient.shutdown();
}
}
}三、两种实现方式的对比
以下是基础实现和Redisson实现的特性对比:
| 对比维度 | 基于SET命令的基础实现 | 基于Redisson的实现 |
|---|---|---|
| 原子性保证 | 需要自行使用Lua脚本保证释放锁的原子性 | 内部封装了原子操作,无需手动处理 |
| 可重入性 | 不支持可重入,同一线程重复获取锁会失败 | 支持可重入,同一线程可多次获取锁 |
| 自动续期 | 无自动续期,业务执行时间超过锁过期时间会导致锁释放 | 有自动续期机制,Watch Dog会定期延长锁的过期时间 |
| 实现复杂度 | 需要手动处理各类边界问题,实现复杂度高 | 封装完善,开箱即用,实现复杂度低 |
| 适用场景 | 简单的分布式锁场景,对功能要求不高 | 复杂的分布式场景,需要可重入、自动续期等特性 |
四、使用Redis分布式锁的注意事项
锁的过期时间设置需要合理,既要避免设置过短导致业务未完成锁就释放,也要避免设置过长导致锁长时间占用。
释放锁时必须校验锁的持有者身份,避免误删其他客户端的锁。
如果是Redis集群环境,需要考虑主从切换导致的锁丢失问题,此时可以使用RedLock算法实现更可靠的分布式锁,不过RedLock本身的性能和适用场景也需要结合实际业务评估。
尽量不要在锁的持有期间执行耗时的IO操作,避免锁占用时间过长影响系统并发性能。
以上就是Redis实现分布式锁的两种常见方式的介绍,开发者可以根据自身的业务场景选择合适的实现方案。如果对功能完整性要求较高,优先选择Redisson这类成熟的工具库,避免重复造轮子带来的潜在问题。