业务分析
对于访问商品首页的时候需要去后台去访问商品的分类数据,是一个树形结构的json,这种数据我们把它加入缓存中提升页面的加载速度
导入依赖
gulimall-redis模块
这里我建议抽离出一个redis的模块,或者将redis的依赖导出common模块中,后期可以为其他服务调用复用,并且可以使用工具类封装redis的一些常用操作
pom.xml
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
gulimall-product
application.yml
spring:
redis:
host: 192.168.163.131
port: 6379
核心代码部分
springboot对redis的依赖整合了starter,可以通过封装了的
RedisTemplate 去操作redis
这里我们使用StringRedisTemplate操作redis
@Autowired
StringRedisTemplate redisTemplate;
@Override
public Map<String, List<Catalogs2Vo>> getCatalogJson() {
// 1.从缓存中读取分类信息
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
// 2. 缓存中没有,查询数据库
Map<String, List<Catalogs2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
// 3. 查询到的数据存放到缓存中,将对象转成 JSON 存储
redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catalogJsonFromDB));
return catalogJsonFromDB;
}
return JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalogs2Vo>>>(){});
}
/**
* 加缓存前,只读取数据库的操作
*
* @return
*/
public Map<String, List<Catalogs2Vo>> getCatalogJsonFromDB() {
System.out.println("查询了数据库");
// 性能优化:将数据库的多次查询变为一次
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
//1、查出所有分类
//1、1)查出所有一级分类
List<CategoryEntity> level1Categories = getParentCid(selectList, 0L);
//封装数据
Map<String, List<Catalogs2Vo>> parentCid = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParentCid(selectList, v.getCatId());
//2、封装上面的结果
List<Catalogs2Vo> catalogs2Vos = null;
if (categoryEntities != null) {
catalogs2Vos = categoryEntities.stream().map(l2 -> {
Catalogs2Vo catalogs2Vo = new Catalogs2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());
//1、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = getParentCid(selectList, l2.getCatId());
if (level3Catelog != null) {
List<Catalogs2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
//2、封装成指定格式
Catalogs2Vo.Category3Vo category3Vo = new Catalogs2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return category3Vo;
}).collect(Collectors.toList());
catalogs2Vo.setCatalog3List(category3Vos);
}
return catalogs2Vo;
}).collect(Collectors.toList());
}
return catalogs2Vos;
}));
return parentCid;
}
错误分析
这里可能会产生堆外内存溢出异常:OutOfDirectMemoryError。
下面进行分析:
SpringBoot 2.0 以后默认使用 lettuce 作为操作 redis 的客户端,它使用 netty 进行网络通信;
lettuce 的 bug 导致 netty 堆外内存溢出;
netty 如果没有指定堆外内存,默认使用 -Xmx 参数指定的内存;
可以通过 -Dio.netty.maxDirectMemory 进行设置;
这里是lettuce 的bug,连接池问题,资源没有得到很好的释放,达到正向资源循环
解决方案:不能只使用 -Dio.netty.maxDirectMemory 去调大堆外内存,这样只会延缓异常出现的时间。
升级 lettuce 客户端,或使用 jedis 客户端
通过修改
gulimall-redis
pom.xml
<!-- redis -->
<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>
使用jedis代替lettuce 客户端
并发下的问题
在大并发下,首次读取缓存不存在的情况,可能出现大量访问被定向到数据库,且存在缓存有效期失效时,缓存失效带来的一系列问题
分布式锁的redis的实现
上面的情况我们可以通过redis实现的分布式锁实现
redis分布式锁的原理:setnx,同一时刻只能设置成功一个
前提,锁的key是一定的,value可以变
没获取到锁阻塞或者sleep一会
设置好了锁,玩意服务出现宕机,没有执行删除锁逻辑,这就造成了死锁
解决:设置过期时间
业务还没执行完锁就过期了,别人拿到锁,自己执行完去删了别人的锁
解决:锁续期(redisson有看门狗),。删锁的时候明确是自己的锁。如uuid
判断uuid对了,但是将要删除的时候锁过期了,别人设置了新值,那删除了别人的锁
解决:删除锁必须保证原子性(保证判断和删锁是原子的)。使用redis+Lua脚本完成,脚本是原子的
代码变动
/**
* 从数据库查询并封装数据::分布式锁
*
* @return
*/
public Map<String, List<Catalogs2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、占分布式锁。去redis占坑 设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
Map<String, List<Catalogs2Vo>> dataFromDb = null;
try {
//加锁成功...执行业务
dataFromDb = getCatalogJsonFromDB();
} finally {
// lua 脚本解锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 删除锁
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList("lock"), uuid);
}
//先去redis查询下保证当前的锁是自己的
//获取值对比,对比成功删除=原子性 lua脚本解锁
// String lockValue = stringRedisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue)) {
// //删除我自己的锁
// stringRedisTemplate.delete("lock");
// }
return dataFromDb;
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...重试机制
//休眠一百毫秒
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
}
}
其中加锁和删除锁的操作一定保证原子性,我们是通过
setIfAbsent
封装的是setnx的lua脚本
lua脚本具有原子性的操作
实现了分布式锁的作用,解决了缓存失效的问题