Java秒杀系统优化(高性能高并发)

源码download:java秒杀系统 (resourcecode.cn)

主题:在大并发大流量的情况下如何提升吞吐量或者说QPS?

而秒杀活动恰恰就是属于大并发的情形,因此下面简单来谈谈大并发下秒杀方案的优化。

项目采用技术:SpringBoot + MyBatis + MySql + RabbitMq + Redis

RabbitMq安装参考:rabbitmq安装_Garry1115的博客-CSDN博客

文章首先说明优化思路方案和步骤,然后阐述代码具体实现,最后采用JMeter进行简单压测。

  • 针对大并发的主体优化常见有如下几种方式:

1.页面缓存 + URL缓存 + 对象级缓存

2.页面静态化(浏览器缓存),前后端分离+ajax

3.静态资源优化(js/css压缩,减少流量),多个js/css组合,减少连接数

4.CDN优化

而作为一个程序猿我们都知道并发最大的瓶颈基本就是数据库因此最好就是减少数据库的访问次数即加缓存。

  • 从访问开始到后端返回整个缓存链可以是:

浏览器端缓存(页面静态化)--> CDN --> nginx缓存 --> 后端服务缓存(页面缓存、对象级缓存等) --> 数据库

而作为一名后端工程师的优化则主要是针对接口优化,这也是我们的重头戏;

  • 接口优化大概通过如下步骤处理:

1.redis预减库存减少 数据库访问 

2.内存标记减少redis访问 

3.请求先入队缓冲,异步下单 ,增强用户体验

  • 针对秒杀接口优化:

核心思路:减少数据库访问(数据库瓶颈)

1.系统初始化,把商品库存数量加载到redis

2.收到请求,redis预减库存,库存不足,直接返回,否则进入3

3.请求入队,立即返回 排队中

4.请求出队,生成订单,减少库存

5.客户端轮询,是否秒杀成功

  • 针对安全方面优化:

1.秒杀接口地址隐藏

2.数学公式验证码

3.接口限流防刷

  • 秒杀接口地址隐藏:

思路:秒杀开始之前,先去请求接口获取秒杀地址 

1.接口改造 ,带上PathVariable参数

2.添加生成地址的接口

3.秒杀收到请求,先验证PathVariable

  • 数学公式验证码:

目的:防机器人,分散请求

思路:点击秒杀之前,先输入验证码,分散用户的请求

1.添加生成验证码接口

2.在获取秒杀路径的时候,验证验证码

  • 接口防刷限流:

1.利用redis缓存:比如限制用户1min中内只允许访问多少次

2.可以利用拦截器减少对业务代码的入侵

  • 解决超卖问题:

1.数据库加唯一索引:防止用户重复购买 

2.更新库存sql增加库存数量判断:防止库存变成负数 

(update table set count=count-1 where count>0) 

  • 秒杀优化后的主体详细流程大致可以分为如下步骤:
  1. 程序启动后将秒杀库存写入redis,设置内存标记商品是否已秒杀完
  2. 加入验证码
  3. 秒杀前首先获取秒杀路径
  4. 开始秒杀:
 a.判断用户登录信息是否异常

 b.验证秒杀路径

 c.获取秒杀商品内存标记并判断是否已秒杀完

 d.redis获取用户订单,判断该用户是否是重复秒杀

 e.如果是正常秒杀,对于未秒杀完的商品进行redis减库存操作

 f.如果redis库存已<0,标记内存商品已秒杀完

 g.秒杀正常则加入队列,异步处理订单入库,返回排队中

5.前端轮询订单结果(是否秒杀成功)

  • 实现效果如下:

用户登录界面

Java秒杀系统优化(高性能高并发)_第1张图片

描述商品列表页:

Java秒杀系统优化(高性能高并发)_第2张图片

秒杀商品详情页(增加验证码):

Java秒杀系统优化(高性能高并发)_第3张图片

增加访问限制(防刷限流):

Java秒杀系统优化(高性能高并发)_第4张图片

重复秒杀处理:

