import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
/**
* 较为基本的redis-lock
* 不支持重入
* 暂时不支持锁续命,锁续命可能导致死锁
* uuid -> 视为客户端版本,只有正确的客户端才能解锁
* 增加了lock,会持续获取,直到成功
*/
@Slf4j
@Service
public class RedisLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final ScheduledExecutorService scheduledSinglePool =
Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors() * 2 + 1);
private static final String FIXED_TIME = "15";
// 无法获取到锁时,尝试重试
public void lock(String key, String uuid) {
Long pttl = tryAcquire(key, uuid);
// 如果能快速获得,则不用进入循环
if (pttl == null) {
return;
}
while (true) {
if (pttl == null || pttl <= 0) {
startDaemon(key, uuid);
break;
}
try {
Thread.sleep(pttl);
} catch (InterruptedException e) {
}
}
}
/**
* @return 注意返回的是 毫秒数
*/
public Long tryAcquire(String key, String uuid) {
// 当前lua脚本中,加锁时间偏短, 15s
String tryLockInner =
"if ((redis.call('exists', KEYS[1]) == 0)) then " +
"redis.call('set', KEYS[1], ARGV[1]); " +
"redis.call('expire', KEYS[1], ARGV[2]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);";
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.eval(tryLockInner.getBytes(), ReturnType.INTEGER, 1,
key.getBytes(StandardCharsets.UTF_8),
uuid.getBytes(StandardCharsets.UTF_8),
FIXED_TIME.getBytes(StandardCharsets.UTF_8)));
}
// 未获取到redis锁时,应当放弃任务
public boolean tryLock(String key, String uuid) {
Long pttl = tryAcquire(key, uuid);
return pttl == null || pttl <= 0;
}
private void startDaemon(String key, String uuid) {
Thread daemon = new Thread(new DaemonTask(key, uuid));
daemon.setDaemon(true);
daemon.start();
}
public void unlock(String key, String uuid) {
try {
// 释放锁使用 Lua 脚本,验证传入的 key--uuid 跟 Redis 中是否一样
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
key.getBytes(StandardCharsets.UTF_8), uuid.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
log.error("解锁失败 key = {}", key);
}
}
@AllArgsConstructor
class DaemonTask implements Runnable {
private String key;
private String uuid;
// 开启守护线程,可以续命锁
// 但是如果时序不正确,会导致死锁
@Override
public void run() {
// lua 脚本
// 当前锁不存在,则是由当前客户端删除的,不应当继续续命
// 锁的声明周期已经完全结束
// 当前锁存在,不论ttl<0还是>0,都应当续命
Long pttl = redisTemplate.getExpire(key);
// 删除锁时,应当直接结束任务
if (pttl == null) {
return;
}
Long pttl = tryAcquire(key, uuid);
// 使用延时任务,延续时间
if (pttl >= 0) {
scheduledSinglePool.schedule(this, pttl, TimeUnit.MICROSECONDS);
}
}
}
}
代码是完成了,应当做哪些测试来确保安全哪?
修改版本
todo 修改续命锁逻辑
// 追加 lua 脚本
// 当前锁不存在,则是由当前客户端删除的,不应当继续续命
// 锁的声明周期已经完全结束
// 当前锁存在,则应当续命