PHP+JMeter模拟测试高并发场景。附代码。

文章目录

      • 1.建表
      • 2.编写模型类和控制器
      • 3.使用jmeter模拟高并发场景
      • 4. 开始测试
      • 5. 解决办法
      • 6. 其他的测试高并发的工具

1.建表

  1. 新建商品表

    CREATE TABLE `zlsn_concurrency_goods` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `goods_code` varchar(10) NOT NULL COMMENT '商品编码',
      `num` int(10) NOT NULL COMMENT '商品剩余数量',
      `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
      `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='模拟高并发-商品表';
    
  2. 新建订单表

    CREATE TABLE `zlsn_concurrency_user_order` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `user_code` varchar(10) NOT NULL COMMENT '用户编码',
      `order_code` varchar(10) NOT NULL COMMENT '订单编码',
      `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
      `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=utf8mb4 COMMENT='模拟高并发-用户订单表';
    

2.编写模型类和控制器

  1. 模型类

    
    
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    /**
     * 测试高并发商品表
     * Class TestGoods
     * @package App\Models
     */
    class TestGoods extends Model
    {
        protected $table = "concurrency_goods";
    }
    
    
    
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    
    /**
     * 测试高并发用户订单表
     * Class UserOrder
     * @package App\Models
     */
    class TestUserOrder extends Model
    {
        protected $table = "concurrency_user_order";
    }
    
  2. 控制器

    
    
    namespace App\Http\Controllers\Test;
    
    use App\Models\TestGoods;
    use App\Models\TestUserOrder;
    
    /**
     * 测试高并发接口
     * Class ConcurrencyController
     * @package App\Http\Controllers\Test
     */
    class ConcurrencyController
    {
        /**
         * Notes:秒杀功能
         * Author:zlsn <[email protected]>
         * DateTime: 2022/7/9 10:32
         */
        public function seckill()
        {
            $detail = TestGoods::where('id', 1)->select('id', 'num')->get()->toArray();
            //模拟存放用户id
            $ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
            $userId = "测试用户" . $ids[rand(0, 9)];
            if (!empty($detail) && $detail[0]['num'] > 0) {
    
                //商品id和商品数量
                $id = $detail[0]['id'];
                $number = $detail[0]['num'];
    
                //商品数量减一
                $number -= 1;
                $testGoodModel = new TestGoods();
                $param['num'] = $number;
                $testGoodModel->where('id', $id)->update($param);
                //保存到订单表
                $testUserOrderModel = new TestUserOrder();
                $testUserOrderModel->user_code = $userId;
                $testUserOrderModel->order_code = rand(111111, 999999);
                $testUserOrderModel->save();
                echo '抢购成功';
            } else {
                echo '商品数量不足';
            }
        }
    
    }
    

3.使用jmeter模拟高并发场景

jmeter依赖于jdk,需要本地配置一下。jdk配置方法

  1. 首先去下载jemter 下载链接

    1. windows下载zip或者tgz格式的都可以。解压-然后去bin目录双击jemter.bat文件,启动jemter。

    2. 右键test plan-add-Threads-Thread group (添加线程组)

    3. 再右键Thread group-add-Sampler-HTTP Request (添加http请求)

    4. 配置http请求,如图
      PHP+JMeter模拟测试高并发场景。附代码。_第1张图片

    5. 配置thread group,如图
      PHP+JMeter模拟测试高并发场景。附代码。_第2张图片

4. 开始测试

重现超卖情况以及商品数量变成负数的情况

  1. 设置商品数量初始值为10

    在这里插入图片描述

  2. 点击jemter菜单栏的start按钮,然后选择一下测试文件保存的路径即可。会发现订单表出现了超卖的情况,并且还会出现一个用户秒杀到多个商品的情况。(我这里出现了15份订单)。

    这里要说明一下为什么商品表的数量没有变成负数?

    ​ 因为秒杀方法中有对于商品数量的判断,即使最后剩余1件商品,然后同时进来了多个请求,此时它们都会把商品数量修改成0,然后再去保存到订单表。

    ​ 如果在判断内,重新获取商品数量,再去减一的话,就会出现负数。如果改成如下代码

    public function seckill()
        {
            $detail = TestGoods::where('id', 1)->select('id', 'num')->get()->toArray();
            //模拟存放用户id
            $ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
            $userId = "测试用户" . $ids[rand(0, 9)];
            if (!empty($detail) && $detail[0]['num'] > 0) {
    
                //商品id和商品数量
                $id = $detail[0]['id'];
    
                //商品数量减一
                //再次获取当前的商品数量,再进行减一,而不是直接用判断外获取的商品数量减一
                $data = TestGoods::where('id', 1)->select('num')->get()->toArray();
                $number = $data[0]['num'];
                $number -= 1;
                
                $testGoodModel = new TestGoods();
                $param['num'] = $number;
                $testGoodModel->where('id', $id)->update($param);
                //保存到订单表
                $testUserOrderModel = new TestUserOrder();
                $testUserOrderModel->user_code = $userId;
                $testUserOrderModel->order_code = rand(111111, 999999);
                $testUserOrderModel->save();
                echo '抢购成功';
            } else {
                echo '商品数量不足';
            }
        }
    

    PHP+JMeter模拟测试高并发场景。附代码。_第3张图片

