首页 文章详情

使用JMeter模拟秒杀场景

程序媛和她的猫 | 922 2021-10-20 12:53 0 0 0
UniSMS (合一短信)

最近工作中,需要开发一个健身房预定的功能,我主要负责后端的开发。这是一个很经典的秒杀场景,所以想记录一下我关于秒杀的设计,以及如何使用工具来模拟秒杀。

一、什么是秒杀?

秒杀就是大量用户同一时间同时进行抢购,从系统层面来看,就是多个进程(线程)同时访问同一个共享资源。

举个栗子:双十一李佳琪直播间,这个就属于很经典的秒杀场景,每放出一个商品,很多人就一起去抢购,谁抢到就是谁的。

温馨提示:各位男士可以提前做下笔记,双十一给女朋友 or 老婆一个惊喜哦!

二、秒杀场景的特点

1、高并发:秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
2、读多写少:访问请求量远远大于库存数量,只有少部分用户能够抢购成功。
3、防止超卖现象:秒杀流程比较简单,一般就是下订单减库存。但是,很容易出现超卖问题,我们可以使用分布式锁(集群部署)或者synchrnized(单机部署),或者先修改库存再生成订单等方法,防止订单生成了但没有库存的超卖问题。

超卖问题:比如Lamer面霜库存有 100 件,但是在抢购过程中,导致 1000 个用户下单成功。那么就会有 9900 个用户,显示下单成功,但库存不够,没有商品发给她们,这个体验太不好了,很容易招到疯狂投诉。

三、秒杀 Demo

我做的这个健身房预定属于一个小项目,由于公司内部使用,并发量不是很高,所以是单机部署,我使用的是 Java 中的 synchronized 关键字来控制超卖。

/**
 * 预定控制类
 */

@RestController
@RequestMapping("/ding")
public class DingController {
    @Resource
    private DingService dingService;

    /**
     * 
     * @param dingDetail 
     * @return
     */

    @RequestMapping("/saveDing")
    public Result saveDingDetail(@RequestBody DingDetail dingDetail){
        return Result.success(dingService.saveDingDetail(dingDetail));
    }
    
}
/**
 * 预定接口类
 */

public interface DingService {
    /**
     * 
     * @param dingDetail
     * @return
     */

    String saveDingDetail(DingDetail dingDetail);
}
/**
 * 预定接口实现类
 */

@Service
public class DingServiceImpl implements DingService {
    /**
     * 
     * @param dingDetail
     * @return
     */

    @Override
    public synchronized String saveDingDetail(DingDetail dingDetail) {
        String dingId = dingDetail.getDingId();
        Ding ding = dingMapper.selectById(dingDetail.getDingId());
        int dingNum = ding.getDingNum();
        int num = ding.getNum();
        if(dingNum < num){
            // 下单:将当前用户插入到预定详情表
            dingDetailMapper.insert(dingDetail);
            
            // 修改库存:预定表,已预定人数加1
            dingNum = dingNum + 1;
            dingMapper.updateDingNum(dingId, dingNum);
            
            return "预定成功!";
        }
        return "预定失败!";
    }
}
/**
 * 预定表实体类
 * 存储每个预定的信息,包括预定名称、预定类型......可预定人数、已预定人数
 */

@Data
@TableName("t_ding")
public class Ding extends BaseEntity {
    /**
     * 预定名称
     */

    private String name;
    /**
     * 预定类型
     */

    private String type;
    /**
     * 可预定开始时间
     */

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    private Date startTime;
    /**
     * 可预定结束时间
     */

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    private Date endTime;
    /**
     * 预定日期
     */

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern ="yyyy-MM-dd", timezone = "GMT+8")
    @JSONField(format = "yyyy-MM-dd")
    private Date dingDate;
    /**
     * 预定开始时间
     */

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    private Date dingStartTime;
    /**
     * 预定结束时间
     */

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    private Date dingEndTime;
    /**
     * 可预定人数
     */

    private Integer num;
    /**
     * 已预定人数
     */

    private Integer dingNum;
}
/**
 * 预定详情表实体类
 * 存储用户信息,包括用户ID、用户姓名、预定ID(和预定表的 ID 关联,是预定表的外键)......
 */

@Data
@TableName("t_ding_detail")
public class DingDetail extends BaseEntity {
    /**
     * 用户ID
     */

    private String userId;
    /**
     * 用户姓名
     */

    private String userName;
    /**
     * 预定ID
     */

