在学习php的过程中自然会有一些相关的业务问题需要想办法解决,我们都在寻找更简便的方法解决问题,曾经面对秒杀等业务场景最多的莫过于数据库锁或者redis队列了,最近laravel业务有些多所以又接触了一个词叫随机拒绝,直到前几天才恍然大悟,这才是更加简便和常用的方法,也更节省硬件资源。不得不说laravel很多词语都非常的国际化而TP更本地化。

随机拒绝

在我们现在的实现中,每次提交秒杀下单请求至少会产生一次 Redis 查询,那有没有办法将这个查询也省掉呢?答案就是接下来我们将要学习的电商秒杀系统中另外一个大杀器:随机拒绝。

1. 原理介绍

现在各大电商的秒杀系统都会需要用户提前预约,只有预约了的用户才有资格去参与秒杀。假设 iPhone X 在天猫首发时有 10000 部库存,而预约购买的人数是 100 万,也就是预约和库存比是 100:1,一百个用户里只有一个能抢到。

既然大部分人都不可能抢到,那我们完全可以在用户提交下单请求时生成一个 0 ~ 100 的随机数,如果这个数大等于 2 则直接告诉用户抢购的人太多请重试,这样能落到 Redis 的请求就只剩下了 2% 也就是 2 万人。

而生成随机数这个操作完全不涉及到其他服务,可以通过扩展 Web 服务器来线性地提高吞吐量,这就实现了用很低的成本消减掉 98% 的压力。

2. 使用中间件实现

接下来我们就要实现随机拒绝这个功能,基于我们目前的实现,代码进入到控制器时就已经完成了 Redis 查询,因此我们需要在进入控制器之前就完成随机拒绝这个行为,在 Laravel 中可以用中间件来实现。

现在先创建一个中间件:

$ php artisan make:middleware RandomDropSeckillRequest

app/Http/Middleware/RandomDropSeckillRequest.php

<?php
namespace App\Http\Middleware;

use App\Exceptions\InvalidRequestException;
use Closure;

class RandomDropSeckillRequest
{
    // $percent 参数是在路由添加中间件时传入
    public function handle($request, Closure $next, $percent)
    {
        if (random_int(0, 100) < (int)$percent) {
            throw new InvalidRequestException('参与的用户过多,请稍后再试', 403);
        }

        return $next($request);
    }
}

然后需要注册一下这个中间件:

app/Http/Kernel.php

.
.
.
    protected $routeMiddleware = [
        .
        .
        .
        'random_drop' => \App\Http\Middleware\RandomDropSeckillRequest::class,
    ];
.
.
.

3. 配置路由

接下来我们需要修改一下路由文件:

routes/web.php

Route::post('seckill_orders', 'OrdersController@seckill')->name('seckill_orders.store')->middleware('random_drop:100');
.
.
.

我们将这个拒绝的百分比设成了 100,也就是百分百拒绝,现在我们在本地测试一下:

file

可以看到确实有效。现在我们将这个 100 改为 80:

routes/web.php

Route::post('seckill_orders', 'OrdersController@seckill')->name('seckill_orders.store')->middleware('random_drop:80');

4. 查看效果

执行压测:

file

Error %那一栏的值是 78%,与我们设定的拒绝概率差不多。这是因为当请求被随机拒绝时我们的代码会抛出 403 错误,而这个压测脚本没有把 403 当成一个合法的返回所以认为出错了。

现在吞吐量是 866 qps,比之前 802 qps 要高出 8%,平均响应时间从 391ms 减少到 344ms,减少了 12%。

由于 Laravel 本身性能问题,现在的 866 qps 已经接近极限,如果扩展更多的 Web 服务器,可以线性地提高此接口的吞吐量。

5. 总结

至此我们已经完成了秒杀接口的性能优化,根据目前的压测数据,每台 4 核 8 G 配置的阿里云服务器可以承载大约 400 qps 的并发量。

要发起一个参与用户数有 10W 的秒杀活动也只需要大约 250 台同配置的 Web 服务器、一台 Redis 服务器(Redis 单机性能 10W+ qps)、一台 MySQL 服务器(大多数秒杀请求不会到达 MySQL,所以不会成为瓶颈)。这些服务器每小时的费用大约是 400 RMB,秒杀活动通常只持续数十分钟,秒杀结束后可以直接销毁掉这些服务器,也就是说我们只需要花费不到 1000 RMB 就可以承载起 10W 用户的秒杀活动。