目录
以查询商品的三级分类表为例,演示过程
一、为什么使用缓存?
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 DB 承担数据落盘工作。
哪些数据适合放入缓存?
-
即时性、数据一致性要求不高的
-
访问量大且更新频率不高的数据(读多、写少)
举例:电商应用,商品分类、商品列表等适合缓存并加一个失效时间(根据数据更新频率而定),后台发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。
String data = cache.load(id);//从缓存加载数据
if(data==null){
data = db.load(id);//从数据库加载数据
cache.put(id,data);//数据放入缓存
}
return data;
注意:
在开发中,凡是放入缓存中的数据,应该指定过期时间,使其可在系统及时没有主动更新数据也能自动触发数据加载进缓存的流程,避免业务崩溃导致的数据永久不一致文档。
接下来我们看不使用缓存,使用本地缓存及使用分布式缓存的对比
二、无缓存情况
在不使用缓存的情况下,每次查询商品分类信息,都会去查询数据库。当并发访问量高时,数据库也承担了很大的压力。
java示例:
/**
* 从数据库查询分类信息
*
* @return
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
log.info("getCatalogJsonFromDb查询了数据库");
//将数据库的多次查询变为一次
List<Category> selectList = this.baseMapper.selectList(null);
//1、查出所有分类
//1、1)查出所有一级分类
List<Category> level1Categorys = getParent_cid(selectList, 0L);
//封装数据
return level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、每一个的一级分类,查到这个一级分类的二级分类
List<Category> categoryEntities = getParent_cid(selectList, v.getCatId());
//2、封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//1、找当前二级分类的三级分类封装成vo
List<Category> level3Catelog = getParent_cid(selectList, l2.getCatId());
if (level3Catelog != null) {
List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
//2、封装成指定格式
return new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(category3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
}
使用jmeter进行压力测试:
IDEA控制台打印:
从测试结果看到,每次请求都落到数据库中,在2500线程并发下,吞吐量只有132,数据库压力山大啊。
我们知道,分类信息这一类型的信息,读多,写少。大部分时候,查询到的数据都一样。那有何必每次都去查下数据库。我们可以使用缓存来解决这个问题,提高并发访问的吞吐量。
三、本地缓存
本地缓存:也叫单机缓存,也就是说可以应用在单机环境下的缓存,所谓的单机环境是指,将服务部署到一台服务器上。本地缓存的特征是只适用于当前系统。
在 java 中我们可以用 Map 来存储缓存。示例:
添加本地变量:
private HashMap<String, Map<String, List<Catelog2Vo>>> catalogJsonMap = new HashMap<>(16);
/**
* 先从本地缓存中获取分类信息
*
* @return
*/
@Override
public Map<String, List<Catelog2Vo>> getCatalogJsonByLocalCache() {
Map<String, List<Catelog2Vo>> catalogJson = catalogJsonMap.get("catalogJson");
if (CollectionUtils.isEmpty(catalogJson)) {
catalogJson = getCatalogJsonFromDb();
catalogJsonMap.put("catalogJson", catalogJson);
}
return catalogJson;
}
测试:
IDEA控制台打印:
从测试结果来看:
同样并发量下,吞吐量明显增加了很多。并且查下数据库的次数明显少了很多,明显的减轻了数据库压力。
但是,依旧会有不少请求,请求到数据库。
要知道现在基本上都是分布式系统开发,多个微服务。
在这种情况下,缓存存储在每个服务本地,无法共享,就会产生以下两个问题:
-
请求多次落库查询:每个请求进入服务时,都会查询一次数据库。(部署几份会查询几次)
-
缓存更新:当数据库表数据更新(增删改)时,请求(增删改)只会进入一个服务,当前服务可以删除缓存,下次请求在此查询数据库写入缓存。但是其他服务的缓存却没有变化,会造成服务之间缓存数据不一致问题。
解决这个问题,就需要我们引入分布式缓存。
三、分布式缓存
分布式缓存:是指可以应用在分布式系统中的缓存,所谓的分布式系统是指将一套服务器部署到多台服务器,并且通过负载分发将用户的请求按照一定的规则分发到不同服务器。
在实际的业务场景中,常用Redis作为分布式缓存数据库。
Redis缓存的优点:
相比于数据库而言,缓存的操作性能更高,缓存性能高的主要原因有以下几点:
1、缓存一般都是key-value查询数据的,因为不像数据库一样还有查询的条件等因素,所以查询的性能一般会比数据库高;
2、缓存的数据是存储在内存当中的,而数据库的数据是存储在磁盘当中的,因为内存的操作性能远远大于磁盘,因此缓存的查询效率会高很多;
3、缓存更容易做分布式部署(当一台服务器变成多台相连的服务器集群),而数据库一般比较难实现分布式部署,因此缓存的负载和性能更容易平行扩展和增加。
首先引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
示例代码:
/**
* 查询所有分类信息从redis缓存中
*
* @return
*/
@Override
public Map<String, List<Catelog2Vo>> getCatalogJsonByRedisCache() {
String catalogJson = redisTemplate.opsForValue().get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
redisTemplate.opsForValue().set("catalogJson", JSON.toJSONString(catalogJsonFromDb), 30 * 1000L, TimeUnit.MILLISECONDS);
return catalogJsonFromDb;
}
Map<String, List<Catelog2Vo>> catalogJsonMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return catalogJsonMap;
}
这样就可以做到多个微服务共享一个缓存,在其中一个服务,进行更新操作时,删除缓存,下次查询时更新缓存即可。
看到这里你会发下,上述代码中,好像只解决了分布式系统之中 缓存更新的问题,请求多次落库查询的问题并没有解决。
在高并发,多个缓存(示例中只有一个缓存key)的情况下,所有问题可以总结为以下几个问题:
-
缓存穿透:缓存当缓存key不存在时,大量的并发请求进入后端数据库,导致其压力瞬间增大。
-
缓存击穿:当缓存key失效时,致使大量的并发请求进入后端数据库,导致其压力瞬间增大。
-
缓存雪崩:缓存中大批量的 key 同时过期,而此时数据访问量又非常大,从而导致后端数据库压力突然暴增,甚至会挂掉。
别急,请看下两篇文章: