为什么加缓存要放在释放锁之前?
线程拿到锁会去查缓存是否有数据,又因为我们向redis存入缓存数据是在释放锁之后
那么释放锁之后,下一个线程查缓存,上一个线程并未存入完成。此时就会出现查询多次数据库的情况,锁失效
故,存缓存数据应在锁释放之前完成,尽量保证原子性操作
测试1:锁释放之后向redis缓存存入数据
//TODO 产生堆外内存溢出 OutOfDirectMemoryError
//gulimall.com查询分类
@Override
public Map<String, List<CategoryLevel2Vo>> getCatelogJson() {
/**
* 问题 :解决办法
* 1.缓存穿透 高并发情况下查询缓存不存在的数据导致并发查数据库 解决办法:查询数据库结果为null时,在缓存中设置不为null的空值(0,1),并设置过期时间,保证缓存能查到该数据
* 2.缓存雪崩 高并发情况下查询时,因为key设置相同的过期时间而key集体失效 解决办法:给key设置随机过期时间,防止集体失效 set("categoryJson",s,1,xxx)
* 3.缓存击穿 高并发情况下同时访问同一个Key 解决办法:加锁
*/
String categoryJson = stringRedisTemplate.opsForValue().get("categoryJson");
//缓存为空就查数据库
if (StringUtils.isEmpty(categoryJson)){
System.out.println("缓存未命中,查数据库");
//查数据库
Map<String, List<CategoryLevel2Vo>> fromData = getCatelogJsonFromData();
//存入redis
String s = JSON.toJSONString(fromData);
stringRedisTemplate.opsForValue().set("categoryJson",s,1, TimeUnit.DAYS);
return fromData;
}
System.out.println("缓存命中,直接返回");
//缓存有数据直接返回缓存数据
Map<String, List<CategoryLevel2Vo>> object = JSON.parseObject(categoryJson, new TypeReference<Map<String, List<CategoryLevel2Vo>>>() {});
return object;
}
查数据库逻辑
//Map<一级分类id,二级分类集合>
public Map<String, List<CategoryLevel2Vo>> getCatelogJsonFromData() {
synchronized (this) {
/**
* 这种加锁,此项目只部署在一台服务器的情况下可以,本地锁锁住一个实例
* 分布式情况下就不行,此项目部署在多台服务器上吗,每个锁锁住自己的实例只放一个线程进来
* 如果8太服务器就会有8个线程同时访问,失去了锁的作用
* 本地锁只能锁住当前进程,不能锁住其他服务
*
*/
//TODO 本地锁(当前进程锁)synchronized 、 (JUC)Lock ,分布式情况下必须使用分布式锁
//下一个线程拿到锁之后先查缓存,缓存中没有再查数据库,避免频繁查库
String categoryJson = stringRedisTemplate.opsForValue().get("categoryJson");
if (StringUtils.isNotBlank(categoryJson)){
Map<String, List<CategoryLevel2Vo>> object = JSON.parseObject(categoryJson, new TypeReference<Map<String, List<CategoryLevel2Vo>>>() {});
return object;
}
System.out.println("当前进程锁查询了数据库");
List<CategoryEntity> categoryGetAll = baseMapper.selectList(null);
//一级分类
List<CategoryEntity> category1Level = getParentCid(categoryGetAll,0L);
Map<String, List<CategoryLevel2Vo>> collect = category1Level.stream().collect(Collectors.toMap(item -> item.getCatId().toString()
, l1 -> {
//二级分类
List<CategoryEntity> category2Level = getParentCid(categoryGetAll,l1.getCatId());
List<CategoryLevel2Vo> collect2 = null;
if (category2Level != null){
collect2 = category2Level.stream().map(l2 -> {
//三级分类
List<CategoryEntity> category3Level = getParentCid(categoryGetAll,l2.getCatId());
List<CategoryLevel2Vo.CategoryLevel3Vo> collect3 = null;
if (category3Level != null){
collect3 = category3Level.stream().map(level3 -> {
return new CategoryLevel2Vo.CategoryLevel3Vo(l2.getCatId().toString(),level3.getCatId().toString(),level3.getName());
}).collect(Collectors.toList());
}
return new CategoryLevel2Vo(l1.getCatId().toString(),collect3,l2.getCatId().toString(),l2.getName());
}).collect(Collectors.toList());
}
return collect2;
}));
return collect;
}
}
JMeter性能压测
测试环境
- 测试之前清空redis缓存
- 因为是在本地服务器上面运行,单体应用服务器测试进程锁synchronized,实例对象为当前服务(数量:1)
- 测试线程数:100
预期结果:只查询一次数据库,根据逻辑,如果控制台只打印一次 “当前进程锁查询了数据库” 代表成功
测试结果
线程锁测试失败,两次查询数据库,打印了两次 “当前进程锁查询了数据库”
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
当前进程锁查询了数据库
缓存未命中,查数据库
缓存未命中,查数据库
2023-06-04 00:05:18.208 INFO 6348 --- [io-10001-exec-7] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
2023-06-04 00:05:18.297 INFO 6348 --- [io-10001-exec-7] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
缓存未命中,查数据库
2023-06-04 00:05:18.304 DEBUG 6348 --- [io-10001-exec-7] c.a.g.p.dao.CategoryDao.selectList : ==> Preparing: SELECT cat_id,name,parent_cid,cat_level,show_status,sort,icon,product_unit,product_count FROM pms_category WHERE show_status=1
缓存未命中,查数据库
2023-06-04 00:05:18.316 DEBUG 6348 --- [io-10001-exec-7] c.a.g.p.dao.CategoryDao.selectList : ==> Parameters:
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
2023-06-04 00:05:18.344 DEBUG 6348 --- [io-10001-exec-7] c.a.g.p.dao.CategoryDao.selectList : <== Total: 1425
当前进程锁查询了数据库
2023-06-04 00:05:18.351 DEBUG 6348 --- [o-10001-exec-42] c.a.g.p.dao.CategoryDao.selectList : ==> Preparing: SELECT cat_id,name,parent_cid,cat_level,show_status,sort,icon,product_unit,product_count FROM pms_category WHERE show_status=1
2023-06-04 00:05:18.351 DEBUG 6348 --- [o-10001-exec-42] c.a.g.p.dao.CategoryDao.selectList : ==> Parameters:
缓存未命中,查数据库
2023-06-04 00:05:18.365 DEBUG 6348 --- [o-10001-exec-42] c.a.g.p.dao.CategoryDao.selectList : <== Total: 1425
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
测试2:锁释放之前向redis缓存存入数据
修改代码,在锁释放前存入redis
//TODO 产生堆外内存溢出 OutOfDirectMemoryError
//gulimall.com查询分类
@Override
public Map<String, List<CategoryLevel2Vo>> getCatelogJson() {
/**
* 问题 :解决办法
* 1.缓存穿透 高并发情况下查询缓存不存在的数据导致并发查数据库 解决办法:查询数据库结果为null时,在缓存中设置不为null的空值(0,1),并设置过期时间,保证缓存能查到该数据
* 2.缓存雪崩 高并发情况下查询时,因为key设置相同的过期时间而key集体失效 解决办法:给key设置随机过期时间,防止集体失效 set("categoryJson",s,1,xxx)
* 3.缓存击穿 高并发情况下同时访问同一个Key 解决办法:加锁
*/
String categoryJson = stringRedisTemplate.opsForValue().get("categoryJson");
//缓存为空就查数据库
if (StringUtils.isEmpty(categoryJson)){
System.out.println("缓存未命中,查数据库");
//查数据库
Map<String, List<CategoryLevel2Vo>> fromData = getCatelogJsonFromData();
return fromData;
}
System.out.println("缓存命中,直接返回");
//缓存有数据直接返回缓存数据
Map<String, List<CategoryLevel2Vo>> object = JSON.parseObject(categoryJson, new TypeReference<Map<String, List<CategoryLevel2Vo>>>() {});
return object;
}
//Map<一级分类id,二级分类集合>
public Map<String, List<CategoryLevel2Vo>> getCatelogJsonFromData() {
synchronized (this) {
/**
* 这种加锁,此项目只部署在一台服务器的情况下可以,本地锁锁住一个实例
* 分布式情况下就不行,此项目部署在多台服务器上吗,每个锁锁住自己的实例只放一个线程进来
* 如果8太服务器就会有8个线程同时访问,失去了锁的作用
* 本地锁只能锁住当前进程,不能锁住其他服务
*
*/
//TODO 本地锁(当前进程锁)synchronized 、 (JUC)Lock ,分布式情况下必须使用分布式锁
//下一个线程拿到锁之后先查缓存,缓存中没有再查数据库,避免频繁查库
String categoryJson = stringRedisTemplate.opsForValue().get("categoryJson");
if (StringUtils.isNotBlank(categoryJson)){
Map<String, List<CategoryLevel2Vo>> object = JSON.parseObject(categoryJson, new TypeReference<Map<String, List<CategoryLevel2Vo>>>() {});
return object;
}
System.out.println("当前进程锁查询了数据库");
List<CategoryEntity> categoryGetAll = baseMapper.selectList(null);
//一级分类
List<CategoryEntity> category1Level = getParentCid(categoryGetAll,0L);
Map<String, List<CategoryLevel2Vo>> collect = category1Level.stream().collect(Collectors.toMap(item -> item.getCatId().toString()
, l1 -> {
//二级分类
List<CategoryEntity> category2Level = getParentCid(categoryGetAll,l1.getCatId());
List<CategoryLevel2Vo> collect2 = null;
if (category2Level != null){
collect2 = category2Level.stream().map(l2 -> {
//三级分类
List<CategoryEntity> category3Level = getParentCid(categoryGetAll,l2.getCatId());
List<CategoryLevel2Vo.CategoryLevel3Vo> collect3 = null;
if (category3Level != null){
collect3 = category3Level.stream().map(level3 -> {
return new CategoryLevel2Vo.CategoryLevel3Vo(l2.getCatId().toString(),level3.getCatId().toString(),level3.getName());
}).collect(Collectors.toList());
}
return new CategoryLevel2Vo(l1.getCatId().toString(),collect3,l2.getCatId().toString(),l2.getName());
}).collect(Collectors.toList());
}
return collect2;
}));
/**
* 为什么将存入redis放在释放锁之前?
* 线程拿到锁会去查缓存是否有数据,又因为我们向redis存入缓存数据是在释放锁之后
* 那么释放锁之后,下一个线程查缓存,上一个线程并未存入完成。此时就会出现查询多次数据库的情况,锁失效
* 故,存缓存数据应在锁释放之前完成
*/
//存入缓存redis
String s = JSON.toJSONString(collect);
stringRedisTemplate.opsForValue().set("categoryJson",s,1, TimeUnit.DAYS);
return collect;
}
}
测试结果
情况redis缓存,再次测试
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
缓存未命中,查数据库
当前进程锁查询了数据库
2023-06-04 00:15:55.335 INFO 20204 --- [o-10001-exec-84] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-06-04 00:15:55.489 INFO 20204 --- [o-10001-exec-84] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2023-06-04 00:15:55.495 DEBUG 20204 --- [o-10001-exec-84] c.a.g.p.dao.CategoryDao.selectList : ==> Preparing: SELECT cat_id,name,parent_cid,cat_level,show_status,sort,icon,product_unit,product_count FROM pms_category WHERE show_status=1
2023-06-04 00:15:55.506 DEBUG 20204 --- [o-10001-exec-84] c.a.g.p.dao.CategoryDao.selectList : ==> Parameters:
2023-06-04 00:15:55.528 DEBUG 20204 --- [o-10001-exec-84] c.a.g.p.dao.CategoryDao.selectList : <== Total: 1425
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
缓存命中,直接返回
测试成功,只打印了一次“当前进程锁查询了数据库”
单体服务本地锁测试成功,如果是分布式服务使用单体锁synchronized,那么多少服务实例就会有多少进程,虽然体量不大,但是未达到加锁的目的,为了能更好的解决缓存击穿问题,分布式情况下就不能使用单体进程锁了,必须使用分布式锁
分布式模拟测试,这里同时启动4个服务,执行线程数100,循环五次
测试结果:
第一次:只有第一个服务查询了一个数据库,其他服务未查询数据库
第二次:四个服务都只查询了一次数据库,这就符合本地锁只能锁住当前服务,不能锁住其他分布式服务