Hyperf 协程 PHP 实战:高并发秒杀接口从 0 到 1 落地

Hyperf 协程 PHP 实战:高并发秒杀接口从 0 到 1 落地

秒杀是高并发场景的典型考题:防超卖、防重复下单、削峰、异步落库、协程高吞吐。本文基于 Hyperf + Swoole 协程 + Redis Lua + 异步队列,给出可直接上线的生产级实现。


一、方案选型与核心要点

1.1 为什么选 Hyperf

  • 原生协程、连接池、异步任务,IO 密集型场景性能提升 5--10 倍
  • 开箱即用:Redis/DB/ 队列 / 限流 / 注解,适合快速搭建高并发服务
  • 同步写法、异步执行,无回调地狱,PHP 开发者低成本上手

1.2 秒杀核心设计(必看)

  1. Redis Lua 原子扣减:库存判断 + 扣减 + 去重单原子执行,杜绝超卖
  2. 一人一单:用户 ID 集合判重,防止重复抢购
  3. 异步队列削峰:Redis 预扣成功后,异步写 DB,接口秒返回
  4. 幂等 + 防刷:请求唯一标识、接口限流、IP / 用户维度风控
  5. 最终一致性:定时任务补偿库存、订单超时回补

二、环境准备

2.1 依赖安装

bash

运行

复制代码
composer require hyperf/redis hyperf/async-queue hyperf/limiter hyperf/database

2.2 配置(config/autoload/)

  • redis.php:连接池、超时、集群按需配置
  • async_queue.php:驱动 Redis,并发数、重试次数
  • databases.php:MySQL 订单 / 商品表

三、核心数据结构与初始化

3.1 Redis Key 设计

plaintext

复制代码
seckill:stock:{product_id}    # 商品库存
seckill:uid:{product_id}     # 已购用户集合(去重)
seckill:token:{request_id}   # 幂等令牌

3.2 库存初始化(命令 / 后台)

php

运行

复制代码
// 商品ID=1001,库存=100
Redis::set('seckill:stock:1001', 100);
Redis::del('seckill:uid:1001');

四、Lua 原子脚本(防超卖核心)

文件:storage/lua/seckill_stock.lua

lua

复制代码
-- KEYS[1]=库存key, KEYS[2]=用户集合key
-- ARGV[1]=用户ID, ARGV[2]=购买数量
local stock = redis.call('GET', KEYS[1])
if not stock or tonumber(stock) < tonumber(ARGV[2]) then
    return -1 -- 库存不足
end
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
    return -2 -- 已购买
end
redis.call('DECRBY', KEYS[1], ARGV[2])
redis.call('SADD', KEYS[2], ARGV[1])
return 0 -- 成功

五、秒杀接口实现(控制器)

5.1 注解与依赖注入

php

运行

复制代码
use Hyperf\Redis\Redis;
use Hyperf\AsyncQueue\Driver\DriverFactory;
use Hyperf\RateLimit\Annotation\RateLimit;
use App\Job\CreateSeckillOrderJob;

#[RateLimit(create: 100, consume: 100)] // 限流
public function seckill(int $product_id, int $user_id, string $request_id)

5.2 完整逻辑

php

运行

复制代码
public function seckill(int $product_id, int $user_id, string $request_id)
{
    // 1. 基础校验
    if ($user_id <= 0 || $product_id <= 0 || empty($request_id)) {
        return $this->fail('参数错误');
    }
    // 2. 幂等去重
    if (Redis::exists("seckill:token:{$request_id}")) {
        return $this->fail('重复请求');
    }
    Redis::setex("seckill:token:{$request_id}", 300, 1);

    // 3. Lua 原子扣减
    $lua = file_get_contents(BASE_PATH . '/storage/lua/seckill_stock.lua');
    $keys = [
        "seckill:stock:{$product_id}",
        "seckill:uid:{$product_id}"
    ];
    $argv = [$user_id, 1];
    $res = Redis::eval($lua, 2, ...$keys, ...$argv);

    // 4. 结果处理
    if ($res === -1) return $this->fail('库存不足');
    if ($res === -2) return $this->fail('已购买');
    if ($res !== 0) return $this->fail('抢购失败');

    // 5. 异步队列落库
    $driver = DriverFactory::get('default');
    $driver->push(new CreateSeckillOrderJob($user_id, $product_id, $request_id));

    return $this->success('抢购成功,请在订单中查看');
}

