当前位置: 首页 > news >正文

从零搭建基于SpringBoot的秒杀系统(七):高并发导致超卖问题分析处理

在没有高并发的环境下,做到现在已经算是一个比较完善的后端逻辑了,但是如果同时有1000个请求或者更多请求的时候,就会产生很多问题,包括秒杀最怕的超卖。想一下,秒杀活动本来就是不赚钱甚至是亏钱的活动,如果超卖了,发货就代表亏本,不发货直接影响信用。因此绝不能出现超卖的情况。

(一)现象展示

我们用apache jmeter进行压力测试,为了方便测试,先将人员登陆认证代码注释掉,注释config下的ShiroConfig。接着在Controller下的killController添加测试代码:

@RequestMapping(value = prefix+"/test/execute",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public BaseResponse testexecute(@RequestBody @Validated KillDto killDto, BindingResult result, HttpSession httpSession){
    if (result.hasErrors()||killDto.getKillid()<0){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    try {
        Boolean res=killService.KillItem(killDto.getKillid(),killDto.getUserid());
        if (!res){
            return new BaseResponse(StatusCode.Fail.getCode(),"商品已经抢购完或您已抢购过该商品");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    BaseResponse baseResponse=new BaseResponse(StatusCode.Success);
    baseResponse.setData("抢购成功");
    return baseResponse;
}

同时清空item_kill_success表,将item_kill表中要卖的商品总数设置为5

接着配置apache jmeter,apache jmeter可以在官网下载,或者在我的公众号《Java鱼仔》中回复 秒杀系统 获取。点击apache jmeter/bin目里下的jmeter.bat即可运行。

下面这些配置的配置文件在上述回复中也可以获取到,或者在https://github.com/OliverLiy/SecondKill的Tool文件夹下获取,下面简单讲一下重要参数

线程组中主要是三个线程的设置

Http请求中主要配置地址、端口、请求方法以及请求数据,我在这里取killid=1,userid从外部文件选择。

csv配置即上面用户存储的文件地址,主要注意路径以及变量名,这里使用userId,前面的用户就需要使用"userid":${userId}。

csv文件通过英文逗号分隔。

查看结果树是用来看处理请求的,HTTP信息头管理器增加信息头

配置完成后启动系统,切换到观察树,点击jmeter的运行按钮,即可看到1000个线程开始跑。观察item_kill表,total总数变成了-47,发生了超卖,同时item_kill_success中也出现了同一用户购买多件商品的情况。

(二)问题分析

产生超卖的原因我们可以从代码中分析出来,在KillServiceImpl中,单线程情况下下面这段语句来判断当前用户是否抢购过该商品没有问题,但是在多线程的情况下,如果第一个线程还未执行后面的抢购代码,第二个线程就进来了,就会导致两个线程都执行抢购代码,从而导致发生超卖。

在commonRecordKillSuccessInfo方法中会再做一次判断,因此实际产生的订单数量会比total总量减少的数量要少

(三)mysql层面优化

通过分析我们找到了超卖的原因,从mysql的层面上可以优化代码。在itemKillMapper中,在每句查询和修改sql语句中增加一条对total的判断,新增sql的V2版本:

@Select("select \n" +
        "a.*,\n" +
        "b.name as itemName,\n" +
        "(\n" +
        "\tcase when(now() BETWEEN a.start_time and a.end_time and a.total>0)\n" +
        "\t\tthen 1\n" +
        "\telse 0\n" +
        "\tend\n" +
        ")as cankill\n" +
        "from item_kill as a left join item as b\n" +
        "on a.item_id = b.id\n" +
        "where a.is_active=1 and a.id=#{id} and a.total>0;")
ItemKill selectByidV2(Integer killId);

@Update("update item_kill set total=total-1 where id=#{killId} and total>0")
int updateKillItemV2(Integer killId);

在KillServiceImpl中增加KillItemV2版本,与第一版的区别就在于mysql的优化

//mysql优化
public Boolean KillItemV2(Integer killId, Integer userId) throws Exception {
    Boolean result=false;
    //判断当前用户是否抢购过该商品
    if (itemKillSuccessMapper.countByKillUserId(killId,userId)<=0){
        //获取商品详情
        ItemKill itemKill=itemKillMapper.selectByidV2(killId);
        if (itemKill!=null&&itemKill.getCanKill()==1 && itemKill.getTotal()>0){
            int res=itemKillMapper.updateKillItemV2(killId);
            if (res>0){
                commonRecordKillSuccessInfo(itemKill,userId);
                result=true;
            }
        }
    }else {
        System.out.println("您已经抢购过该商品");
    }
    return result;
}

同时在KillService接口中把对应的V2版本加上去:

public interface KillService {
    Boolean KillItem(Integer killId,Integer userId) throws Exception;
    Boolean KillItemV2(Integer killId,Integer userId) throws Exception;
}

接着在KillController中,也增加一段代码用来测试V2版本

//测试mysql优化的版本
@RequestMapping(value = prefix+"/test/execute2",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public BaseResponse testexecute2(@RequestBody @Validated KillDto killDto, BindingResult result, HttpSession httpSession){
    if (result.hasErrors()||killDto.getKillid()<0){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    try {
        Boolean res=killService.KillItemV2(killDto.getKillid(),killDto.getUserid());
        if (!res){
            return new BaseResponse(StatusCode.Fail.getCode(),"商品已经抢购完或您已抢购过该商品");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    BaseResponse baseResponse=new BaseResponse(StatusCode.Success);
    baseResponse.setData("抢购成功");
    return baseResponse;
}

(四)压力测试

接下来又可以使用jmeter进行测试了,首先清理一下数据库,item_kill表中id为1的项设置总量为5,清空item_kill_success表。

打开jmeter,将path改成/kill/test/execute2,点击运行项目。这一次1000个请求也没有出现超卖的情况:

但是点开订单表后,我们发现一个用户同时购买两次的问题还是存在

分析代码我们会发现,即使优化了mysql,如果两个请求同时进入下面的代码块,虽然不会导致超卖,但是依旧会导致一个用户购买多次的情况:

下面一章我们将对此再做优化。

到目前为止的代码均放在https://github.com/OliverLiy/SecondKill/tree/version6.0中

我搭建了一个微信公众号《Java鱼仔》,分享大量java知识点与学习经历,如果你对本项目有任何疑问,欢迎在公众号中联系我,我会尽自己所能为大家解答。

 

相关文章:

  • 从零搭建基于SpringBoot的秒杀系统(八):通过分布式锁解决多线程导致的问题
  • 读《世界是数字的》有感
  • 面试官问我:什么是静态代理?什么是动态代理?注解、反射你会吗?
  • redis入门到精通系列(十):springboot集成redis及redis工具类的编写
  • css3延时动画
  • redis入门到精通系列(十一):redis的缓存穿透、缓存击穿以及缓存雪崩详解
  • 子数组最大值设计02
  • redis入门到精通系列(十二):看完这一篇文章别再说不懂布隆过滤器
  • 如何用SpringBoot(2.3.3版本)快速搭建一个项目?文末有小彩蛋
  • Linux上find命令详解
  • 一步步带你看SpringBoot(2.3.3版本)自动装配原理
  • CCF系列之I’m stuck!(201312-5)
  • SpringBoot配置文件及自动配置原理详解,这应该是SpringBoot最大的优势了吧
  • SpringBoot整合jdbc、durid、mybatis详解,数据库的连接就是这么简单
  • Git学习笔记(一)--- Git的安装与配置
  • 【跃迁之路】【444天】程序员高效学习方法论探索系列(实验阶段201-2018.04.25)...
  • Android组件 - 收藏集 - 掘金
  • avalon2.2的VM生成过程
  • javascript从右向左截取指定位数字符的3种方法
  • Linux快速复制或删除大量小文件
  • python 装饰器(一)
  • Python学习笔记 字符串拼接
  • Vue实战(四)登录/注册页的实现
  • vue--为什么data属性必须是一个函数
  • 短视频宝贝=慢?阿里巴巴工程师这样秒开短视频
  • 关于字符编码你应该知道的事情
  • 基于Mobx的多页面小程序的全局共享状态管理实践
  • 如何在GitHub上创建个人博客
  • 体验javascript之美-第五课 匿名函数自执行和闭包是一回事儿吗?
  • 小程序开发之路(一)
  • 源码之下无秘密 ── 做最好的 Netty 源码分析教程
  • 《TCP IP 详解卷1:协议》阅读笔记 - 第六章
  • 【运维趟坑回忆录】vpc迁移 - 吃螃蟹之路
  • Unity3D - 异步加载游戏场景与异步加载游戏资源进度条 ...
  • 函数计算新功能-----支持C#函数
  • ​LeetCode解法汇总307. 区域和检索 - 数组可修改
  • #if #elif #endif
  • (bean配置类的注解开发)学习Spring的第十三天
  • (分享)自己整理的一些简单awk实用语句
  • (附源码)springboot助农电商系统 毕业设计 081919
  • (附源码)ssm跨平台教学系统 毕业设计 280843
  • (更新)A股上市公司华证ESG评级得分稳健性校验ESG得分年均值中位数(2009-2023年.12)
  • (转)Android学习系列(31)--App自动化之使用Ant编译项目多渠道打包
  • (轉貼) UML中文FAQ (OO) (UML)
  • .chm格式文件如何阅读
  • .NET Framework 的 bug?try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃
  • .net 托管代码与非托管代码
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .NET/C# 解压 Zip 文件时出现异常:System.IO.InvalidDataException: 找不到中央目录结尾记录。
  • .net图片验证码生成、点击刷新及验证输入是否正确
  • [ 环境搭建篇 ] 安装 java 环境并配置环境变量(附 JDK1.8 安装包)
  • [20171106]配置客户端连接注意.txt
  • [Android Pro] listView和GridView的item设置的高度和宽度不起作用
  • [C#]winform部署yolov5-onnx模型
  • [C#小技巧]如何捕捉上升沿和下降沿