    private String dingId;
    /**
     * 是否签到(0-否,1-是)
     */

    private Integer signIn;
    /**
     * 预定开始时间
     */

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    private Date dingStartTime;
    /**
     * 预定结束时间
     */

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    private Date dingEndTime;
}

四、使用 JMeter 模拟秒杀场景

上述就是预定过程的代码,在将这段代码交付给前端开发调用之前,需要做好充分的测试,所以我想模拟秒杀场景,对这个接口做个测试,确保其没有问题,再将其交给前端同事。

有想过自己写一个线程池,创建 100 个线程去模拟这个场景,但是觉得有点麻烦,上网查了一下,发现有很多现成的压测工具,最终决定使用 Apache JMeter 来做这个模拟。

1、JMeter 下载

JMeter 官网下载安装包,JMeter 官网下载地址:https://jmeter.apache.org/,见图1、2,下载下来的 JMeter 安装包见图3。

图1
图2
图3
2、JMeter 安装

下载之后,解压到任意目录。由于我本机所有软件都放在 D:\software 下,所以我将其解压到这个目录。

首先将压缩包从下载目录移动到 D:\software 目录,右键压缩包,选择“解压到当前文件夹”。

图4
3、JMeter 启动

解压之后,以后每次需要启动 JMeter,就进到 bin 目录,双击 jmeter.bat 即可启动。

图5

注意:在安装 JMeter 之前,本机应该已经安装了 1.8 及以上版本的 JDK,因为 JMeter 是用 Java 写的,运行的时候需要 Java 环境。否则你双击 jmeter.bat 启动 Jmeter 时会报错,报错信息见图6。

图6 Java未安装错误

注意:双击 jmeter.bat 启动 JMeter 的时候会有两个窗口,Jmeter 的命令窗口(图7)和 Jmeter 的图形操作界面(图8),不可以关闭命令窗口。

图7
图8
4、JMeter 基础设置
(1)图形操作界面语言切换

JMeter 默认界面语言是英文,为了方便,我们将其切换成中文,有两种方式,临时切换和永久切换,临时切换,下次重启 JMeter ,又变回英文了,永久切换,下次重启仍然是中文。

(a)临时切换

图9

(b)永久切换

修改 JMeter 配置文件,进入 bin 目录,找到 jmeter.properties 文件,使用编辑器打开,在 #language=en 下面插入一行 language=zh_CN,修改后保存,然后重启 JMter。以后每次启动 Jmeter 界面显示的都是简体中文。

图10
图11
(2)修改 Jmeter 默认编码为 utf-8 解决控制台乱码

JMeter 下载下来之后,默认编码是 ISO-8859-1,但是使用这种编码方式,如果 HTTP 响应中包含中文,就会出现中文乱码的问题,见图12。

图12

解决方案就是将编码方式修改为 utf-8,有两种方式修改编码,一种是使用后置处理器 BeanShell PostProcessor,但是重启之后,编码又变回 ISO-8859-1,还是会出现中文乱码问题,一种是修改 JMeter 配置文件,永久修改编码。

(a)通过后置处理器 BeanShell PostProcessor 修改编码

  • 右键点击 “刚才创建的线程组” → “添加” → “后置处理器” → “BeanShell PostProcessor”
图13

输入 “prev.setDataEncoding("utf-8"); ”,修改响应数据编码格式为utf-8,此时发起请求,响应结果中就没有乱码了。

(b)修改配置文件

进入 bin 目录,找到 jmeter.properties 文件,使用编辑器打开,在 #sampleresult.default.encoding=ISO-8859-1 下面插入一行 sampleresult.default.encoding=utf-8,修改后保存,然后重启 JMeter。

图14
5、JMeter 模拟秒杀
(1)新建测试计划
  • 点击 "文件” → “新建”
图15
图16
(2)添加线程组
  • 右键点击 "测试计划” → “添加” → “线程(用户)” → “线程组”
图17
  • 配置线程组参数
图18

线程组主要参数详细介绍

  • 线程数:虚拟用户数。一个虚拟用户占用一个进程或线程,模拟多少用户访问就填写多少个线程数。
  • Ramp-Up(秒):设置的虚拟用户数需要多长时间全部启动。如果线程数为100,Ramp-Up 为 5 秒,那么需要 5 秒钟启动 100 个线程,也就是每秒钟启动 20 个线程,相当于每秒模拟20个用户进行访问。Ramp-Up 设置为 0 既是并发访问。
  • 循环次数:如果线程数为 100,循环次数为 100。那么总请求数为 100*100=10000 。如果勾选了“永远”,那么所有线程会一直发送请求,直到选择停止运行脚本。

