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

本文涉及的产品
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 在没有高并发的环境下,做到现在已经算是一个比较完善的后端逻辑了,但是如果同时有1000个请求或者更多请求的时候,就会产生很多问题,包括秒杀最怕的超卖。想一下,秒杀活动本来就是不赚钱甚至是亏钱的活动,如果超卖了,发货就代表亏本,不发货直接影响信用。因此绝不能出现超卖的情况。

网络异常,图片无法展示
|


在没有高并发的环境下,做到现在已经算是一个比较完善的后端逻辑了,但是如果同时有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://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FOliverLiy%2FSecondKill的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://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FOliverLiy%2FSecondKill%2Ftree%2Fversion6.0%25E4%25B8%25AD


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

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
21小时前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的毕业生学历证明系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的毕业生学历证明系统的详细设计和实现(源码+lw+部署文档+讲解等)
19 5
|
21小时前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的房屋租赁系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的房屋租赁系统的详细设计和实现(源码+lw+部署文档+讲解等)
18 6
|
21小时前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的成都奥科厨具厂产品在线销售系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的成都奥科厨具厂产品在线销售系统的详细设计和实现(源码+lw+部署文档+讲解等)
17 6
|
21小时前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的大学生在线论坛系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的大学生在线论坛系统的详细设计和实现(源码+lw+部署文档+讲解等)
12 6
|
4天前
|
缓存 NoSQL Java
Spring Boot中的高并发处理
Spring Boot中的高并发处理
|
4天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的大学生心理健康诊断专家系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的大学生心理健康诊断专家系统的详细设计和实现(源码+lw+部署文档+讲解等)
17 0
|
4天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的宝鸡文理学院学生成绩动态追踪系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的宝鸡文理学院学生成绩动态追踪系统的详细设计和实现(源码+lw+部署文档+讲解等)
6 0
|
4天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的研究生志愿填报辅助系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的研究生志愿填报辅助系统的详细设计和实现(源码+lw+部署文档+讲解等)
8 0
|
4天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的社区便民服务管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的社区便民服务管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
4 0
|
4天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的美食分享系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的美食分享系统的详细设计和实现(源码+lw+部署文档+讲解等)
5 0