5. 解决办法

​ 解决同一个用户秒杀多件商品的情况—添加判断(但是判断有时候未生效,还会存在一个用户抢到两件商品的情况,表示费解。。

  1. 解决方法一 悲观锁

    public function seckill()
        {
    //        $detail = TestGoods::where('id', 1)->select('id', 'num')->get()->toArray();
            DB::beginTransaction();
            $detail = DB::select("select num,id from zlsn_concurrency_goods where id = 1 for update ");
            //模拟存放用户id
            $ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
            $userId = "测试用户" . $ids[rand(0, 9)];
            if (!empty($detail) && $detail[0]->num > 0) {
    
                //商品id和商品数量
                $number = $detail[0]->num;
                $id = $detail[0]->id;
    
                $testUserOrderModel = new TestUserOrder();
                $check = $testUserOrderModel->where('user_code', $userId)->get()->toArray();
                //判断当前用户是否已经抢购过--这判断不一定会生效,依然可能存在一个用户购买多次的情况
                if (empty($check)) {
                    //商品数量减一
                    $number -= 1;
                    $testGoodModel = new TestGoods();
                    $param['num'] = $number;
                    $res = $testGoodModel->where('id', $id)->update($param);
    
                    if ($res) {
                        //保存到订单表
                        $testUserOrderModel->user_code = $userId;
                        $testUserOrderModel->order_code = rand(111111, 999999);
                        $testUserOrderModel->save();
                        echo '抢购成功';
                        DB::commit();
                    } else {
                        echo '商品数量修改失败';
                        DB::rollBack();
                    }
                } else {
                    echo '您已经购买过此商品';
                    DB::rollBack();
                }
            } else {
                echo '商品数量不足';
                DB::rollBack();
            }
        }
    

    在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,其余请求必须等待。

    缺点:虽然可以解决线程安全的问题,但是对于高并发并不适用。对于很多没有抢到锁的请求,会陷入一直等待的状态,同时,这种请求很多的话,瞬间增大系统的平均响应时间,结果可能是数据库连接数耗尽,系统陷入异常。

  2. 解决方法二 FIFO队列思路

    FIFO(first input first output):即先进先出。这样的话就会避免有些请求永远获取不到锁的情况。但是有种将多线程强行变成单线程的感觉。就像是一条八车道的路,强行并称了单行道。

    缺点:如果使用这种情况,可能一瞬间把队列内存撑爆,然后系统陷入异常。假如设计一个超级大的内存队列,但是系统处理队列内请求的速度,根本无法和疯狂涌入队列内的数目相比。也就是说队列内的请求会越积累越多,最终web系统平均相应时长还是会增大,造成系统异常。

  3. 解决方法三 文件锁思路

    使用非阻塞的文件排他锁

    缺点:在我们对文件进行读写操作时,很有可能多个进程对进一文件进行操作,如果这时不对文件的访问进行相应的独占,就容易造成数据丢失

    public function seckill()
        {
            $fp = fopen("lock.txt", "w+");
            if (!flock($fp, LOCK_EX | LOCK_NB)) {
                echo "系统繁忙,请稍后再试";
                return;
            }
    
            $detail = TestGoods::where('id', 1)->select('id', 'num')->get()->toArray();
            //模拟存放用户id
            $ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
            $userId = "测试用户" . $ids[rand(0, 9)];
            if (!empty($detail) && $detail[0]['num'] > 0) {
    
                //商品id和商品数量
                $number = $detail[0]['num'];
                $id = $detail[0]['id'];
    
                $testUserOrderModel = new TestUserOrder();
                $check = $testUserOrderModel->where('user_code', $userId)->get()->toArray();
                //判断当前用户是否已经抢购过--这判断不一定会生效,依然可能存在一个用户购买多次的情况
                if (empty($check)) {
                    //商品数量减一
                    $number -= 1;
                    $testGoodModel = new TestGoods();
                    $param['num'] = $number;
                    $res = $testGoodModel->where('id', $id)->update($param);
    
                    if ($res) {
                        //保存到订单表
                        $testUserOrderModel->user_code = $userId;
                        $testUserOrderModel->order_code = rand(111111, 999999);
                        $testUserOrderModel->save();
                        echo '抢购成功';
                        flock($fp, LOCK_UN);//释放锁
                    } else {
                        echo '商品数量修改失败';
                    }
                } else {
                    echo '您已经购买过此商品';
                }
            } else {
                echo '商品数量不足';
            }
            fclose($fp);
        }
    
  4. 解决方法四 乐观锁

    在数据表中添加version字段。

    所有进来的请求都可以去修改数据,但是在修改数据之前会先获得该数据的一个版本号,修改之后如果和修改前的版本号不一致,则进行事务回滚。

    缺点:增大CPU的计算开销。

    public function seckill()
        {
            DB::beginTransaction();
            $detail = TestGoods::where('id', 1)->select('id', 'num','version')->get()->toArray();
            //模拟存放用户id
            $ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
            $userId = "测试用户" . $ids[rand(0, 9)];
            if (!empty($detail) && $detail[0]['num'] > 0) {
    
                //商品id和商品数量
                $number = $detail[0]['num'];
                $id = $detail[0]['id'];
                $version = $detail[0]['version'];
    
                $testUserOrderModel = new TestUserOrder();
                $check = $testUserOrderModel->where('user_code', $userId)->get()->toArray();
                //判断当前用户是否已经抢购过--这判断不一定会生效,依然可能存在一个用户购买多次的情况
                if (empty($check)) {
                    //商品数量减一
                    $number -= 1;
                    $testGoodModel = new TestGoods();
                    $param['num'] = $number;
                    $res = $testGoodModel->where('id', $id)->update($param);
                    $data = TestGoods::where('id', 1)->select('version')->get()->toArray();
                    $version_after = $data[0]['version'];
                    if($version_after != $version){
                        DB::rollBack();
                    }
                    if ($res) {
                        //保存到订单表
                        $testUserOrderModel->user_code = $userId;
                        $testUserOrderModel->order_code = rand(111111, 999999);
                        $testUserOrderModel->save();
                        echo '抢购成功';
                        DB::commit();
                    } else {
                        echo '商品数量修改失败';
                        DB::rollBack();
                    }
                } else {
                    echo '您已经购买过此商品';
                    DB::rollBack();
                }
            } else {
                echo '商品数量不足';
                DB::rollBack();
            }
        }
    
  5. Redis中的watch

    这种方法也属于乐观锁之一。(模拟了多次,其中有两次还是出现了超卖的情况,不知道是不是自己操作的问题。

    public function seckill()
        {
            $myWatchKey = Redis::get('myWatchKey'); //不需要提前set,本就是获取一个null值。如果提前set的话,那每次请求接口的时候,这个可以都是固定的值
            $number = 20;//商品数量
            if ($myWatchKey < $number) {
                Redis::watch("myWatchKey");
                Redis::multi(); //开启事务
    
                //插入抢购数据
                Redis::set("myWatchKey", $myWatchKey + 1);
    
                $rob_result = Redis::exec();//执行事务
                if ($rob_result) {
                    Redis::hSet("watchKeyList", "user_" . mt_rand(1, 9999), $myWatchKey);
                    $myWatchList = Redis::hGetAll("watchKeyList");
    
                    echo "抢购成功!
    "
    ; echo "剩余数量:" . ($number - $myWatchKey - 1) . "
    "
    ; echo "用户列表:
    ";
                    var_dump($myWatchList);
                } else {
                    Redis::hSet("watchKeyList", "user_" . mt_rand(1, 9999), '没抢到');
                    echo "手气不好,再抢购!";
                    exit;
                }
            }else{
                echo '商品数量不足,抢购失败';
            }
        }
    

6. 其他的测试高并发的工具

  1. apache安装目录下的bin目录可以使用ab命令测试高并发

    [root@myhost vhost]# ab -c 10000 -n 100000 http://www.baidu.com
    -c 表示并发用户数
    -n 表示请求总数
    
  2. postman也可以测试高并发

    步骤:

    1. 新建一个collection

    2. 在collection中新建请求

    3. 点击collection,有个run按钮。可以配置请求数。如图
      PHP+JMeter模拟测试高并发场景。附代码。_第4张图片

PHP+JMeter模拟测试高并发场景。附代码。_第5张图片

  1. 同理Apifox也一样

参考链接:

​ 如何用PHP解决高并发问题?(附源码)
推荐一篇文章 PHP+Redis怎么解决商品超卖问题

你可能感兴趣的:(php,Jmeter,高并发)