V1: setnx命令
一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占用。
先来先占,用完了,再调用del指令释放。
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致del指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
V2: 锁超时释放
所以在拿到锁之后,再给锁加上一个过期时间,比如5s,这样即使中间出现异常也可以保证5秒之后锁会自动释放。
但是如果在setnx和expire之间服务器进程突然挂掉了,就会导致expire得不到执行,也会造成死锁。
这种问题的根源就在于setnx和expire是两条指令而不是原子指令。如果这两条指令,可以一起执行就不会出现问题。
V3:set指令
这个问题在Redis2.8版本中得到了解决,这个版本加入了set指令的扩展参数,使得setnx和expire指令可以一起执行。
上面这个指令就是setnx和expire组合在一起的原子指令
// v1
public class RedisLock {
@Autowired
StringRedisTemplate stringRedisTemplate;
private String name;
private Double timeoutSec;
private static final String KEY_PREFIX = "lock:";
public boolean tryLock(long timeoutSec) {
//获取线程标示
Long threadId = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void unlock() {
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
v4:解决误删问题
线程1进行加锁 但是由于意外原因发送阻塞,没能在设置key过期时间内进行操作。锁自动释放
线程2之后进行加锁,然后正常处理业务。。。
线程1恢复正常 执行完成后直接删除了key,线程1误删了线程2 的锁
为此应该加入验证机制,释放锁之前检查是否是当前线程设置的锁
使用UUID+线程id
private static final string ID_PREFIX = UUID.randomUUID().tostring(true);
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS
);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if (threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
v5:lua脚本保证原子性
判断锁和释放锁之间出现了堵塞,也会导致释放其他线程的锁
可以使用lua脚本保证判断锁和释放锁的原子性
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get',KEYS[1]) == ARGV[1]) then
-- 释放锁del key
return redis.call('del',KEYS[1])
end
return 0
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
//执行lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId()
);
//释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
基本完成了一个生产可用的分布式锁