以前章的redis做缓存出现缓存击穿加锁为例
1.加本地锁
synchronized (this)
在单体应用没问题,但是分布式下本地锁只能锁住当前线程,锁不住其他。
如有多台应用服务,每个加本地锁,也有几把锁,发送多个请求,
所以在分布式情况下,就需要分布式锁
2.分布式锁
缺点:性能慢
我们可以同时去一个地方
“
占坑
”
,如果占到,就执行逻辑。否则就必须等待,直到释放锁。
“
占坑
”
可以去
redis
,可以去数据库,可以去任何大家都能访问的地方。
等待可以
自旋
的方式。
分布式锁演进
-
阶段一
使用redis的NX机制
死锁:业务代码异常或者程序宕机,没有执行删除锁逻辑,这就照成了死锁
解决:设置锁的自动过期,即使没有删除,会自动删除
![](https://img-blog.csdnimg.cn/e9be0f1cfd514b55b06abb4adac578cc.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6L-96YCQ6Lev5LiK55qE5bCP5Lq654mp,size_20,color_FFFFFF,t_70,g_se,x_16)
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁。去redis占坑
Boolean locak = redisTemplate.opsForValue().setIfAbsent("locak", "111");
if(locak){
//加锁成功。。。
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("locak");
return dataFromDb;
}else{
//没成功-...重试
//休眠100毫秒
return getCatalogJsonFromDbWithRedisLock();
}
}
分布式锁演进
-
阶段二
业务代码设置过期时间
同意会出现的问题如果,设置过期时间的代码块出现了系统异常也会出现死锁
解决:
设置过期时间和占位必须是原子的。
redis
支持使用
setnx ex 命令
![](https://img-blog.csdnimg.cn/9615d5371e9e47ccbacf22136c0bda74.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA6L-96YCQ6Lev5LiK55qE5bCP5Lq654mp,size_20,color_FFFFFF,t_70,g_se,x_16)
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁。去redis占坑
Boolean locak = redisTemplate.opsForValue().setIfAbsent("locak", "111");
if(locak){
//加锁成功。。。
//2.设置过期时间
redisTemplate.expire("locak",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("locak");
return dataFromDb;
}else{
//没成功-...重试
//休眠100毫秒
return getCatalogJsonFromDbWithRedisLock();
}
}
分布式锁演进
-
阶段三
又出现一个问题就是删除锁,
由于业务时间很长,自己的锁过期了,已经删除了,我再删除锁,可能删除的就是别人正在持有的锁
解决:
占锁的时候,值指定为
uuid
,每个人匹配是自己
的锁才删除。
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁。去redis占坑
Boolean locak = redisTemplate.opsForValue().setIfAbsent("locak", "111",300,TimeUnit.SECONDS);
if(locak){
//加锁成功。。。
//2.设置过期时间,必须和加锁是同步的,原子的
// redisTemplate.expire("locak",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("locak");
return dataFromDb;
}else{
//没成功-...重试
//休眠100毫秒
return getCatalogJsonFromDbWithRedisLock();
}
}
分布式锁演进
-
阶段四
在里还会出现一个问题就是,当判断值和删除锁的时候,锁过期,别人已经设置到了新的值,那么我们删除的就是别人的锁的值
解决:
删除锁必须保证原子行,即:获取值对比+对比成功删除=原子操作
使用redis+Lua脚本完成
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁。去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean locak = redisTemplate.opsForValue().setIfAbsent("locak", uuid,300,TimeUnit.SECONDS);
if(locak){
//加锁成功。。。
//2.设置过期时间,必须和加锁是同步的,原子的
// redisTemplate.expire("locak",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
// redisTemplate.delete("locak");
//获取值对比+对比成功删除=原子操作
String locaValue = redisTemplate.opsForValue().get("locak");
if(uuid.equals(locaValue)){
//删除自己的锁
redisTemplate.delete("locak");
}
return dataFromDb;
}else{
//没成功-...重试
//休眠100毫秒
return getCatalogJsonFromDbWithRedisLock();
}
}
分布式锁演进
-
阶段五
-
最终形态
总结:(保证加锁和删除都是原子行的操作)
保证加锁【占位
+
过期时间】和删除锁【判断
+
删除】的原子性。
更难的事情,锁的自动续期
当然最简单的自动续期解决是把锁的时间加长
当然最简单的自动续期解决是把锁的时间加长
//从数据库查询并封装数据
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁。去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean locak = redisTemplate.opsForValue().setIfAbsent("locak", uuid,300,TimeUnit.SECONDS);
if(locak){
//加锁成功。。。
//2.设置过期时间,必须和加锁是同步的,原子的
// redisTemplate.expire("locak",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
String script="if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//删除锁
Integer locak1 = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class)
, Arrays.asList("locak"), uuid);
}
// redisTemplate.delete("locak");
//获取值对比+对比成功删除=原子操作
// String locaValue = redisTemplate.opsForValue().get("locak");
// if(uuid.equals(locaValue)){
// //删除自己的锁
// redisTemplate.delete("locak");
// }
return dataFromDb;
}else{
//没成功-...重试
//休眠100毫秒
return getCatalogJsonFromDbWithRedisLock();
}
}
Redisson-lock(可重入锁)
A方法里有B ,A加了1号锁,b发现A加了1号锁,就沿用A里的1号锁,但是必须等A用完1号锁,这种锁叫不可重入锁