秒杀功能(4)缓存技术

从这一节开始就是讲如何优化秒杀的功能了。第一步考虑的是页面优化技术

页面优化技术有:
  1. 页面缓存 + URL缓存 + 对象缓存
  2. 页面静态化,前后端分离
  3. 静态资源优化
  4. CDN优化

这篇文章先讲第一项:页面缓存 + URL + 对象缓存 具体如何实现。

页面缓存

在controller层的GoodsController中以获取商品列表的list方法举例。将原本springboot自动渲染页面改成手动渲染,把页面放在缓存里。

  • 修改前的代码为:
	@RequestMapping("/to_list")
    public String list(Model model, HttpServletResponse response,
                       @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String cookieToken,
                       @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String paramToken) {
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return "login";
        }
        String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
        MiaoshaUser user = userService.getByToken(response,token);//从token中读用户信息
        model.addAttribute("user", user); //将user对象和goods_list.html页面中的user“关联”
        //查询商品列表
        List<GoodsVo> goodsList = goodsService.listGoodsVo();
        model.addAttribute("goodsList", goodsList);
        return "goods_list"; //返回goods_list.html
    }

大致步骤是:先通过缓存中的token获取用户;再从数据库中获取商品列表;最后渲染页面。

  • 修改后的代码为:
	@RequestMapping(value = "/to_list", produces = "text/html")
    @ResponseBody
    public String list(Model model, HttpServletResponse response, HttpServletRequest request,
                       @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN, required = false) String cookieToken,
                       @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN, required = false) String paramToken) {
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return "login";
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        MiaoshaUser user = userService.getByToken(response, token);//从token中读用户信息
        model.addAttribute("user", user); //将user对象和goods_list.html页面中的user“关联”
        //取缓存
        String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
        if (!StringUtils.isEmpty(html)) {
            return html;
        }

        //查询商品列表
        List<GoodsVo> goodsList = goodsService.listGoodsVo();
        model.addAttribute("goodsList", goodsList);
        //  return "goods_list"; //返回goods_list.html,由springBoot渲染

        //手动渲染
        WebContext ctx = new WebContext(request, response,
                request.getServletContext(), request.getLocale(), model.asMap());
        html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
        if (!StringUtils.isEmpty(html)) { //如果html不为空,先存入redis中
            redisService.set(GoodsKey.getGoodsList, "", html);
        }
        return html;
    }

大致步骤是:先通过缓存中的token获取用户;再从缓存中取页面返回;如果缓存中没有页面,则再去数据库中查询并添加到缓存中去,然后手动渲染页面。这里在方法体上需要加@ResponseBody。

注意:这里在缓存中存放页面的有效期不能太长,因为如果页面修改了但是缓存中没有修改会造成页面不一致,所以应该把页面设置成合理的有效期,我这里设置60s,认为用户看到1min前的页面是可以接受的。

URL缓存

个人认为URL缓存和页面缓存差别不大,可能是URL缓存的细粒度更小。这里在controller层的GoodsController中以获取商品详情列表的list方法举例。

  • 修改前的代码:
	@RequestMapping("/to_detail/{goodsId}")
    public String detail(Model model, @PathVariable("goodsId") long goodsId, HttpServletResponse response,
                         @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String cookieToken,
                         @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String paramToken) {
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return "login";
        }
        String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
        MiaoshaUser user = userService.getByToken(response,token);//从token中读用户信息
        model.addAttribute("user", user);

        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        model.addAttribute("goods", goods);

        long startAt = goods.getStartDate().getTime();//转换成毫秒值
        long endAt = goods.getEndDate().getTime();
        long now = System.currentTimeMillis();

        int miaoshaStatus = 0;
        int remainSeconds = 0;
        if (now < startAt) {//秒杀还没开始,倒计时
            miaoshaStatus = 0;
            remainSeconds = (int) ((startAt - now) / 1000);
        } else if (now > endAt) {//秒杀已经结束
            miaoshaStatus = 2;
            remainSeconds = -1;
        } else {//秒杀进行中
            miaoshaStatus = 1;
            remainSeconds = 0;
        }
        model.addAttribute("miaoshaStatus", miaoshaStatus);
        model.addAttribute("remainSeconds", remainSeconds);
        return "goods_detail";
    }

