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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 在没有高并发的环境下,做到现在已经算是一个比较完善的后端逻辑了,但是如果同时有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知识点与学习经历,如果你对本项目有任何疑问,欢迎在公众号中联系我,我会尽自己所能为大家解答。

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
打赏
0
0
0
0
10
分享
相关文章
springboot图书馆管理系统前后端分离版本
springboot图书馆管理系统前后端分离版本
31 12
基于SpringBoot+Vue实现的大学生体质测试管理系统设计与实现(系统源码+文档+数据库+部署)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
16 2
基于SpringBoot+Vue实现的大学生就业服务平台设计与实现(系统源码+文档+数据库+部署等)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
18 6
基于SpringBoot+Vue的班级综合测评管理系统设计与实现(系统源码+文档+数据库+部署等)
✌免费选题、功能需求设计、任务书、开题报告、中期检查、程序功能实现、论文辅导、论文降重、答辩PPT辅导、会议视频一对一讲解代码等✌
20 4
基于Java+SpringBoot+Vue实现的车辆充电桩系统设计与实现(系统源码+文档+部署讲解等)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
22 6
|
11天前
|
社交软件红包技术解密(四):微信红包系统是如何应对高并发的
本文将为读者介绍微信百亿级别红包背后的高并发设计实践,内容包括微信红包系统的技术难点、解决高并发问题通常使用的方案,以及微信红包系统的所采用高并发解决方案。
50 13
高并发交易场景下业务系统性能不足?体验构建高性能秒杀系统!完成任务可领取锦鲤抱枕!
高并发交易场景下业务系统性能不足?体验构建高性能秒杀系统!完成任务可领取锦鲤抱枕!
|
17天前
|
Spring Boot 3 集成Spring AOP实现系统日志记录
本文介绍了如何在Spring Boot 3中集成Spring AOP实现系统日志记录功能。通过定义`SysLog`注解和配置相应的AOP切面,可以在方法执行前后自动记录日志信息,包括操作的开始时间、结束时间、请求参数、返回结果、异常信息等,并将这些信息保存到数据库中。此外,还使用了`ThreadLocal`变量来存储每个线程独立的日志数据,确保线程安全。文中还展示了项目实战中的部分代码片段,以及基于Spring Boot 3 + Vue 3构建的快速开发框架的简介与内置功能列表。此框架结合了当前主流技术栈,提供了用户管理、权限控制、接口文档自动生成等多项实用特性。
69 8
基于SpringBoot+Vue实现的冬奥会科普平台设计与实现(系统源码+文档+数据库+部署)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
17 0
高并发场景秒杀抢购超卖Bug实战重现
在电商平台的秒杀活动中,高并发场景下的抢购超卖Bug是一个常见且棘手的问题。一旦处理不当,不仅会引发用户投诉,还会对商家的信誉和利益造成严重损害。本文将详细介绍秒杀抢购超卖Bug的背景历史、业务场景、底层原理以及Java代码实现,旨在帮助开发者更好地理解和解决这一问题。
78 12

热门文章

最新文章