Java秒杀系统优化(高性能高并发)_第5张图片

换一个秒杀商品重新秒杀:

Java秒杀系统优化(高性能高并发)_第6张图片

秒杀成功进入订单详情页:

Java秒杀系统优化(高性能高并发)_第7张图片

数据库查看库存正常减1

Java秒杀系统优化(高性能高并发)_第8张图片

项目结构如下:

Java秒杀系统优化(高性能高并发)_第9张图片

秒杀接口部分核心代码如下:

获取验证码:

    @AccessLimit(seconds = 5, maxCount = 5, needLogin = true)
    @RequestMapping(value = "/path", method = RequestMethod.GET)
    @ResponseBody
    public Result getMiaoshaPath(HttpServletRequest request, MiaoshaUser user,
                                         @RequestParam("goodsId") long goodsId,
                                         @RequestParam(value = "verifyCode", defaultValue = "0") int verifyCode
    ) {
        if (user == null) {
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        boolean check = miaoshaService.checkVerifyCode(user, goodsId, verifyCode);
        if (!check) {
            return Result.error(CodeMsg.REQUEST_ILLEGAL);
        }
        String path = miaoshaService.createMiaoshaPath(user, goodsId);
        return Result.success(path);
    }

获取秒杀路径: 

@RequestMapping(value = "/verifyCode", method = RequestMethod.GET)
    @ResponseBody
    public Result getMiaoshaVerifyCod(HttpServletResponse response, MiaoshaUser user,
                                              @RequestParam("goodsId") long goodsId) {
        if (user == null) {
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        try {
            BufferedImage image = miaoshaService.createVerifyCode(user, goodsId);
            OutputStream out = response.getOutputStream();
            ImageIO.write(image, "JPEG", out);
            out.flush();
            out.close();
            return null;
        } catch (Exception e) {
            e.printStackTrace();
            return Result.error(CodeMsg.MIAOSHA_FAIL);
        }
    }

执行秒杀:

@RequestMapping(value = "/{path}/do_miaosha", method = RequestMethod.POST)
    @ResponseBody
    public Result miaosha(Model model, MiaoshaUser user,
                                   @RequestParam("goodsId") long goodsId,
                                   @PathVariable("path") String path) {
        model.addAttribute("user", user);
        if (user == null) {
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        //验证path
        boolean check = miaoshaService.checkPath(user, goodsId, path);
        if (!check) {
            return Result.error(CodeMsg.REQUEST_ILLEGAL);
        }
        //内存标记,减少redis访问
        if (localOverMap.size() > 0) {
            boolean over = localOverMap.get(goodsId);
            if (over) {
                return Result.error(CodeMsg.MIAO_SHA_OVER);
            }
        }

        //判断是否已经秒杀到了
        MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order != null) {
            return Result.error(CodeMsg.REPEATE_MIAOSHA);
        }
        //预减库存
        long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);//10
        if (stock < 0) {
            localOverMap.put(goodsId, true);
            return Result.error(CodeMsg.MIAO_SHA_OVER);
        }
        //mq入队
        MiaoshaMessage mm = new MiaoshaMessage();
        mm.setUser(user);
        mm.setGoodsId(goodsId);
        sender.sendMiaoshaMessage(mm);
        return Result.success(0);//排队中
  • 采用JMeter做个简单的压力测试

JMeter简单使用参考:JMeter压测入门简单使用_Garry1115的博客-CSDN博客

操作系统:centos 虚拟机双核

测试参数:生成5000个用户(token信息),设置5000个线程数循环10次,即运行50000次,然后查看聚合报告中的吞吐量。

Java秒杀系统优化(高性能高并发)_第10张图片

虚拟机测试结果吞吐量为2000左右,取决于机器配置

注:该接口是在未加入验证码动态获取秒杀路径的前提下测试的

项目完整代码下载地址:java秒杀系统 (resourcecode.cn)

你可能感兴趣的:(java,spring,cloud,秒杀系统,秒杀方案,java秒杀系统)