为什么要使用页面缓存技术?
系统都是逐渐演进的,一个系统在运行中必须是根据场景逐渐地提高优化性能。高并发就是对资源的节约的考验,这种考验除了更换优秀和先进的技术,优化架构,还在于从小处出发,对尽可能节约的资源进行节约。而在一个系统的数据访问中,系统的瓶颈往往是来自于数据库,因此我们要尽可能减少对数据库的访问!在不影响用户体验的情况下,对于一些静态或者变化不大的页面,我们使用缓存来减少对数据库的访问!
缓存技术的原理
在一个请求中,逻辑越复杂,调用,依赖,访问数据库的越多,耗时也就越长,响应时间也就越长,性能也就越差!因此降低逻辑复杂度,减低耦合,提高内聚,减少数据库访问,将频繁用到的变化不大的数据给缓存起来也就成为了提高性能的主要核心。
缓存雪崩-数据穿透问题
缓存穿透
缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。
想象一下这个情况,如果传入的参数为-1,会是怎么样?这个-1,就是一定不存在的对象。就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。即便是采用UUID,也是很容易找到一个不存在的KEY,进行攻击。
缓存雪崩
缓存雪崩是指因为数据未加载到缓存中,或者缓存同一时间大面积的失效,在某一时刻大量的缓存没有命中,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机.
解决方法:做电商项目的时候,一般是采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。
缓存击穿
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决方法:对主打商品都是早早的做好了准备,让缓存永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。
下面是一个我在开发秒杀项目的时候,一些优化的地方,例如在进入商品列表的时候,我是直接采用Thymeleaf模板动态渲染了,这样的处理效率不高,不适合高并发,所以采用页面缓存进行优化,将页面的源代码缓存到Redis中,这样可以减少对数据库的访问,从而减少对数据库访问的耗时。
如下代码是进入商品列表的源代码,只是简单的将数据放进model里,然后返回页面,但是每次加载的时候,都会进行一次数据库访问,如果在是高并发的情况下,会加剧数据库的访问,造成加载时间变慢,甚至导致数据库宕机等情况发生,所以我们需要采用一些缓存技术进行优化。
@RequestMapping("to_list")
public String toList(Model model,MiaoshaUser user){
if(user == null)
return "login";
model.addAttribute("user",user);
List<GoodsVo> goodsVoList = goodsService.getGoodsVoList();
model.addAttribute("goodsList",goodsVoList);
return "goods_list";
}
如下代码是经过了页面缓存优化后的代码,每次进入方法前,先取缓存,看看缓存中是否含有之前缓存的html源码,如果有,则返回。如果没有,则进行数据库访问等操作,然后用thymeleafViewResolver
进行手动渲染,将渲染后的结果放进html
中,然后放进缓存,再返回给浏览器解析。
注意:缓存的有效时间不宜过长,也不宜过短,本文中的优化,我是设置了60s的缓存有效时间,用户看到60s前的页面,也还是正常的。
@RequestMapping(value ="/to_list",produces = "text/html")
@ResponseBody
public String toList(HttpServletRequest request,HttpServletResponse response, Model model, MiaoshaUser user){
model.addAttribute("user",user);
//取缓存
String html = redisService.get(GoodsKey.getGoodsList,"",String.class);
if (!StringUtils.isEmpty(html)){
return html;
}
//查询商品列表
List<GoodsVo> goodsList = goodsService.listGoodsVo();
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;
}
上面的代码有一处地方需要注意一下,有些同学用的WebContext
是SpringWebContext
由于我采用的是thymeleaf.spring5
的版本,大部分API被移到了IWebContext
下面。可能会有些小差别,在此说明一下。