参考
谷粒商城p158:https://www.bilibili.com/video/BV1np4y1C7Yf?p=158
redis官网:http://www.redis.cn/commands/set.html
提要
分布式场景下将原有的本地锁换为,基于redis的setnx命令的分布式锁
getCatalogJsonFromDb
:从数据库查数据getCatalogJsonFromDbWithLocalLock
:利用本地锁查数据getCatalogJsonFromDbWithRedisLock
:利用redis的setnx命令的分布式锁查数据,现需要完成的
分布式锁演进-阶段一
代码
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
// 1、占分布式锁,setnx
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
if(lock){
// 加锁成功。。。执行任务
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
redisTemplate.delete("lock");
return catalogJsonFromDb;
}else{
// 加锁失败。。。重试
// 休眠100ms重试
// 自旋方式
return getCatalogJsonFromDbWithRedisLock();
}
}
问题
setnx
占好了位置,业务代码异常或程序宕机,没有执行删锁逻辑,死锁!!!
解决
- 设置锁自动过期,即使没有删除,到期也会消失
分布式锁演进-阶段二
代码
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
// 1、占分布式锁,setnx
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
if(lock){
// 加锁成功。。。执行任务
// 2、设置过期时间
redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
redisTemplate.delete("lock");
return catalogJsonFromDb;
}else{
// 加锁失败。。。重试
// 休眠100ms重试
// 自旋方式
return getCatalogJsonFromDbWithRedisLock();
}
}
问题
- 加锁与设置过期时间非原子操作,所以仍会出现死锁问题
解决
- 利用
redis
提供的setexnx
,做原子操作
分布式锁演进-阶段三
代码
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
// 1、占分布式锁,setnx
// 同时设置时间
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if(lock){
// 加锁成功。。。执行任务
// 2、设置过期时间
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
redisTemplate.delete("lock");
return catalogJsonFromDb;
}else{
// 加锁失败。。。重试
// 休眠100ms重试
// 自旋方式
return getCatalogJsonFromDbWithRedisLock();
}
}
问题
- 如果业务时间过长,我们的锁过期自动删除,这时直接删锁,有可能把别人正在持有的锁删除了
解决
- 占锁时指定
uuid
保证唯一性,删锁需要验证是否是自己的锁
分布式锁演进-阶段四
代码
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
// 1、占分布式锁,setnx
// 同时设置时间
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if(lock){
// 加锁成功。。。执行任务
// 2、设置过期时间
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
// redisTemplate.delete("lock");
String lockValue = redisTemplate.opsForValue().get("lock");
// 验证是否是自己的锁
if(uuid.equals(lockValue)){
// 是,删锁
redisTemplate.delete("lock");
}
return catalogJsonFromDb;
}else{
// 加锁失败。。。重试
// 休眠100ms重试
// 自旋方式
return getCatalogJsonFromDbWithRedisLock();
}
}
问题
- 注意在
redis
,get
锁的值时,即String lockValue = redisTemplate.opsForValue().get("lock");
,这时可能redis
还存在我们的锁,这时返回的正是我们uuid
,但是因为网络传输的延时,我们要执行delete
操作时,我们的锁已经因为过期策略删除了,所以虽然这是的锁不是我们的,但程序代码仍然会执行删除锁(并非我们的),本质仍是非原子操作问题
解决
lua
脚本,实现原子操作
分布式锁演进-阶段五
代码
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
// 1、占分布式锁,setnx
// 同时设置时间
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
Map<String, List<Catalog2Vo>> catalogJsonFromDb = null;
if (lock) {
// 加锁成功
// 2、设置过期时间
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
try {
catalogJsonFromDb = getCatalogJsonFromDb();
} finally {
// 获取值对比+对比成功删除=原子操作
// String lockValue = redisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue)) {
// redisTemplate.delete("lock");
// }
// 脚本解锁
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long unlock = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return catalogJsonFromDb;
} else {
// 加锁失败。。。重试
// 休眠100ms重试
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock();
}
}
总结
使用redis
做分布式锁
- 加锁和设置过期时间的原子性问题
- 解锁与验证锁的归属的原子性问题
- 还有业务时间与过期时间的设置,有时需要延长过期时间
可以考虑使用Redisson