1 缓存使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访间。而db承担数据落盘工作。
哪些数据适合放入锾存?
- 即时性、数据—致性要求不高的
- 访问量大且更新频率不高的数据(读多,写少)
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的。
2 本地缓存与分布式缓存
2.1 本地缓存
2.2 分布式缓存-本地模式在分布式下的问题
其中一个服务的缓存修改了,但是其他服务里的缓存没有办法更新,无法保证数据的一致性
2.3分布式缓存
3 整合Redis
3.1 引入依赖
product模块
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 配置redis
redis:
host: 192.168.56.10
port: 6379
3.3 单元测试
@Test
void test03() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("hello", "world_" + UUID.randomUUID());
String hello = ops.get("hello");
System.out.println("保存的数据是:" + hello);
}
4 改造三级分类业务
将原来的getCatalogJson方法改名为getCatalogJsonFromDB,并创建一个新的getCatalogJson方法
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 给缓存中放JSON字符串,拿出的JSON字符串,还需要逆转为能用的对象类型
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
// 缓存中没有,查询数据库
Map<String, List<Catelog2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
// 将查到的数据放入缓存
String s = JSON.toJSONString(catalogJsonFromDB);
stringRedisTemplate.opsForValue().set("catalogJSON", s);
// 返回查询数据库查到的数据
return catalogJsonFromDB;
}
// 将缓存中查询出的json转换为指定的对象
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
// 从数据库查询并封装分类数据
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
// 查询出表pms_category所有的记录实体
List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
// 查出所有的一次分类
List<CategoryEntity> level_1_categorys = getParent_cid(categoryEntityList, 0L);
// 封装数据,构造一个以1级id为键,2级分类列表为值的map
Map<String, List<Catelog2Vo>> collect = level_1_categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
// 根据一级分类id查找二级分类
List<CategoryEntity> level_2_categorys = getParent_cid(categoryEntityList, l1.getCatId());
// 封装结果为Catelog2Vo的集合
List<Catelog2Vo> catelog2Vos = null;
if (level_2_categorys != null) {
// 把 level_2_categorys 封装为 catelog2Vos
catelog2Vos = level_2_categorys.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
// 根据二级分类id查找三级分类
List<CategoryEntity> level_3_categorys = getParent_cid(categoryEntityList, l2.getCatId());
// 将 level_3_categorys 封装为 catelog3Vos
if (level_3_categorys != null) {
List<Catelog2Vo.Catelog3Vo> catelog3Vos = level_3_categorys.stream().map(l3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(catelog3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return collect;
}
重启服务,访问http://localhost:10000/index/catalog.json测试
5 压力测试内存溢出
云服务器的吞吐量低是网络带宽原因。
问题原因:
- SpringBoot2.0以后默认使用Lettuce作为操作redis的客户端。它使用netty进行通信
- Lettuce的bug导致netty堆外内存溢出
- netty如果没有指定堆外内存,会默认使用服务配置里的 -Xmx 作为堆外内存的大小
- 可以通过-Dio.netty.maxDirectMemory进行设置
解决方法:
- 升级 Lettuce客户端
- 切换使用 jedis客户端
5.1 切换为jedis
1、排除Lettuce依赖包
2、导入jedis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
3、重启服务测试
坑:切换为jedis客户端后压力测试时报如下异常nested exception is redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out] with root cause
问题原因:
我的redis在阿里云服务器上,压力测试时可能由于网速太慢的问题,redis连接可能超时了
解决方法:
配置redis的超时时间
redis:
timeout: 100000
5.2 redisTemplate和Lettuce的关系
redisTemplate:spring对Lettuce、jedis的再次封装
Lettuce、jedis:操作redis的底层客户端
6 缓存失效问题
6.1 缓存穿透
6.2 缓存雪崩
6.3 缓存击穿
7 解决缓存击穿
7.1 分布式锁
7.2 锁时序图
7.3 本地锁查询数据库
给getCatalogJsonFromDB
方法加锁,
将getCatalogJson
方法中添加缓存的代码转移到getCatalogJsonFromDB
里
改进后的getCatalogJson
方法
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 给缓存中放JSON字符串,拿出的JSON字符串,还需要逆转为能用的对象类型
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.hasText(catalogJSON)) {
// 缓存中没有,查询数据库
return getCatalogJsonFromDB();
}
// 将缓存中查询出的json转换为指定的对象
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
改进后的getCatalogJsonFromDB
方法
// 从数据库查询并封装分类数据
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
synchronized (this) {
// 得到锁以后,在去缓存中确定一次
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if(!StringUtils.isEmpty(catalogJSON)) {
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
// 查询出表pms_category所有的记录实体
List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
// 查出所有的一次分类
List<CategoryEntity> level_1_categorys = getParent_cid(categoryEntityList, 0L);
// 封装数据,构造一个以1级id为键,2级分类列表为值的map
Map<String, List<Catelog2Vo>> collect = level_1_categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
// 根据一级分类id查找二级分类
List<CategoryEntity> level_2_categorys = getParent_cid(categoryEntityList, l1.getCatId());
// 封装结果为Catelog2Vo的集合
List<Catelog2Vo> catelog2Vos = null;
if (level_2_categorys != null) {
// 把 level_2_categorys 封装为 catelog2Vos
catelog2Vos = level_2_categorys.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
// 根据二级分类id查找三级分类
List<CategoryEntity> level_3_categorys = getParent_cid(categoryEntityList, l2.getCatId());
// 将 level_3_categorys 封装为 catelog3Vos
if (level_3_categorys != null) {
List<Catelog2Vo.Catelog3Vo> catelog3Vos = level_3_categorys.stream().map(l3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(catelog3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
// 将查到的数据放入缓存
String s = JSON.toJSONString(collect);
stringRedisTemplate.opsForValue().set("catalogJSON", s);
return collect;
}
}