一、页面缓存
页面缓存是应对高并发的一个比较常见的方案,当请求页面的时候,会先查询Redis缓存中是否存在,若存在则直接从缓存中返回页面,否则会通过代码逻辑去渲染页面,并将渲染后的页面缓存到Redis中,然后返回。
因此在秒杀系统中,将秒杀商品详情页面存入Redis,将大大提高并发访问量。页面缓存逻辑如下:
详细代码如下:
@ApiOperation("商品详情")
@RequestMapping(value = "/detail/{goodsId}", produces="text/html;charset=utf-8")
@ResponseBody
public String toDetail(TUser user, @PathVariable Long goodsId) {
ValueOperations valueOperations = redisTemplate.opsForValue();
//首先就去缓存查找页面
String html = (String) valueOperations.get("goodsDetail:" + goodsId);
//若缓存存在从Redis直接返回
if (!StringUtils.isEmpty(html)) {
return html;
}
model.addAttribute("user", user);
GoodsVo goodsVo = itGoodsService.findGoodsVobyGoodsId(goodsId);
Date startDate = goodsVo.getStartDate();
Date endDate = goodsVo.getEndDate();
Date nowDate = new Date();
//秒杀状态
int seckillStatus = 0;
//秒杀倒计时
int remainSeconds = 0;
if (nowDate.before(startDate)) {
//秒杀还未开始0
remainSeconds = (int) ((startDate.getTime() - nowDate.getTime()) / 1000);
} else if (nowDate.after(endDate)) {
//秒杀已经结束
seckillStatus = 2;
remainSeconds = -1;
} else {
//秒杀进行中
seckillStatus = 1;
remainSeconds = 0;
}
model.addAttribute("remainSeconds", remainSeconds);
model.addAttribute("goods", goodsVo);
model.addAttribute("seckillStatus", seckillStatus);
//若不存在,手动渲染之后存入Redis
WebContext webContext = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goodsDetail", webContext);
if (!StringUtils.isEmpty(html)) {
//页面缓存,设置过期时间60秒
valueOperations.set("goodsDetail:" + goodsId, html, 60, TimeUnit.SECONDS);
}
return html;
}
不过,缓存整个页面的数据量也是非常大的,页面多了非常占用内存,因此仍有待改进。
二、对象缓存结合页面静态化
众所周知,模板的诞生是为了将显示与数据分离,模板技术多种多样,但其本质是将模板文件和数据通过模板引擎生成最终的 HTML 代码。Thymeleaf 亦是如此。
模板文件是静态的,数据是动态的。因此我们可以将模板文件用Nginx代理,即之前提到的动静分离;动态的数据存入Redis缓存中,可以减少数据库的访问次数。
结合到我们的秒杀项目上来,我们可以做到如下几点用Redis缓存来优化我们的项目:
1、缓存预热:通过一个定时任务,每天凌晨三点将未来三天即将进行的秒杀活动信息以及对应的商品信息存入Redis。
2、热点数据对象缓存:例如商城的商品三级分类信息,每一次用户进入商城首页都会访问到的数据,将其存入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;
}
由下图可见,开启缓存之后吞吐量有了成倍的提升。