使用JMeter模拟秒杀场景

程序媛和她的猫

共 1665字,需浏览 4分钟

 · 2021-10-20

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

一、什么是秒杀?

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

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

温馨提示:各位男士可以提前做下笔记,双十一给女朋友 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


浏览 176
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报