ThinkPHP 8队列深度实战:用事件与Redis驱动优雅地解耦订单异步任务

2026-07-15 0 346

前段时间给一个小型电商系统做性能优化,发现用户提交订单后的响应时间经常超过3秒。分析之后看到,订单控制器在完成数据库写入后又同步调用了「库存扣减通知」「短信发送」「数据统计更新」等好几个耗时操作。用户那边等得着急,服务器资源也被拖累。大家第一反应可能是把这些操作改成异步执行,但问题在于这些操作散落在控制器各处,直接硬改成异步会让代码更难维护。于是我们想到了用ThinkPHP 8内置的队列系统搭配事件机制,把订单创建和后续的异步任务彻底解耦,响应时间直接从3秒降到了300毫秒以内。下面就复盘一下这个改造过程。

一、为什么是队列加事件,而不是直接在控制器里dispatch

ThinkPHP 8的队列组件本身已经很好用,可以在控制器里直接调用Queue::push()把任务推入队列。但是这么做有一个缺点:订单控制器需要明确知道“创建订单后要做哪些异步任务”,这违反了单一职责原则。当后续新增一个异步任务(比如同步到第三方ERP)时,我们还得改订单控制器。

更好的方式是引入事件系统:在订单模型或服务层触发一个OrderCreated事件,然后多个监听器各自决定是否需要推送到队列。这样订单核心逻辑与扩展操作完全隔离,新增任务只需要添加一个监听器,无需改动订单创建代码。TP8的事件和队列结合得相当流畅,我们一步步来。

二、前期准备:队列配置与Redis驱动

首先确保安装了队列扩展,ThinkPHP 8使用topthink/think-queue,可以在项目根目录执行:

composer require topthink/think-queue

然后配置队列的驱动,这里使用Redis,因为它的性能和可扩展性足够应对中小型系统。在config/queue.php中设置:

<?php
return [
    'default' => 'redis',
    'connections' => [
        'redis' => [
            'type' => 'redis',
            'queue' => 'default',
            'host' => env('redis.host', '127.0.0.1'),
            'port' => env('redis.port', 6379),
            'password' => env('redis.password', ''),
            'select' => env('redis.select', 0),
            'timeout' => 0,
            'persistent' => false,
        ],
    ],
];

注意这里的queue参数可以指定不同的任务队列名,方便区分优先级。确保Redis服务已启动,队列消费者稍后配置。

三、创建订单事件与监听器

按照TP8的规范,事件类放在app/event目录下。新建OrderCreated.php,目的是携带订单数据:

<?php
namespace appevent;

class OrderCreated
{
    public function __construct(
        public readonly int    $orderId,
        public readonly string $phone,
        public readonly float  $amount
    ) {}
}

接下来编写监听器。假设我们有两个异步任务:发送短信和更新统计报表。它们的监听器分别放在app/listener目录下。第一个监听器SendOrderSms.php

<?php
namespace applistener;

use appeventOrderCreated;
use appjobSendSmsJob;
use thinkfacadeQueue;

class SendOrderSms
{
    public function handle(OrderCreated $event): void
    {
        // 将任务推入队列,延迟5秒执行,给数据库同步留出时间
        Queue::later(5, SendSmsJob::class, [
            'phone' => $event->phone,
            'orderId' => $event->orderId,
        ], 'sms');
    }
}

第二个监听器UpdateStatistics.php,用于更新订单统计:

<?php
namespace applistener;

use appeventOrderCreated;
use appjobUpdateReportJob;
use thinkfacadeQueue;

class UpdateStatistics
{
    public function handle(OrderCreated $event): void
    {
        Queue::push(UpdateReportJob::class, [
            'orderId' => $event->orderId,
            'amount' => $event->amount,
        ], 'reports');
    }
}

然后在app/event.php中绑定事件与监听器:

<?php
return [
    'bind' => [
        appeventOrderCreated::class => [
            applistenerSendOrderSms::class,
            applistenerUpdateStatistics::class,
        ],
    ],
];

这样订单事件一触发,两个监听器就会各自把对应的任务推入不同的队列。

四、编写队列任务的具体逻辑

任务类必须实现thinkqueueJob接口,通常继承thinkqueueJob基类。我们在app/job目录下创建SendSmsJob.php

<?php
namespace appjob;

use thinkfacadeLog;
use thinkqueueJob;

class SendSmsJob
{
    public function fire(Job $job, array $data): void
    {
        $phone   = $data['phone'] ?? '';
        $orderId = $data['orderId'] ?? 0;
        if (empty($phone) || $orderId delete();
            return;
        }
        // 模拟短信发送逻辑
        try {
            $result = $this->sendSms($phone, "您的订单{$orderId}已创建成功");
            if ($result) {
                $job->delete(); // 成功则删除任务
                Log::info("短信发送成功: orderId={$orderId}");
            } else {
                // 失败重试,最多尝试3次
                if ($job->attempts() > 3) {
                    $job->delete();
                    Log::error("短信发送失败,已达最大重试次数: orderId={$orderId}");
                } else {
                    $job->release(10); // 10秒后重试
                }
            }
        } catch (Exception $e) {
            Log::error("短信任务异常: " . $e->getMessage());
            if ($job->attempts() > 3) {
                $job->delete();
            } else {
                $job->release(10);
            }
        }
    }

