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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
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
相关文章
|
2月前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的服装商城管理系统
基于Java+Springboot+Vue开发的服装商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的服装商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
153 2
基于Java+Springboot+Vue开发的服装商城管理系统
|
12天前
|
XML Java 数据库连接
SpringBoot集成Flowable:打造强大的工作流管理系统
在企业级应用开发中,工作流管理是一个核心组件,它能够帮助我们定义、执行和管理业务流程。Flowable是一个开源的工作流和业务流程管理(BPM)平台,它提供了强大的工作流引擎和建模工具。结合SpringBoot,我们可以快速构建一个高效、灵活的工作流管理系统。本文将探讨如何将Flowable集成到SpringBoot应用中,并展示其强大的功能。
45 1
|
20天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
30天前
|
存储 安全 Java
打造智能合同管理系统:SpringBoot与电子签章的完美融合
【10月更文挑战第7天】 在数字化转型的浪潮中,电子合同管理系统因其高效、环保和安全的特点,正逐渐成为企业合同管理的新宠。本文将分享如何利用SpringBoot框架实现一个集电子文件签字与合同管理于一体的智能系统,探索技术如何助力合同管理的现代化。
61 4
|
30天前
|
前端开发 Java Apache
SpringBoot实现电子文件签字+合同系统!
【10月更文挑战第15天】 在现代企业运营中,合同管理和电子文件签字成为了日常活动中不可或缺的一部分。随着技术的发展,电子合同系统因其高效性、安全性和环保性,逐渐取代了传统的纸质合同。本文将详细介绍如何使用SpringBoot框架实现一个电子文件签字和合同管理系统。
53 1
|
1月前
|
文字识别 安全 Java
SpringBoot3.x和OCR构建车牌识别系统
本文介绍了一个基于Java SpringBoot3.x框架的车牌识别系统,详细阐述了系统的设计目标、需求分析及其实现过程。利用Tesseract OCR库和OpenCV库,实现了车牌图片的识别与处理,确保系统的高准确性和稳定性。文中还提供了具体的代码示例,展示了如何构建和优化车牌识别服务,以及如何处理特殊和异常车牌。通过实际应用案例,帮助读者理解和应用这一解决方案。
|
2月前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
基于Java+Springboot+Vue开发的大学竞赛报名管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的大学竞赛报名管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
219 3
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
|
2月前
|
Java 应用服务中间件 API
Vertx高并发理论原理以及对比SpringBoot
Vertx 是一个基于 Netty 的响应式工具包,不同于传统框架如 Spring,它的侵入性较小,甚至可在 Spring Boot 中使用。响应式编程(Reactive Programming)基于事件模式,通过事件流触发任务执行,其核心在于事件流 Stream。相比多线程异步,响应式编程能以更少线程完成更多任务,减少内存消耗与上下文切换开销,提高 CPU 利用率。Vertx 适用于高并发系统,如 IM 系统、高性能中间件及需要较少服务器支持大规模 WEB 应用的场景。随着 JDK 21 引入协程,未来 Tomcat 也将优化支持更高并发,降低响应式框架的必要性。
Vertx高并发理论原理以及对比SpringBoot
|
2月前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的蛋糕商城管理系统
基于Java+Springboot+Vue开发的蛋糕商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的蛋糕商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
156 3
基于Java+Springboot+Vue开发的蛋糕商城管理系统
|
2月前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的美容预约管理系统
基于Java+Springboot+Vue开发的美容预约管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的美容预约管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
54 3
基于Java+Springboot+Vue开发的美容预约管理系统