✅P156-157_缓存-缓存使用-加锁解决缓存击穿问题
大约 6 分钟
操作前注意删除缓存
本地锁实现
代码实现
给查数据库的方法加上锁,得到锁之后,再去缓存中查询一次,如果没有,再继续查询
代码如下:
cfmall-product/src/main/java/com/gyz/cfmall/product/service/impl/CategoryServiceImpl.java
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//加入缓存逻辑
String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
if (StringUtils.isEmpty(catelogJSON)) {
//缓存中没有,查询数据库
System.out.println("缓存未命中.....查询数据库");
Map<String, List<Catelog2Vo>> categoryJsonMap = getCatalogJsonFromDb();
String newCategoryJson = JSON.toJSONString(categoryJsonMap);
// 设置过期时间,解决缓存雪崩
stringRedisTemplate.opsForValue().set("catelogJSON", newCategoryJson, 1, TimeUnit.DAYS);
return categoryJsonMap;
}
System.out.println("缓存命中.....直接返回");
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
//从数据库查询并封装分类的数据
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
//this代表当前实例对象,这里是锁当前对象。
synchronized (this) {
//得到锁后,我们应该再去缓存确定一次,如果没有才需要继续查询
String catalogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
System.out.println("查询了数据库.....");
// 一次性获取所有数据
List<CategoryEntity> selectList = baseMapper.selectList(null);
// 1)、所有1级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
// 2)、封装数据
Map<String, List<Catelog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
// 查到当前1级分类的2级分类
List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId());
List<Catelog2Vo> catalog2Vos = null;
if (category2level != null) {
catalog2Vos = category2level.stream().map(level12 -> {
// 查询当前2级分类的3级分类
List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId());
List<Catelog2Vo.Category3Vo> catalog3Vos = null;
if (category3level != null) {
catalog3Vos = category3level.stream().map(level13 -> {
return new Catelog2Vo.Category3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName());
}).collect(Collectors.toList());
}
return new Catelog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName());
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
return result;
}
}
Jmeter压测
线程组
HTTP请求
效果
出现问题 :查询了多次数据库,本地锁时序问题
问题原因:当线程1获得锁之后查询数据库,返回查询的结果并释放锁;线程1在将查询的数据存储到redis时,线程2获得锁,发现此时缓存并未命中,因此进行数据库的查询
解决方案:确认缓存没有、查询数据库、把存入缓存的操作放在锁中
锁时序问题解决
实现代码如下:将getCatalogJson()
方法存储分类数据到Redis的操作放到synchronized锁中进行
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//加入缓存逻辑
String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
if (StringUtils.isEmpty(catelogJSON)) {
//缓存中没有,查询数据库
System.out.println("缓存未命中.....查询数据库");
Map<String, List<Catelog2Vo>> categoryJsonMap = getCatalogJsonFromDb();
// String newCategoryJson = JSON.toJSONString(categoryJsonMap);
// // 设置过期时间,解决缓存雪崩
// stringRedisTemplate.opsForValue().set("catelogJSON", newCategoryJson, 1, TimeUnit.DAYS);
// return categoryJsonMap;
}
System.out.println("缓存命中.....直接返回");
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
/**
* 从数据库查询并封装分类的数据
* @return
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
//this代表当前实例对象,这里是锁当前对象。
synchronized (this) {
//得到锁后,我们应该再去缓存确定一次,如果没有才需要继续查询
String catalogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
System.out.println("查询了数据库.....");
// 一次性获取所有数据
List<CategoryEntity> selectList = baseMapper.selectList(null);
// 1)、所有1级分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
// 2)、封装数据
Map<String, List<Catelog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
// 查到当前1级分类的2级分类
List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId());
List<Catelog2Vo> catalog2Vos = null;
if (category2level != null) {
catalog2Vos = category2level.stream().map(level12 -> {
// 查询当前2级分类的3级分类
List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId());
List<Catelog2Vo.Category3Vo> catalog3Vos = null;
if (category3level != null) {
catalog3Vos = category3level.stream().map(level13 -> {
return new Catelog2Vo.Category3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName());
}).collect(Collectors.toList());
}
return new Catelog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName());
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
//getCatalogJson()方法存储分类数据到Redis的操作移动到此处
String catelogInfo = JSON.toJSONString(collect);
stringRedisTemplate.opsForValue().set("catelogJSON", catelogInfo, 1, TimeUnit.DAYS);
return collect;
}
}
Jmeter压测
线程组
HTTP请求
效果:1000个线程访问接口,只查询了一次数据库
本地锁时序问题
测试结果发现查询了多次数据库
原因:
- 假设现在有100W并发,进来之后这些线程先去看缓存,结果缓存里面都没有,都准备去查询数据库
- 以其中一台机器为例,查数据库的时候,上来就加了一把锁,确保只让一个线程进来,假设为A线程,它在查询数据库之后,结果放入缓存之前,就将锁释放掉了
- 此时,锁住的B线程进来了,它进来之后,也是先确认缓存中有没有,此时A线程刚释放锁,要往缓存中放数据,然而放数据是一次网络交互,可能会很慢,包括系统刚启动、还要为redis建立连接、还要整线程池、线程池还没有初始化等等,所以第一次操作是一个很慢的过程,假设会花费30ms的时间。
- 可能C线程刚进来,A线程的数据才放到缓存中,B线程的数据还没放完,所以C线程判断缓存有没有数据的时候,可能判断的就是A线程之前给里边放的缓存数据,所以C线程就不会查询数据库了。
- 最终A线程查询了一次,B线程查询了一次。
分析
- 无论我们是给方法块、还是代码块上加锁,都是将当前实例作为锁,当前实例在我们容器中是单实例;
- 但是我们是一个服务对应一个容器,里面的每一个 this 只能代表当前实例的对象,以上图为例,八个容器就有八个锁, 每一个 this 都是不同的锁;
- 最终导致的现象就是,第1个商品服务的一把锁,锁住了1W个请求,只放进了一个请求,第2个乃至后面的商品服务,都是如此,有几台机器,最终就会放几个线程进来,那就相当于有8个线程同时进来,去数据库查相同的数据
比如:我们可以多创建几个商品服务,来模拟这种测试,由网关负载均衡到某一个商品服务,通过Nginx-> 网关 -> 多个商品服务,来进行压测。不同端口代表不同商品服务。
测试步骤
复制cfmall-product
服务,端口分别为8200
、8201
、8202
、8203
,具体操作如下:
选中服务名称,鼠标右键,
Jmeter压测三级分类
测试结果