    private function sendSms(string $phone, string $message): bool
    {
        // 实际对接短信服务商API
        return true;
    }
}

注意fire方法是队列消费者的入口,$data就是从监听器推入的任务数据。通过$job->attempts()获取重试次数,$job->release()重新放回队列。另一个统计任务UpdateReportJob.php类似:

<?php
namespace appjob;

use thinkfacadeDb;
use thinkfacadeLog;
use thinkqueueJob;

class UpdateReportJob
{
    public function fire(Job $job, array $data): void
    {
        $orderId = $data['orderId'] ?? 0;
        $amount  = $data['amount'] ?? 0;
        if ($orderId delete();
            return;
        }
        try {
            // 使用事务更新统计表,例如 orders_summary
            Db::transaction(function () use ($amount) {
                Db::table('orders_summary')
                    ->where('date', date('Y-m-d'))
                    ->inc('total_amount', $amount)
                    ->inc('order_count', 1)
                    ->update();
            });
            $job->delete();
        } catch (Exception $e) {
            Log::error("统计更新失败: " . $e->getMessage());
            if ($job->attempts() > 3) {
                $job->delete();
            } else {
                $job->release(30);
            }
        }
    }
}

把耗时的短信发送和统计更新都放到后台队列处理,订单接口的负担就轻了许多。

五、在订单控制器中触发事件

订单创建成功后,我们只需要触发事件,而无需知道后续有多少个任务。控制器示例:

<?php
namespace appcontroller;

use appeventOrderCreated;
use appmodelOrder as OrderModel;
use thinkfacadeEvent;

class Order
{
    public function create()
    {
        // 接收请求参数,省略校验
        $phone  = request()->param('phone');
        $amount = request()->param('amount');
        // 订单入库
        $order = OrderModel::create([
            'phone'  => $phone,
            'amount' => $amount,
            'status' => 1,
        ]);
        // 触发事件
        Event::dispatch(new OrderCreated($order->id, $phone, (float)$amount));
        return json(['code' => 200, 'msg' => '订单创建成功', 'order_id' => $order->id]);
    }
}

这段代码纯粹聚焦订单本身的创建,异步任务通过事件扩展出去,后续要增加新的任务只需新增监听器并在event.php里绑定即可,订单控制器完全不用动。

六、启动队列消费者与守护进程

队列任务被推入Redis后需要消费者来处理。ThinkPHP提供了命令行工具:

php think queue:listen --queue=sms,reports --delay=5 --sleep=3 --tries=3

参数解释:--queue指定监听的队列名称,多个用逗号分隔;--delay任务失败后的延迟重试秒数;--sleep队列无任务时的休眠秒数;--tries最大尝试次数。开发环境可以直接运行,生产环境建议使用Supervisor或systemd把queue:work作为常驻进程守护,避免因为异常退出而停止消费。

一个简单的Supervisor配置示例(在服务器上):

[program:thinkphp-queue]
command=/usr/bin/php /path/to/your/project/think queue:work --queue=sms,reports --delay=5 --sleep=3 --tries=3
directory=/path/to/your/project
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/thinkphp-queue.log

七、实际性能对比与一些踩坑记录

改造前,订单创建接口的平均响应时间在2.8秒左右,压力测试下CPU和Redis的连接数较高。改造后,接口响应时间稳定在230毫秒左右,因为控制器只做了数据库写入和事件分发(事件内的队列推入操作非常快)。短信发送和统计更新的压力被分散到消费者进程,不再阻塞HTTP请求。

过程中也遇到几个问题:首先是Redis连接超时,原因是消费者长时间空闲后连接被断开,后来设置了心跳和persistent参数。其次是任务数据序列化问题,Queue::later传入的任务数据如果包含对象会序列化失败,确认所有数据都是标量和数组后解决。还有一点,多个队列消费者共享Redis时,务必给不同的业务使用不同的队列名,以免互相干扰。

另外,ThinkPHP 8的队列组件默认使用topthink/think-queue,底层支持Redis、Database等驱动。如果项目后期任务量暴增,可以考虑换成RabbitMQ驱动,但改配置即可,任务代码无需变化。

ThinkPHP 8队列深度实战:用事件与Redis驱动优雅地解耦订单异步任务
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 thinkphp ThinkPHP 8队列深度实战:用事件与Redis驱动优雅地解耦订单异步任务 https://www.taomawang.com/server/thinkphp/2366.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务