首先了解redis的set key value NX命令,该命令是只有当该key不存在时才能添加。
阶段一:
代码例:
public Map<String,List<Catelog2Vo>> getcatalogJsonFromDBWithRedisLock() {
//占用分布式锁,去redis占坑
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "xxx");
if(lock){
//加锁成功
//执行业务
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDb();
stringRedisTemplate.delete("lock");//删除锁
return dataFromDb;
}else {
//加锁失败...重试
//可以休眠一段时间
return getcatalogJsonFromDBWithRedisLock();
}
}
此时可能出现的问题:业务代码异常或程序在此过程中宕机,就不会执行删除逻辑,就会出现死锁
解决:设置锁的自动过期,即一段时间后锁会自动消失。
阶段二:
代码例:
public Map<String,List<Catelog2Vo>> getcatalogJsonFromDBWithRedisLock() {
//占用分布式锁,去redis占坑
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "xxx");
if(lock){
//加锁成功
//执行业务
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDb();
//设置过期时间
stringRedisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDb();
stringRedisTemplate.delete("lock");//删除锁
return dataFromDb;
}else {
//加锁失败...重试
//可以休眠一段时间
return getcatalogJsonFromDBWithRedisLock();
}
}
此时可能出现的问题:setnx刚刚设置好,在设置休眠时间之前又宕机,又会造成死锁。
解决:设置过期时间和占位是原子操作,redis支持使用setnx ex命令。
阶段三:
代码例:
public Map<String,List<Catelog2Vo>> getcatalogJsonFromDBWithRedisLock() {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "xxx",300,TimeUnit.SECONDS);
if(lock){
//加锁成功
//执行业务
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDb();
stringRedisTemplate.delete("lock");//删除锁
return dataFromDb;
}else {
//加锁失败...重试
//可以休眠一段时间
return getcatalogJsonFromDBWithRedisLock();
}
}
此时可能出现的问题:如果执行业务的时间过长,锁自己过期了,此时再执行删除操作,可能把其他线程的锁删除了。
解决:占用锁的时候,值指定为uuid,删除时需匹配,只能删除匹配成功的。
阶段四:
代码例:
public Map<String,List<Catelog2Vo>> getcatalogJsonFromDBWithRedisLock() {
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDb();
String lockValue = stringRedisTemplate.opsForValue().get("lock");
if(uuid.equals(lockValue)){
stringRedisTemplate.delete("lock");
}
return dataFromDb;
}else {
//加锁失败...重试
//可以休眠一段时间
return getcatalogJsonFromDBWithRedisLock();
}
}
此时可能出现的问题:获取到的lockValue与uuid匹配成功,但是在执行删除锁操作之前,锁自己过期了,就会将其他线程的锁删除。
解决:将获取lockVlue和删除lock设置成原子操作。
阶段五:
代码例:
public Map<String,List<Catelog2Vo>> getcatalogJsonFromDBWithRedisLock() {
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
//加锁成功
//执行业务
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDb();
//获值对比+匹配成功删除是原子操作;lua脚本解锁
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//删除锁
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
return dataFromDb;
}else {
//加锁失败...重试
//可以休眠一段时间
return getcatalogJsonFromDBWithRedisLock();
}
}
此时还可能出现的问题:业务还没有执行完,锁过期了,此时我们需要让锁自动续期。当然最简单的方法是加长锁的时间,但是不现实。所以这里就让业务正常执行,执行完了再删除锁。
最终代码例:
public Map<String,List<Catelog2Vo>> getcatalogJsonFromDBWithRedisLock() {
//占用分布式锁,去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
Map<String,List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
//获值对比+匹配成功删除是原子操作;lua脚本解锁
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//删除锁
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
}
return dataFromDb;
}else {
//加锁失败...重试
//可以休眠一段时间
return getcatalogJsonFromDBWithRedisLock();
}
}
总结:保证加锁过程的原子性和删锁过程的原子性。