本地锁只能锁住当前进程,所以我们需要分布式锁
Redis: SET key value [EX] [NX] [XX]
其中:EX:设置过期时间
NX:Only set the key if it does not already exist. 当redis中没有这个key的时候才去设置这个key
在docker中模拟:
向5个客户端发送(docker环境): docker exec -it redis redis -cli 以命令行的方式进入redis会话中
向5个客户端发送:set lock haha NX 这5个去抢占,只有一个能成功
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock(){
// 1.占分布式锁,去redis占坑 setIfAbsent相当于set lock 111 NX
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
if(lock){
// 加锁成功,执行业务
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("lock");
return dataFromDb;
} else {
// 加锁失败,重试 休眠100ms
return getCatelogJsonFromDbWithRedisLock(); // 自旋的方式
}
}
存在问题:如果没有执行到删锁逻辑,就变成了死锁;
考虑:设置过期时间:
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock(){
// 1.占分布式锁,去redis占坑 setIfAbsent相当于set lock 111 NX
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
if(lock){
// 加锁成功,执行业务
// 设置过期时间
redisTemplate.expire("lock",30,TimeUnit.SECONDS)
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("lock");
return dataFromDb;
} else {
// 加锁失败,重试 休眠100ms
return getCatelogJsonFromDbWithRedisLock(); // 自旋的方式
}
}
还存在问题: 如果加锁和设置过期时间之间突然断电,设置过期时间的代码执行不到,导致死锁
解决:将加锁和设置过期时间处理为原子操作 set lock 111 EX 300 NX
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock(){
// 1.占分布式锁,去redis占坑 setIfAbsent相当于set lock 111 NX
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111",300,TimeUnit.SECONDS); // 设置加锁和过期时间1行代码完成
if(lock){
// 加锁成功,执行业务
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("lock");
return dataFromDb;
} else {
// 加锁失败,重试 休眠100ms
return getCatelogJsonFromDbWithRedisLock(); // 自旋的方式
}
}
还存在问题:如果业务的执行时间,大于设置的锁的过期时间,这样会导致锁过期后,别的锁进来,删除的是别人的锁
解决:占锁的时候,指定一个UUID,每个人的都不一样
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock(){
// 1.占分布式锁,去redis占坑 setIfAbsent相当于set lock 111 NX
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS); // 设置加锁和过期时间1行代码完成
if(lock){
// 加锁成功,执行业务
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
String lockValue = redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockValue)){
redisTemplate.delete("lock");
}
return dataFromDb;
} else {
// 加锁失败,重试 休眠100ms
return getCatelogJsonFromDbWithRedisLock(); // 自旋的方式
}
}
还可能存在问题: String lockValue = redisTemplate.opsForValue().get("lock"); 耗时比较长,导致uuid和缓存中的虽然一样,满足(uuid.equals(lockValue),但是实际上redis中的lock可能已经设置为新的值了
解决:获取值对比 + 对比成功删除 ==> 需要为原子操作 使用lua脚本解锁
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock(){
// 1.占分布式锁,去redis占坑 setIfAbsent相当于set lock 111 NX
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS); // 设置加锁和过期时间1行代码完成
if(lock){
// 加锁成功,执行业务
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
String script = "if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end";
// 删除锁(原子操作)
redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid)
return dataFromDb;
} else {
// 加锁失败,重试
return getCatelogJsonFromDbWithRedisLock(); // 自旋的方式
}
}