【SpringBoot商城秒杀系统项目实战17】页面优化技术(页面缓存+URL缓存+对象缓存)
页面优化技术
- 页面缓存+URL缓存+对象缓存
由于并发瓶颈在数据库,想办法如何减少对数据库的访问,所以加若干缓存来提高,通过各种粒度的缓存,最大粒度页面缓存到最小粒度的对象级缓存。 - 页面静态化,前后端分离
都是纯的html,通过js或者ajax来请求服务器,如果做了静态化,浏览器可以把html缓存在客户端。 - 静态资源优化
JS/CSS压缩,减少流量。(压缩版的js,去掉多余的空格字符。区别于阅读版)
JS/CSS组合,减少连接数。(将多个JS和CSS的组合到一个请求里面去,一下子从服务端全部下载下来) - CDN优化
内容分发网络,就近访问。
缓存特征:
命中率
当某个请求能够通过访问缓存而得到响应时,称为缓存命中。
缓存命中率越高,缓存的利用率也就越高。
最大空间
缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。
当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据。
淘汰策略
FIFO(First In First Out):先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使得最先进入的数据(最晚的数据)被淘汰。
LRU(Least Recently Used):最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最久的数据。该策略可以保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率。
LFU(Least Frequently Used):最不经常使用策略,优先淘汰一段时间内使用次数最少的数据。
一般,页面缓存和URL缓存时间比较短,适合场景:变化不大的页面。如果分页,不会全部缓存,一般缓存前一两页。
首先介绍:页面缓存+URL缓存+对象缓存
页面缓存
1.取缓存 (缓存里面存的是html)
2.手动渲染模板
3.结果输出(直接输出html代码)
GoodsController里面的toListCache方法改造一下
/** * 做页面缓存的list页面,防止同一时间访问量巨大到达数据库,如果缓存时间过长,数据及时性就不高。 */ //5-17 @RequestMapping(value="/to_list",produces="text/html") @ResponseBody public String toListCache(Model model,MiaoshaUser user,HttpServletRequest request, HttpServletResponse response) { // 1.取缓存 // public <T> T get(KeyPrefix prefix,String key,Class<T> data) String html = redisService.get(GoodsKey.getGoodsList, "", String.class); if (!StringUtils.isEmpty(html)) { return html; } model.addAttribute("user", user); //1.查询商品列表 List<GoodsVo> goodsList= goodsService.getGoodsVoList(); model.addAttribute("goodsList", goodsList); //2.手动渲染 使用模板引擎 templateName:模板名称 String templateName="goods_list"; SpringWebContext context=new SpringWebContext(request,response,request.getServletContext(), request.getLocale(),model.asMap(),applicationContext); html=thymeleafViewResolver.getTemplateEngine().process("goods_list", context); //保存至缓存 if(!StringUtils.isEmpty(html)) { redisService.set(GoodsKey.getGoodsList, "", html);//key---GoodsKey:gl---缓存goodslist这个页面 } return html; //return "goods_list";//返回页面login }
当访问goods_list页面的时候,如果从缓存中取到就返回这个html,(这里方法的返回格式已经设置为text/html,这样就是返回html的源代码),如果取不到,利用ThymeleafViewResolver的getTemplateEngine().process和我们获取到的数据,渲染模板,并且在返回到前端之前保存至缓存里面,然后之后再来获取的时候,只要缓存里面存的goods_list页面的html还没有过期,那么直接返回给前端即可。
一般这个页面缓存时间,也不会很长,防止数据的时效性很低。但是可以防止短时间大并发访问。
GoodsKey :作为页面缓存的缓存Key的前缀,缓存有效时间,一般设置为1分钟
public class GoodsKey extends BasePrefix{ //考虑页面缓存有效期比较短 public GoodsKey(int expireSeconds,String prefix) { super(expireSeconds,prefix); } //goods_list页面 1分钟 public static GoodsKey getGoodsList=new GoodsKey(60,"gl"); //goods_detail页面 1分钟 public static GoodsKey getGoodsDetail=new GoodsKey(60,"gd"); //秒杀的商品的数量stock,0不失效 public static GoodsKey getMiaoshaGoodsStock=new GoodsKey(0,"gs"); }
URL缓存
这里的url缓存相当于页面缓存,针对不同的详情页显示不同缓存页面,对不同的url进行缓存(redisService.set(GoodsKey.getGoodsDetail, “”+goodsId, html),与页面缓存实质一样。
/** * 做了页面缓存的to_detail商品详情页。 * 做了页面缓存 URL缓存 ""+goodsId 不同的url进行缓存redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html); * @param model * @param user * @param goodsId * @return */ @RequestMapping(value="/to_detail_html/{goodsId}") //produces="text/html" @ResponseBody public String toDetailCachehtml(Model model,MiaoshaUser user, HttpServletRequest request,HttpServletResponse response,@PathVariable("goodsId")long goodsId) {//id一般用snowflake算法 // 1.取缓存 // public <T> T get(KeyPrefix prefix,String key,Class<T> data) String html = redisService.get(GoodsKey.getGoodsDetail, ""+goodsId, String.class);//不同商品页面不同的详情 if (!StringUtils.isEmpty(html)) { return html; } //缓存中没有,则将业务数据取出,放到缓存中去。 model.addAttribute("user", user); GoodsVo goods=goodsService.getGoodsVoByGoodsId(goodsId); model.addAttribute("goods", goods); //既然是秒杀,还要传入秒杀开始时间,结束时间等信息 long start=goods.getStartDate().getTime(); long end=goods.getEndDate().getTime(); long now=System.currentTimeMillis(); //秒杀状态量 int status=0; //开始时间倒计时 int remailSeconds=0; //查看当前秒杀状态 if(now<start) {//秒杀还未开始,--->倒计时 status=0; remailSeconds=(int) ((start-now)/1000); //毫秒转为秒 }else if(now>end){ //秒杀已经结束 status=2; remailSeconds=-1; //毫秒转为秒 }else {//秒杀正在进行 status=1; remailSeconds=0; //毫秒转为秒 } model.addAttribute("status", status); model.addAttribute("remailSeconds", remailSeconds); // 2.手动渲染 使用模板引擎 templateName:模板名称 String templateName="goods_detail"; SpringWebContext context = new SpringWebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap(), applicationContext); html = thymeleafViewResolver.getTemplateEngine().process("goods_detail", context); // 将渲染好的html保存至缓存 if (!StringUtils.isEmpty(html)) { redisService.set(GoodsKey.getGoodsDetail, ""+goodsId, html); } return html;//html是已经渲染好的html文件 //return "goods_detail";//返回页面login }
对象缓存
相比页面缓存是更细粒度缓存。在实际项目中, 不会大规模使用页面缓存,对象缓存就是当用到用户数据的时候,可以从缓存中取出。比如:更新用户密码,根据token来获取用户缓存对象。
MiaoshaUserService里面增加getById方法,先去取缓存,如果缓存中拿不到,那么就去取数据库,然后再设置到缓存中去
/** * 根据id取得对象,先去缓存中取 * @param id * @return */ public MiaoshaUser getById(long id) { //1.取缓存 ---先根据id来取得缓存 MiaoshaUser user=redisService.get(MiaoshaUserKey.getById, ""+id, MiaoshaUser.class); //能再缓存中拿到 if(user!=null) { return user; } //2.缓存中拿不到,那么就去取数据库 user=miaoshaUserDao.getById(id); //3.设置缓存 if(user!=null) { redisService.set(MiaoshaUserKey.getById, ""+id, user); } return user; }
MiaoshaUserKey,这里我们认为对象缓存一般没有有效期,永久有效
public class MiaoshaUserKey extends BasePrefix{ public static final int TOKEN_EXPIRE=3600*24*2;//3600S*24*2 =2天 public MiaoshaUserKey(int expireSeconds,String prefix) { super(expireSeconds,prefix); } public static MiaoshaUserKey token=new MiaoshaUserKey(TOKEN_EXPIRE,"tk"); //对象缓存一般没有有效期,永久有效 public static MiaoshaUserKey getById=new MiaoshaUserKey(0,"id"); }
更新用户密码:更新数据库与缓存,一定保证数据一致性,修改token关联的对象以及id关联的对象,先更新数据库后删除缓存,不能直接删除token,删除之后就不能登录了,再将token以及对应的用户信息一起再写回缓存里面去。
/** * 注意数据修改时候,保持缓存与数据库的一致性 * 需要传入token * @param id * @return */ public boolean updatePassword(String token,long id,String passNew) { //1.取user对象,查看是否存在 MiaoshaUser user=getById(id); if(user==null) { throw new GlobalException(CodeMsg.MOBILE_NOTEXIST); } //2.更新密码 MiaoshaUser toupdateuser=new MiaoshaUser(); toupdateuser.setId(id); toupdateuser.setPwd(MD5Util.inputPassToDbPass(passNew, user.getSalt())); miaoshaUserDao.update(toupdateuser); //3.更新数据库与缓存,一定保证数据一致性,修改token关联的对象以及id关联的对象 redisService.delete(MiaoshaUserKey.getById, ""+id); //不能直接删除token,删除之后就不能登录了 user.setPwd(toupdateuser.getPwd()); redisService.set(MiaoshaUserKey.token, token,user); return true; }
RedisService里面的delete方法
public boolean delete(KeyPrefix prefix,String key){ Jedis jedis=null; try { jedis=jedisPool.getResource(); String realKey=prefix.getPrefix()+key; long ret=jedis.del(realKey); return ret>0;//删除成功,返回大于0 //return jedis.decr(realKey); }finally { returnToPool(jedis); } }
MiaoshaUserDao 代码:
@Mapper public interface MiaoshaUserDao { @Select("select * from miaosha_user where id=#{id}") //这里#{id}通过后面参数来为其赋值 public MiaoshaUser getById(@Param("id")long id); //绑定 //绑定在对象上面了----@Param("id")long id,@Param("pwd")long pwd 效果一致 @Update("update miaosha_user set pwd=#{pwd} where id=#{id}") public void update(MiaoshaUser toupdateuser); //public boolean update(@Param("id")long id); //绑定 }
思考
为什么不能先处理缓存,再更新数据库呢?
为什么你的缓存更新策略是先更新数据库后删除缓存,讲讲其他的情况有什么问题?
问题:怎么保持缓存与数据库一致?
要解答这个问题,我们首先来看不一致的几种情况。我将不一致分为三种情况
数据库有数据,缓存没有数据;
数据库有数据,缓存也有数据,数据不相等;
数据库没有数据,缓存有数据。
策略:
-
首先尝试从缓存读取,读到数据则直接返回;如果读不到,就读数据库,并将数据会写到缓存,并返回。
-
需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)。
读的逻辑大家都很容易理解,谈谈更新。如果不采取我提到的这种更新方法,你还能想到什么更新方法呢?大概会是:先删除缓存,然后再更新数据库。这么做引发的问题是,如果A,B两个线程同时要更新数据,并且A,B已经都做完了删除缓存这一步,接下来,A先更新了数据库,C线程读取数据,由于缓存没有,则查数据库,并把A更新的数据,写入了缓存,最后B更新数据库。那么缓存和数据库的值就不一致了。
另外有人会问,如果采用你提到的方法,为什么最后是把缓存的数据删掉,而不是把更新的数据写到缓存里。这么做引发的问题是,如果A,B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据也不一致。
我想出的解决方案大概有以下几种:
1. 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。
2. 定期全量更新,简单地说,就是我定期把后再全量加载。
3. 给所有的缓存一个失效期。
第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。
作者:小小少年Boy
链接:https://www.jianshu.com/p/8950c52ce53b
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
缓存问题
缓存穿透
指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。
解决方案:
- 对这些不存在的数据缓存一个空数据;
- 对这类请求进行过滤。
缓存雪崩
指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。
在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。
解决方案:
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。
缓存一致性
缓存一致性要求数据更新的同时缓存数据也能够实时更新。
解决方案:
- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。
- 要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。
缓存 “无底洞” 现象
指的是为了满足业务要求添加了大量缓存节点,但是性能不但没有好转反而下降了的现象。
产生原因:缓存系统通常采用 hash 函数将 key 映射到对应的缓存节点,随着缓存节点数目的增加,键值分布到更多的节点上,导致客户端一次批量操作会涉及多次网络操作,这意味着批量操作的耗时会随着节点数目的增加而不断增大。此外,网络连接数变多,对节点的性能也有一定影响。
解决方案:
- 优化批量数据操作命令;
- 减少网络通信次数;
- 降低接入成本,使用长连接 / 连接池,NIO 等。
转发至:https://github.com/CyC2018/CS-Notes/blob/master/notes/缓存.md
- 【SpringBoot商城秒杀系统项目实战18】页面优化技术-商品详情页面静态化(前后端分离)
- 【SpringBoot商城秒杀系统项目实战23】安全优化 数学图形验证码
- 【SpringBoot商城秒杀系统项目实战24】安全优化 接口限流防刷
- 【SpringBoot商城秒杀系统项目实战20】高并发秒杀接口优化
- 【SpringBoot商城秒杀系统项目实战22】安全优化 秒杀接口地址隐藏
- 【SpringBoot商城秒杀系统项目实战21】高并发秒杀系统接口优化 RabbitMQ异步下单
- 【SpringBoot商城秒杀系统项目实战19】秒杀静态化+订单详情静态化
- 【SpringBoot商城秒杀系统项目总结】 项目的亮点和难点及问题解决(源码地址)
- 某课网Spring Boot 2.0深度实践之核心技术项目实战篇
- 2019Java秒杀系统方案优化 高性能高并发项目实战
- 基于微服务的秒杀项目实战使用Spring Boot2.x Actuator监控应用
- 【SSH网上商城项目实战19】订单信息的级联入库以及页面的缓存问题
- 【SSH网上商城项目实战19】订单信息的级联入库以及页面的缓存问题
- Spring Boot 启动项目返回对象时 页面 spring boot type=Not Acceptable, status=406
- 【SSH网上商城项目实战19】订单信息的级联入库以及页面的缓存问题
- 基于微服务的秒杀项目实战使用Spring Boot2.x Actuator监控应用
- 【SSH网上商城项目实战19】订单信息的级联入库以及页面的缓存问题
- SSM实战——秒杀系统之Web层Restful url设计、SpringMVC整合、页面设计
- 优效学院 基于微服务的秒杀项目实战 Spring Boot 2.0基础篇01
- 10(maven+SSH)网上商城项目实战之页面缓存OSCache