其大致步骤为:先通过缓存中的token获取用户;再从数据库中通过商品id获取商品的详情;最后渲染商品详情页面。

  • 修改后的代码
	@RequestMapping(value = "/to_detail/{goodsId}", produces = "text/html")
    @ResponseBody
    public String detail(Model model, @PathVariable("goodsId") long goodsId, HttpServletResponse response, HttpServletRequest request,
                         @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN, required = false) String cookieToken,
                         @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN, required = false) String paramToken) {
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return "login";
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        MiaoshaUser user = userService.getByToken(response, token);//从token中读用户信息
        model.addAttribute("user", user);

        //取缓存
        String html = redisService.get(GoodsKey.getGoodsDetail, "" + goodsId, String.class);
        if (!StringUtils.isEmpty(html)) {
            return html;
        }
        //手动渲染
        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        model.addAttribute("goods", goods);

        long startAt = goods.getStartDate().getTime();//转换成毫秒值
        long endAt = goods.getEndDate().getTime();
        long now = System.currentTimeMillis();

        int miaoshaStatus = 0;
        int remainSeconds = 0;
        if (now < startAt) {//秒杀还没开始,倒计时
            miaoshaStatus = 0;
            remainSeconds = (int) ((startAt - now) / 1000);
        } else if (now > endAt) {//秒杀已经结束
            miaoshaStatus = 2;
            remainSeconds = -1;
        } else {//秒杀进行中
            miaoshaStatus = 1;
            remainSeconds = 0;
        }
        model.addAttribute("miaoshaStatus", miaoshaStatus);
        model.addAttribute("remainSeconds", remainSeconds);
        //return "goods_detail";

        WebContext 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;
    }

大致步骤为:先通过缓存中的token获取用户;再从缓存中取页面返回;若缓存中没有则需要从数据库中取,并添加到缓存中去。

对象缓存

关于对象缓存,是一种细粒度更小的缓存操作。在实际项目中, 不会大规模使用页面缓存,因为涉及到分页,一般只缓存前面1-2页。

对象缓存举例:需要用到用户信息时,可以从缓存中取出。比如已知用户id获取用户信息,本来是从数据库中取,可以到缓存中取。
另外,在redis中存入token和user对也是对象缓存。

给一个对象缓存的例子:更新用户密码

	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;
    }

	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);
        //处理缓存,对象缓存一定要更新redis,否则会发生数据不一致的情况
        //这也说明了在service中调用其它的对象动作要调用service,不要调用别人的dao,
        // 因为别人的service可能有缓存,而别人的dao是直接在数据库中操作
        redisService.delete(MiaoshaUserKey.getById, "" + id);
        user.setPassword(toBeUpdate.getPassword());
        redisService.set(MiaoshaUserKey.token, token, user);
        return true;
    }

注:更新缓存后保证数据库中数据和缓存数据一致性。 一般可以选择淘汰缓存和更新缓存。

  1. 淘汰:性价比高,仅仅会涉及一次未命中,再从数据库取
  2. 更新:操作复杂,涉及到对应的业务逻辑。

若一定要更新,比如登陆状态下更新密码,要先更新数据库,再删除缓存、更新缓存。

最好先淘汰缓存,再更新数据库信息。(若先更新在淘汰,淘汰失败缓存中有脏数据)

JMeter压测

对商品列表页面goods_list进行压测,实际上只有一个优化:页面缓存。观察其TPS是否有提高。

先测一组优化前的数据:5000个线程;10次循环。
结果为:
在这里插入图片描述
因为需要从数据库中取数据,运行的特别慢。TPS:245。

优化后的结果为:
在这里插入图片描述
运行速度比刚才快多了,TPS:989.2。
页面缓存的作用是很明显的。

缓存分类

缓存技术是运用比较广泛的技术,它有很多种类型,从前到后整理:

  1. 从用户发起请求开始,可以做页面的静态化,把页面缓存在用户的浏览器端;(在后面秒杀功能(5)中实现)
  2. 在请求到达服务器之前,可以部署CDN节点,让请求首先访问CDN;
  3. 请求到达网站时,也可以加缓存,比如Nginx也可以加缓存;
  4. 在应用程序加页面缓存,存到redis中(本篇实现的);
  5. 更细粒度的对象缓存(本篇实现);
  6. 经历上述这些后才会到达数据库,逐步削减到达数据库的压力。需要注意的是用了缓存之后会引入数据不一致的问题,所以要做平衡。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值