六、异步队列:订单落库(防 DB 雪崩)

6.1 任务类:app/Job/CreateSeckillOrderJob.php

php

运行

复制代码
use Hyperf\AsyncQueue\Job;
use Hyperf\DB\DB;

class CreateSeckillOrderJob extends Job
{
    public $user_id;
    public $product_id;
    public $request_id;

    public function __construct($user_id, $product_id, $request_id)
    {
        $this->user_id = $user_id;
        $this->product_id = $product_id;
        $this->request_id = $request_id;
    }

    public function handle()
    {
        // 幂等校验
        if (DB::table('seckill_order')->where('request_id', $this->request_id)->exists()) {
            return;
        }
        // 事务写订单 + 扣减DB库存
        DB::beginTransaction();
        try {
            // 扣减DB库存
            DB::table('product')->where('id', $this->product_id)->decrement('stock', 1);
            // 创建订单
            DB::table('seckill_order')->insert([
                'user_id' => $this->user_id,
                'product_id' => $this->product_id,
                'request_id' => $this->request_id,
                'status' => 1,
                'created_at' => date('Y-m-d H:i:s')
            ]);
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            // 失败回补Redis
            Redis::incr("seckill:stock:{$this->product_id}");
            Redis::srem("seckill:uid:{$this->product_id}", $this->user_id);
            throw $e;
        }
    }
}

七、生产级加固(必做)

7.1 限流防刷

  • 注解 / 中间件:用户 / IP 维度限流
  • 黑名单:恶意请求直接拦截

7.2 超时未支付回补

定时任务:15 分钟未支付,释放库存:

php

运行

复制代码
// 取消订单
Redis::srem("seckill:uid:{$product_id}", $user_id);
Redis::incr("seckill:stock:{$product_id}", 1);

7.3 一致性补偿

定时任务对比 Redis 库存与 DB 库存,差异自动修复。

7.4 安全加固

  • 请求鉴权、参数签名
  • 商品上下线状态控制
  • 前端按钮防抖 + 排队提示

八、压测与调优

8.1 压测指标

  • 单机 QPS:5000--10000+(4 核 8G)
  • 接口响应:<10ms
  • 无超卖、无重复单

8.2 调优项

  • Redis:关闭持久化 / 使用集群 / 开启 pipeline
  • Hyperf:worker_num=CPU 核数 * 2,协程池大小合理
  • 队列:提高并发,开启多消费
  • DB:分库分表、订单表索引、读写分离

九、总结

这套方案用协程高吞吐 + Lua 原子性 + 异步削峰,完美解决秒杀三大痛点:

  1. 超卖:Lua 单原子扣减
  2. 慢响应:Redis 预扣 + 异步落库
  3. 雪崩:限流 + 队列 + 连接池

可直接用于电商大促、优惠券、抢票等场景,按需扩展分布式锁、Sentinel 熔断、Redis Stream 等即可支撑更大流量。

相关推荐
名字还没想好☜3 天前
用 Redis + Redisson 实现分布式锁:从踩坑到生产可用
java·redis·分布式·junit·分布式锁·redisson
ipad协议源码3 天前
为何网站会出现的挂马?
junit·软件开发·开源源码
再玩一会儿看代码4 天前
JUnit 测试框架详解:从实际开发、业务测试到 Java 面试高频问题
java·经验分享·笔记·junit·面试
童话的守望者5 天前
BookHub (RWCTF 2018) 通关
junit
techdashen8 天前
Uber 如何完成超大规模 JUnit 迁移:从 JUnit 4 到 JUnit 5 的自动化工程实践
junit·sqlserver·自动化
会周易的程序员10 天前
使用 LuaBridge 封装 C++ 日志库 microLog 为 Lua 模块
c++·物联网·junit·lua·日志·iot·aiot
欢呼的太阳11 天前
在上一篇随笔中介绍了四种编程语言。这次再介绍四种编程语言:Fortran、Lua、Lisp 和 Logo。
junit·lua·lisp
Full Stack Developme11 天前
SpringBoot JUnit 教程
数据库·spring boot·spring·junit
殳翰12 天前
spring对junit的支持
spring·junit·sqlserver