概念: 在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行,以防止并发修改变量带来数据不一致或者数据污染的现象, 而为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。
备注: 如果是单机情况下(单JVM),线程之间共享内存,只要使用本地锁就可以解决并发问题。但如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决
我们先测试一下在多线程单机情景下不加锁的情况,
一:编写代码示例如下
public Map<String, List<Catelog2Vo>> getCatalogJson() {
String category = redisTemplate.opsForValue().get("category");
Map<String, List<Catelog2Vo>> map=null;
if(StringUtils.isEmpty(category)){
System.out.println("未命中缓存---》");
//查询数据库
map= getCatalogJsonWithDb();
//插入缓存
String s = JSONObject.toJSONString(map);
redisTemplate.opsForValue().set("category",s);
}else{
System.out.println("缓存命中了---》");
map= JSON.parseObject(category, new TypeReference<Map<String, List<Catelog2Vo>>>(){
}); //内部类
}
return map;
}
二:启动200个线程,循环执行3次
查看后台打印日志,发现总共打印了100次未命中,打印了500次缓存命中
针对上述现象,和我们的理想情况下不符合,我们的理想情况应该是无论启动多少个线程并发访问,应该是只打印一次未命中
由于现在是单机环境,接下来我们开始尝试使用本地锁来解决这个问题
三:修改代码,加入synchronized锁机制
public Map<String, List<Catelog2Vo>> getCatalogJsonWithDb() {
synchronized (this){
System.out.println("开始查询数据库---》");
//查询所有分类,用来减少数据库io
List<CategoryEntity> selectList=this.baseMapper.selectList(null);//不传查询条件查所有
//List<CategoryEntity> level1 = this.listWithTree1();
//查询所有一级分类,避免直接在数据库查询
List<CategoryEntity> level1 =getParentCid(selectList,0L);
Map<String,List<Catelog2Vo>> map=level1.stream().collect(Collectors.toMap(k->k.getCatId().toString(),v->{
//查询当前一级分类对应的所有二级分类
List<CategoryEntity> categoryEntities= getParentCid(selectList,v.getCatId());
log.info("当前一级分类id是:{},对应的所有二级分类是:{}",v.getCatId(),categoryEntities);
//封装得到的结果
List<Catelog2Vo> catelog2Vos=null;
if(categoryEntities!=null){
catelog2Vos=categoryEntities.stream().map(l2->{
Catelog2Vo catelog2Vo=new Catelog2Vo(v.getCatId().toString(),l2.getCatId().toString(),l2.getName(),null);
//根据当前二级分类找到所有的三级分类再分装成vo
List<CategoryEntity> level3Catelog= getParentCid(selectList,l2.getCatId());
log.info("当前二级分类id是:{},对应的所有三级分类是:{}",l2.getCatId(),level3Catelog);
if(level3Catelog!=null){
List<Catelog2Vo.Category3Vo> collect=level3Catelog.stream().map(l3->{
Catelog2Vo.Category3Vo category3Vo=new Catelog2Vo.Category3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName());
return category3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return map;
}
}
加锁机制很简单,直接在查询数据库的方法上加个synchronized关键字,在测试之前记得先删除redis里面的key数据,由于我后端内存设置的不高,这次设置50个线程循环跑两次,启动jmeter开始测试
查看后台日志发现还是有20多次查询数据库的逻辑,原因是由于我们设置了一秒跑完,时间太短
把Rame-up时间调大到5秒,让jemeter5秒跑完请求,再看后台日志,发现还是有2次查询数据库的现象
这个原因就是接下来要说的锁时序问题,通俗的来讲就是释放锁的时间不对
解决办法,就是在线程一查询数据后不立马释放锁,应该再将查询到的结果写入缓存中以后,再将锁进行释放,
四:解决锁时序问题
示例代码如下
public Map<String, List<Catelog2Vo>> getCatalogJsonWithDb() {
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 {
System.out.println("开始查询数据库---》");
//查询所有分类,用来减少数据库io
List<CategoryEntity> selectList = this.baseMapper.selectList(null);//不传查询条件查所有
//List<CategoryEntity> level1 = this.listWithTree1();
//查询所有一级分类,避免直接在数据库查询
List<CategoryEntity> level1 = getParentCid(selectList, 0L);
Map<String, List<Catelog2Vo>> map = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//查询当前一级分类对应的所有二级分类
List<CategoryEntity> categoryEntities = getParentCid(selectList, v.getCatId());
log.info("当前一级分类id是:{},对应的所有二级分类是:{}", v.getCatId(), categoryEntities);
//封装得到的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), l2.getCatId().toString(), l2.getName(), null);
//根据当前二级分类找到所有的三级分类再分装成vo
List<CategoryEntity> level3Catelog = getParentCid(selectList, l2.getCatId());
log.info("当前二级分类id是:{},对应的所有三级分类是:{}", l2.getCatId(), level3Catelog);
if (level3Catelog != null) {
List<Catelog2Vo.Category3Vo> collect = level3Catelog.stream().map(l3 -> {
Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return category3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
//往redis里写缓存
String s = JSONObject.toJSONString(map);
redisTemplate.opsForValue().set("category", s);
return map;
}
}
}
关键代码有两块,一是线程获取锁以后再去redis中确认是否有缓存,二是在查询得到数据后,写入缓存中再释放锁
五:重新启动服务,开启jemeter进行测试
可以看到300个线程循环执行总计600次,一共就只查询了一次db,满足需求
记录一下吞吐量,响应时间等参数,后续用分布式锁再对比一下性能!!!