【JAVA高级】如何使用Redis加锁和解锁(二)做分布式锁案例(防误删锁)
使用Redis中加锁和解锁的开发中,解锁需要验证当前客户端是否是锁的持有者,然后再解锁,具体怎么验证,下面是一个示例(使用redisTemplate来实现)
在使用Redis进行加锁和解锁的开发中,确实需要验证当前客户端是否是锁的持有者,然后再进行解锁操作。这是为了确保锁的安全性,避免其他客户端非法解锁导致数据不一致的问题。
在Java中,如果你使用Spring Boot并集成了Spring Data Redis,那么redisTemplate是一个常用的操作Redis的模板类。下面是一个使用redisTemplate来实现加锁和解锁(包括验证锁持有者)的示例。
首先,确保你的Spring Boot项目中已经添加了Spring Data Redis的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
你可以创建一个服务类来封装加锁和解锁的逻辑:
第一种实现思路
redisTemplate.execute(); 接收的内容直接是UNLOCK_SCRIPT Lua脚本
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
public class RedisLockService {
private static final String LOCK_PREFIX = "lock:";
private static final RedisScript<Long> UNLOCK_SCRIPT = new DefaultRedisScript<>(
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end",
Long.class
);
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 尝试加锁
*
* @param lockKey 锁的key
* @param requestId 锁的持有者标识符(如UUID)
* @param expireTime 锁的过期时间(秒)
* @return 加锁成功返回true,否则返回false
*/
public boolean tryLock(String lockKey, String requestId, long expireTime) {
String lockKeyWithPrefix = LOCK_PREFIX + lockKey;
// 使用SET命令的NX和PX选项来加锁
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKeyWithPrefix, requestId, expireTime, TimeUnit.SECONDS);
return result != null && result;
}
/**
* 解锁
*
* @param lockKey 锁的key
* @param requestId 锁的持有者标识符(如UUID)
* @return 解锁成功返回true,否则返回false
*/
public boolean unlock(String lockKey, String requestId) {
String lockKeyWithPrefix = LOCK_PREFIX + lockKey;
// 使用Lua脚本来验证锁的持有者并解锁
Long result = (Long) redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(lockKeyWithPrefix), requestId);
return result != null && result == 1L;
}
// 示例用法
public void demoLock() {
String lockKey = "myLock";
String requestId = UUID.randomUUID().toString();
long expireTime = 10L; // 锁的过期时间为10秒
// 尝试加锁
if (tryLock(lockKey, requestId, expireTime)) {
try {
// 执行需要同步的代码块
// ...
// 模拟处理时间
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
// 释放锁
unlock(lockKey, requestId);
}
} else {
// 处理加锁失败的情况
System.out.println("Lock acquisition failed");
}
}
}
代码解读:
tryLock方法使用redisTemplate.opsForValue().setIfAbsent来尝试加锁,该方法结合了SET命令的NX(Not
Exists,不存在则设置)和PX(设置键的过期时间,单位为毫秒)选项。unlock方法则使用了一个Lua脚本来执行解锁操作,该脚本首先检查Redis中存储的锁的值是否与客户端提供的值相匹配,如果匹配则删除锁。请注意,由于Redis的集群模式和网络延迟等因素,上述实现可能不是完全无懈可击的。在生产环境中,你可能需要考虑使用更健壮的分布式锁解决方案,如Redisson提供的锁服务。此外,还应该注意处理Redis服务器宕机或网络分区等异常情况。
第二种实现思路:
redisTemplate.execute()中的接的参数是new RedisCallback()对象
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
public class RedisLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LOCK_PREFIX = "lock:";
/**
* 尝试获取锁
*
* @param key 锁的key
* @param expire 锁的超时时间,单位秒
* @return 锁的唯一标识,如果获取锁失败则返回null
*/
public String tryLock(String key, long expire) {
String requestId = UUID.randomUUID().toString();
Boolean result = redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + key, requestId, expire, TimeUnit.SECONDS);
return result ? requestId : null;
}
/**
* 释放锁
*
* @param key 锁的key
* @param requestId 锁的唯一标识
* @return 是否成功释放锁
*/
public boolean unlock(String key, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws Exception {
Object nativeConnection = connection.getNativeConnection();
if (nativeConnection instanceof jedis.Jedis) {
return ((jedis.Jedis) nativeConnection).eval(script, Collections.singletonList(LOCK_PREFIX + key),
Collections.singletonList(requestId));
}
// 注意:这里应该添加对其他Redis客户端库的支持,比如Lettuce
throw new UnsupportedOperationException("Unsupported Redis client library");
}
});
return "1".equals(result.toString());
}
/**
* 调用需要加锁的方法
*
* @param key 锁的key
*/
public void criticalSection(String key) {
String requestId = tryLock(key, 10); // 尝试获取锁,超时时间为10秒
if (requestId != null) {
try {
// 执行需要同步的代码块
System.out.println("Executing critical section...");
// 模拟耗时操作
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁
unlock(key, requestId);
}
} else {
// 获取锁失败,可以选择重试、等待或返回错误
System.out.println("Failed to acquire lock for key: " + key);
}
}
}
代码解读:
加锁(lock):使用setIfAbsent方法尝试设置锁的key和value(这里value是UUID),并设置过期时间。如果key已经存在,则不会进行设置,表示锁被其他客户端持有。
解锁(unlock):使用Lua脚本来确保操作的原子性。Lua脚本首先检查锁的value是否与预期的UUID相匹配,如果匹配则删除该key,表示成功解锁;如果不匹配,则返回0,表示解锁失败(非锁的持有者)。
上述是可以通用的加锁解锁代码实现思路。OK