缓存4-分布式锁的原理与使用
为什么使用分布式锁?
上一讲缓存穿透、雪崩、击穿讲到本地锁 synchronized (this) ,JUC(lock)在单体、分布式下进行jmeter压力测试,本地锁在单体应用可以锁住所有服务,但是分布式下不能锁住所有服务。怎么办呢?
一、分布式锁
1、分布式锁与本地锁
2.分布式锁演进-基本原理
redis的命令
语法:SET key value [EX seconds] [PX milliseconds] [NX|XX]
过期时间秒 毫秒 not exist、exist
选项
从2.6.12版本开始,redis为SET命令增加了一系列选项:
-EX seconds – Set the specified expire time, in seconds.
-PX milliseconds – Set the specified expire time, in milliseconds.
-NX – Only set the key if it does not already exist.
-XX – Only set the key if it already exist.
-EX seconds – 设置键key的过期时间,单位时秒
-PX milliseconds – 设置键key的过期时间,单位时毫秒
-NX – 只有键key不存在的时候才会设置key的值
-XX – 只有键key存在的时候才会设置key的值
注意: 由于SET命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。
1).测试NX分布式锁
多开几个redis
给所有回话发送,看谁能获得锁
底部撰写栏,发送(全部xshell)
docker exec -it redis redis-cli
set lock haha NX
结果:只有一个redis可以获得锁,去占坑
2).分布式锁阶段1
a. 代码
//2.获取二级、三机分类(从数据库查询并封装分类数据)--分布式锁
public Map<String, List<Category2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁,去redis占坑
Boolean lock = redisTempalte.opsForValue().setIfAbsent("lock", "111");
if (lock) {
//加锁成功。。。执行业务
Map<String, List<Category2Vo>> dataFromDb = getDataFromDb();
//执行结束,释放锁(删除锁)
redisTempalte.delete("lock");
return dataFromDb;
} else {
//加锁失败。。。测试,synchronized()
//休眠100ms,继续重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
3).分布式锁阶段2
a.代码
问题: 设置过期时间后,万一在过期设置过期时间前(如突然断电),出现异常,那就永远死锁了
解决:设置过期时间与占位必须原子
4).分布式锁阶段3
设置过期时间与占位必须原子
public Map<String, List<Category2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁,去redis占坑
Boolean lock = redisTempalte.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
if (lock) {
//加锁成功。。。执行业务
//2.设置过期时间,必须与加锁同步,原子的
// redisTempalte.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Category2Vo>> dataFromDb = getDataFromDb();
//执行结束,释放锁(删除锁)
redisTempalte.delete("lock");
return dataFromDb;
} else {
//加锁失败。。。测试,synchronized()
//休眠100ms,继续重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
问题:锁超时时间短,把别人用的锁删除了,咋办
解决:
5).分布式锁阶段4
代码
public Map<String, List<Category2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁,去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTempalte.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
//加锁成功。。。执行业务
//2.设置过期时间,必须与加锁同步,原子的
// redisTempalte.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Category2Vo>> dataFromDb = getDataFromDb();
String lockValue = redisTempalte.opsForValue().get("lock");
if (uuid.equals(lockValue)){
//执行结束,释放锁(删除自己的锁)
redisTempalte.delete("lock");
}
return dataFromDb;
} else {
//加锁失败。。。测试,synchronized()
//休眠100ms,继续重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
问题:
解决:
6).分布式锁阶段5
操作:
代码:
//2.获取二级、三机分类(从数据库查询并封装分类数据)--分布式锁
public Map<String, List<Category2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1.占分布式锁,去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTempalte.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功。。。");
//加锁成功。。。执行业务
//2.设置过期时间,必须与加锁同步,原子的
// redisTempalte.expire("lock",30,TimeUnit.SECONDS);
//3.获取值对比+对比成功=原子操作, lua脚本解锁
Map<String, List<Category2Vo>> dataFromDb=null;
try {
dataFromDb = getDataFromDb();
}catch(Exception e){
String script= "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//删除锁
Integer lock1 = redisTempalte.execute(new DefaultRedisScript<Integer>(script, Integer.class), Arrays.asList("lock"), uuid);
}
return dataFromDb;
//获取值对比+对比成功=原子操作, lua脚本解锁
// String lockValue = redisTempalte.opsForValue().get("lock");
// if (uuid.equals(lockValue)){
// //执行结束,释放锁(删除自己的锁)
// redisTempalte.delete("lock");
// }
} else {
//加锁失败。。。测试,synchronized()
//休眠100ms,继续重试
System.out.println("获取分布式锁失败。。。等待重试");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
问题:锁自动续期(麻烦)
解决:把过期时间设置大点,(其他)
压力测试–分布式