(9)SprintBoot 2.X 页面优化技术(页面缓存+对象缓存)
1.页面优化技术
1.1 页面缓存+URL缓存+对象缓存
- 由于并发瓶颈在数据库,加入缓存可以使得减少对数据库的访问。
1.2 页面静态化,前后端分离
- 后端向前端传的只是数据如(GoodDetailVo),前端获取数据之后才在浏览器进行渲染,而不是由服务器进行渲染
- 页面静态化的主要目的是为了加快页面的加载速度,将商品的详情和订单详情页面做成静态HTML(纯的HTML),数据的加载只需要通过ajax来请求服务器,并且做了静态化HTML页面可以缓存在客户端的浏览器
1.3 静态资源优化
- JS/CSS压缩,减少流量。
- JS/CSS组合,减少连接数。
1.4 CDN优化
- 内容分发网络,就近访问
3.缓存问题
3.1 缓存穿透
-
指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。
-
解决方案:
- 对这些不存在的数据缓存一个空数据;
- 对这类请求进行过滤。
3.2 缓存雪崩
- 指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。
- 在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。
- 解决方案:
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。
3.3 缓存一致性
- 缓存一致性要求数据更新的同时缓存数据也能够实时更新
- 解决方案:
- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。
- 要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据
3.4 缓存 “无底洞” 现象
- 指的是为了满足业务要求添加了大量缓存节点,但是性能不但没有好转反而下降了的现象
- 产生原因 :缓存系统通常采用 hash 函数将 key 映射到对应的缓存节点,随着缓存节点数目的增加,键值分布到更多的节点上,导致客户端一次批量操作会涉及多次网络操作,这意味着批量操作的耗时会随着节点数目的增加而不断增大。此外,网络连接数变多,对节点的性能也有一定影响。
- 解决方案:
- 优化批量数据操作命令;
- 减少网络通信次数;
- 降低接入成本,使用长连接 / 连接池,NIO 等。
4.项目应用
4.1 页面缓存
- 通过在手动渲染得到的html页面缓存到redis(如to_list)
- 页面缓存和URL缓存时间比较短,适合场景:变化不大的页面。如果分页,不会全部缓存,一般缓存前一两页。可以防止短时间大并发访问。
- 过程:
- 取缓存
- 手动渲染模板
- 结果输出
- 代码
@RequestMapping(value = "/to_list", produces = "text/html")
@ResponseBody
public String list(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user) {
//取缓存
String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("user", user);
model.addAttribute("goodsList", goodsList);
//手动渲染
IWebContext ctx =new WebContext(request,response,
request.getServletContext(),request.getLocale(),model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
if (!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
//结果输出
return html;
}
4.2 URL缓存
- 与页面缓存实质一样,不过加入了商品id
@RequestMapping(value = "/to_detail/{goodsId}", produces = "text/html")
@ResponseBody
public String detail2(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user, @PathVariable("goodsId") long goodsId) {
model.addAttribute("user", user);
//取缓存
String html = redisService.get(GoodsKey.getGoodsDetail, "" + goodsId, String.class);
if (!StringUtils.isEmpty(html)) {
return html;
}
//根据id查询商品详情
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
model.addAttribute("goods", goods);
long startTime = goods.getStartDate().getTime();
long endTime = goods.getEndDate().getTime();
long now = System.currentTimeMillis();
int miaoshaStatus = 0;
int remainSeconds = 0;
if (now < startTime) {//秒杀还没开始,倒计时
miaoshaStatus = 0;
remainSeconds = (int) ((startTime - now) / 1000);
} else if (now > endTime) {//秒杀已经结束
miaoshaStatus = 2;
remainSeconds = -1;
} else {//秒杀进行中
miaoshaStatus = 1;
remainSeconds = 0;
}
model.addAttribute("miaoshaStatus",miaoshaStatus);
model.addAttribute("remainSeconds",remainSeconds);
//手动渲染
IWebContext ctx =new WebContext(request,response,
request.getServletContext(),request.getLocale(),model.asMap());
html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", ctx);
if (!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsDetail, "" + goodsId, html);
}
return html;
}
}
4.3 对象缓存
- 包括对用户信息、商品信息、订单信息和token等数据进行缓存,利用缓存来减少对数据库的访问,大大加快查询速度。
public MiaoshaUser getById(long id){
//对象缓存
MiaoshaUser user = redisService.get(MiaoshaUserKey.getById, "" + id, MiaoshaUser.class);
if (user != null) {
return user;
}
//取数据库
user = miaoShaUserDao.getById(id);
//再存入缓存
if (user != null) {
redisService.set(MiaoshaUserKey.getById, "" + id, user);
}
return user;
}
4.4 典型缓存同步场景:更新密码
- 更新数据库与缓存,一定保证数据一致性,对对象更新时,同时也要对缓存进行失效操作(项目中token是个例外,因为token失效后将无法执行后续操作)
/**
* 典型缓存同步场景:更新密码
*/
public boolean updatePassword(String token, long id, String formPass) {
//取user
MiaoshaUser user = getById(id);
if(user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//更新数据库
MiaoshaUser toBeUpdate = new MiaoshaUser();
toBeUpdate.setId(id);
toBeUpdate.setPassword(MD5Util.formPassToDBPass(formPass, user.getSalt()));
miaoShaUserDao.update(toBeUpdate);
//更新缓存:先删除再插入
redisService.delete(MiaoshaUserKey.getById, ""+id);
user.setPassword(toBeUpdate.getPassword());
redisService.set(MiaoshaUserKey.token, token, user);
return true;
}
5. 缓存与数据库的一致性思考,转自https://www.cnblogs.com/johnsblog/p/6426287.html
为什么不能先处理缓存,再更新数据库呢?
为什么你的缓存更新策略是先更新数据库后删除缓存,讲讲其他的情况有什么问题?
问题:怎么保持缓存与数据库一致?
要解答这个问题,我们首先来看不一致的几种情况。我将不一致分为三种情况
- 数据库有数据,缓存没有数据;
- 数据库有数据,缓存也有数据,数据不相等;
- 数据库没有数据,缓存有数据。
- 策略:
- 首先尝试从缓存读取,读到数据则直接返回;如果读不到,就读数据库,并将数据会写到缓存,并返回。
- 需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)。
- 解决方案大概有以下几种:
-
对删除缓存进行重试,数据的一致性要求越高,越是重试得快。
-
定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。
-
给所有的缓存一个失效期。
- 第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。