因为我想模拟 30 个人同一时间去预定 10 台跑步机,所以我设置的参数如下,线程数:30,Ramp-Up:0(模拟 10 个线程在同一时间并发执行),循环次数:1,总请求数 30*1=30 次,见图17。

(3)添加我们要测试的接口
  • 右键点击 “刚才创建的线程组” → “添加” → “取样器” → “HTTP请求”
图19
  • 填写接口请求参数,我要测试的接口属于 Spring Boot 项目,配置如下:
图20

Http请求主要参数详细介绍

  • 协议:向目标服务器发送HTTP请求协议,可以是 HTTP 或 HTTPS,默认为 HTTP。
  • 服务器名称或IP :HTTP请求发送的目标服务器名称或IP,我们这里是要测试本地接口,所以服务器名称或IP为 localhost 或者 127.0.0.1。
  • 端口号:目标服务器的端口号,我们这里是 8080。
  • 方法:发送 HTTP 请求的方法,可用方法包括GET、POST、HEAD、PUT、OPTIONS、TRACE、DELETE等,我们这个接口使用 POST 访问。
  • 路径:目标 URL 路径,即 URL 中去掉服务器地址、端口及参数后剩余的部分,我们这里是 xxx/ding/saveDing,xxx 是应用名称(spingboot  项目中的 spring.application.name)。
  • 内容编码:编码方式,默认为ISO-8859-1编码,我们这里配置为 utf-8。
  • 参数:接口参数,如果是GET请求,我们将参数设置在参数表中,表中每一行为一个参数(key:参数名,value:参数值),注意参数传入中文时需要勾选“编码”,如果是POST请求,我们可以将参数以JSON方式写在消息体数据框中。
(4)添加察看结果树
  • 右键点击 “刚才创建的线程组” → “添加” → “监听器” → “察看结果树”
图21
  • 然后修改响应数据格式,我这里用的是JSON格式,运行上面的 HTTP 请求,就可以在取样器结果中看到本次请求返回的响应数据。
图22
(5)运行 HTTP 请求,察看模拟结果

接下来,我将使用 JMeter 来模拟健身房预定场景,我将健身房跑步机设置为 10 台,线程组线程数设置为 30,模拟 30 个用户抢购这 10 台跑步机,谁抢到就是谁的。

图23

期间踩过一些坑,我都用红色文字做了标记,大家可以注意一下,如果遇到同样的问题,可以拿来借鉴。

选择你要运行的 HTTP 请求,点击上侧的绿色三角形,运行该 HTTP 请求,此时界面会弹出一个提示框,大概意思是“是否要在测试之前保存这个测试案例”,一般选择“No”或者 X 掉就行

图24

HTTP 请求运行起来之后,察看结果树会显示请求运行结果。此时发现察看结果树中 “HTTP请求”字样是红色的,说明 HTTP 请求失败了,见图25。

图25

查看请求失败的原因,随便点开一个“HTTP请求”,一般的问题从 “取样器结果”、“请求”、“响应数据” 这三个地方基本都能找到原因。

见图26,分析请求失败的原因,响应码是 415,415 一般是由 HTTP 请求头中 Content-Type 不对引起的。从报错信息可以看到我们刚才发送的 HTTP 请求的 Content-type 是 text/plain 类型,而这个请求是 POST 请求,POST 请求默认是 JSON 数据格式。我们只要将请求的 Content-type 修改成 JSON 格式,即可解决这个问题

图26

解决方法是将 HTTP 请求的 Content-Type 修改为 JSON 格式,怎么修改呢?

右键点击 “刚才创建的线程组” → “添加” → “配置元件” → “HTTP信息头管理器”,见图27,在 HTTP 信息头管理器中,将 Content-Type 修改为 application/json,见图28。

图27
图28

将察看结果树清除,“右键察看结果树” → “清除”,见图29,再次运行 HTTP 请求,此时查看结果树中的“HTTP请求”字样是绿色的,表示HTTP请求成功,见图30。

图29
图30

查看模拟结果,30 个用户,10 个预定席位,察看结果树中,30 个请求,有 10 个预定成功,20个预定失败,见图31、32,再看预定详情表,只插入了 10 条数据,见图33,综上这些说明秒杀模拟成功了。

图31
图32
图33


good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter