一:使用场景
如果是单机情况下(单JVM),线程之间共享内存,只要使用本地锁就可以解决并发问题。但如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决
二:基本概念
对于分布式应用场景下,当多个线程对公共资源访问时,需要实现排他性,传统的多线程锁已经不能满足(独立于某个进程之外),这时候就需要分布式锁的实现。
分布式锁主要是要求将锁的管理交由外部公共的第三方管理,主要有数据库、Redis、zookeeper三种
在使用分布式锁之前,我们先测试一下如果使用本地锁,在分布式场景下会出现什么问题
三:模拟分布式场景,后台启动多个服务
四:启动imeter,开启300个线程开始测试
五:查看后台日志
服务一查询了一次数据库
服务二查询了一次数据库
服务三和四也都各自查询了一次数据库
总共也就是查询了四次数据库,这说明我们的本地锁在分布式场景下失效了
六:使用分布式锁
分布式锁主要实现方式有很多种,本文初步使用redis来作为分布式锁进行测试实验 redis实现分布式锁的实现主要就是靠一个命令:SETNX key value redis官网地址:www.redis.cn/commands/se… redis官网关于该命令的详细介绍如下:
流程设计大致如下
6.1:祛除本地锁
public Map<String, List<Catelog2Vo>> getCatalogJsonLock() {
// synchronized (this) {}
//得到锁以后应该再去缓存中查询一次,解决锁时序问题
String category = redisTemplate.opsForValue().get("category");
if (!StringUtils.isEmpty(category)) {
Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(category, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return stringListMap;
} else {
//查询数据库
return getCatalogJsonWithDb();
}
}
6.2添加获取锁逻辑
public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLock() {
// synchronized (this) {}
Boolean mykey = redisTemplate.opsForValue().setIfAbsent("mykey", "1111");
if(mykey){
//得到锁以后去查询缓存
Map<String, List<Catelog2Vo>> map=getCatalogJsonWithRedisCatch();
//执行业务逻辑后释放锁
redisTemplate.delete("mykey");
return map;
}else{
log.info("重试获取锁--->");
return getCatalogJsonWithRedisLock();
}
}
6.3获的锁的线程再去查询缓存
private Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisCatch() {
//得到锁以后应该再去缓存中查询一次,解决锁时序问题
String category = redisTemplate.opsForValue().get("category");
if (!StringUtils.isEmpty(category)) {
Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(category, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return stringListMap;
} else {
//查询数据库
return getCatalogJsonWithDb();
}
}
6.4开启jemter进行测试
发现四个后端服务只查询了一次数据库,说明加锁是成功的
请求中出现了一些异常,都是服务器压力过大,nginx服务崩了,先不管这个报错
仔细思考,这样简单加个setnx锁会有什么问题?
七:分布式锁的改进优化
很容易想到的第一个问题,就是setnx占位以后,在当前线程执行业务代码时,出现异常或者宕机,而没有去主动释放锁,可能就造成了死锁,其它线程就无法获得该锁
7.1设置锁的自动过期时间,即使出现异常,也会到期自动删除
改造getCatalogJsonWithRedisLock方法如下
//设置5分钟的过期时间
redisTemplate.expire("lock",300,TimeUnit.SECONDS);
7.2释放锁的时候,有可能锁已经失效(设置过期时间,突然宕机造成死锁)
解决办法:设置过期时间和占位锁必须是原子性的,对应redis中的命令是setNX EX,代码如下
public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLock() {
// synchronized (this) {}
//Boolean mykey = redisTemplate.opsForValue().setIfAbsent("mykey", "1111");
//设置一分钟的过期时间
Boolean mykey =redisTemplate.opsForValue().setIfAbsent("lock","1111",300, TimeUnit.SECONDS);
if(mykey){
//得到锁以后去查询缓存
Map<String, List<Catelog2Vo>> map=getCatalogJsonWithRedisCatch();
//设置5分钟的过期时间
//redisTemplate.expire("lock",300,TimeUnit.SECONDS);
//执行完业务后释放锁
System.out.println("释放锁--->");
redisTemplate.delete("lock");
return map;
}else{
System.out.println("重试获取锁--->");
return getCatalogJsonWithRedisLock();
}
}
7.3如何删除锁
假设有这样一种场景:如果由于业务时间很长,锁自己已经过期了,我们直接删除,有可能把别的线程正在持有的锁删除了 解决办法:线程占锁的时候,值指定为uuid,每个线程匹配是自己的锁才删除 改造代码如下:
public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLock() {
// synchronized (this) {}
String uuId= UUID.randomUUID().toString();
//Boolean mykey = redisTemplate.opsForValue().setIfAbsent("mykey", "1111");
//设置一分钟的过期时间
Boolean mykey =redisTemplate.opsForValue().setIfAbsent("lock",uuId,300,TimeUnit.SECONDS);
if(mykey){
//得到锁以后去查询缓存
Map<String, List<Catelog2Vo>> map=getCatalogJsonWithRedisCatch();
//redisTemplate.expire("lock",300,TimeUnit.SECONDS);
//执行完业务后释放锁
System.out.println("释放锁--->");
String value=redisTemplate.opsForValue().get("lock");
if(StringUtils.equals(uuId,value)){
//删除自占锁
redisTemplate.delete("lock");
}
return map;
}else{
System.out.println("重试获取锁--->");
return getCatalogJsonWithRedisLock();
}
}
7.4上述删除锁的问题
如果正好判断是当前值,正要删除锁的时候,这个时候锁过期了,别的线程已经设置到了新的值,那么就会删除别的线程正在占用的锁
解决办法:保证删除锁的原子性
redis官网有这样一段介绍 value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过以下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>> getCatalogJsonWithRedisLock() {
// synchronized (this) {}
String uuId= UUID.randomUUID().toString();
//Boolean mykey = redisTemplate.opsForValue().setIfAbsent("mykey", "1111");
//设置一分钟的过期时间
Boolean mykey =redisTemplate.opsForValue().setIfAbsent("lock",uuId,300,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromRedis=null;
if(mykey){
try{
dataFromRedis=getCatalogJsonWithRedisCatch();
}catch (Exception e){
}finally {
String lua="if redis.call("get",KEYS[1]) == ARGV[1] then\n" +
" return redis.call("del",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
redisTemplate.execute(new DefaultRedisScript<Long>(lua,Long.class), Arrays.asList("lock"),uuId);
}
//执行完业务后释放锁
System.out.println("释放锁--->");
return dataFromRedis;
}else{
System.out.println("重试获取锁--->");
return getCatalogJsonWithRedisLock();
}
}
7.5最后加上重试获取锁机制
System.out.println("重试获取锁--->");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) { e.printStackTrace(); }
return getCatalogJsonWithRedisLock();
开启jmeter测试后满足需求,分布式锁的第一个阶段到此结束了,后续使用reddison来实现分布式锁!!!