Loading

php Hyperf框架如何使用异步消息队列组件:hyperf/async-queue

1. 概述

Async-Queue 是 Hyperf 提供的异步消息队列组件,基于 Redis 实现,支持消息的延迟投递和失败重试。

2. 安装

composer require hyperf/async-queue

3. 配置

3.1 配置文件 config/autoload/async_queue.php

<?php

declare(strict_types=1);

return [
    'default' => [
        'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class,
        'redis' => [
            'pool' => 'default',
        ],
        'channel' => 'async-queue',
        'timeout' => 2,
        'retry_seconds' => 5,
        'handle_timeout' => 10,
        'processes' => 1,
        'concurrent' => [
            'limit' => 10,
        ],
    ],
];

3.2 可选:自定义连接池

<?php

declare(strict_types=1);

return [
    'default' => [
        'host' => env('REDIS_HOST', 'localhost'),
        'auth' => env('REDIS_AUTH', null),
        'port' => (int) env('REDIS_PORT', 6379),
        'db' => (int) env('REDIS_DB', 0),
        'pool' => [
            'min_connections' => 1,
            'max_connections' => 10,
            'connect_timeout' => 3.0, // 减少连接超时时间,避免长时间等待
            'wait_timeout' => 2.0, // 减少等待超时时间
            'heartbeat' => 30, // 添加心跳检测,每30秒检查连接
            'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60),
        ],
    ],
];

4. 创建 Job 类

<?php

declare(strict_types=1);

namespace App\Job;

use App\Service\MailService;
use Hyperf\AsyncQueue\Job;
use Hyperf\Context\ApplicationContext;
use Hyperf\Di\Annotation\Inject;

/**
 * 邮件发送队列任务
 */
class SendEmailJob extends Job
{
    /**
     * @var array 任务数据
     */
    protected $data;
    
    /**
     * @var MailService 邮件服务实例
     */
    #[Inject(MailService::class)]
    protected MailService $mailService;
    
    /**
     * @param array $data 任务数据
     */
    public function __construct(array $data)
    {
        $this->data = $data;
    }
    
    /**
     * 序列化时调用
     */
    public function __sleep()
    {
        // 只序列化 data 属性,不序列化 mailService 属性
        return ['data'];
    }
    
    /**
     * 反序列化时调用
     */
    public function __wakeup()
    {
        // 反序列化后,通过容器重新获取 mailService 实例
        $container = ApplicationContext::getContainer();
        $this->mailService = $container->get(MailService::class);
    }
    
    /**
     * 执行任务
     */
    public function handle()
    {
        // 记录任务开始日志
        $startMessage = '邮件发送队列任务开始: ' . json_encode($this->data) . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
        try {
            file_put_contents(__DIR__ . '/../../runtime/logs/email_job_start.log', $startMessage, FILE_APPEND);
        } catch (\Throwable $logError) {
            // 忽略日志记录错误
        }
        
        try {
            $to = $this->data['to'] ?? '';
            $attachments = $this->data['attachments'] ?? [];
            
            if (empty($to)) {
                $emptyMessage = '邮件发送队列任务失败: 收件人邮箱为空 - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
                try {
                    file_put_contents(__DIR__ . '/../../runtime/logs/email_job_error.log', $emptyMessage, FILE_APPEND);
                } catch (\Throwable $logError) {
                    // 忽略日志记录错误
                }
                return;
            }
            
            // 记录发送邮件开始
            $sendStartMessage = '开始发送邮件: to=' . $to . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            try {
                file_put_contents(__DIR__ . '/../../runtime/logs/email_job_debug.log', $sendStartMessage, FILE_APPEND);
            } catch (\Throwable $logError) {
                // 忽略日志记录错误
            }
            
            // 发送邮件
            $result = $this->mailService->send($to, $attachments);
            
            // 记录发送邮件结果
            $sendResultMessage = '邮件发送结果: ' . ($result ? '成功' : '失败') . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            try {
                file_put_contents(__DIR__ . '/../../runtime/logs/email_job_debug.log', $sendResultMessage, FILE_APPEND);
            } catch (\Throwable $logError) {
                // 忽略日志记录错误
            }
            
            // 记录成功日志
            $successMessage = '邮件发送成功: to=' . $to . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            try {
                file_put_contents(__DIR__ . '/../../runtime/logs/email_job_success.log', $successMessage, FILE_APPEND);
            } catch (\Throwable $logError) {
                // 忽略日志记录错误
            }
        } catch (\Exception $e) {
            // 记录错误日志
            $errorMessage = '邮件发送队列任务失败: ' . $e->getMessage() . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            try {
                file_put_contents(__DIR__ . '/../../runtime/logs/email_job_error.log', $errorMessage, FILE_APPEND);
            } catch (\Throwable $logError) {
                // 忽略日志记录错误
            }
            
            // 记录异常详情
            $exceptionMessage = '异常详情: ' . $e->getTraceAsString() . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            try {
                file_put_contents(__DIR__ . '/../../runtime/logs/email_job_exception.log', $exceptionMessage, FILE_APPEND);
            } catch (\Throwable $logError) {
                // 忽略日志记录错误
            }
        } catch (\Throwable $e) {
            // 捕获所有其他 Throwable
            $errorMessage = '邮件发送队列任务失败: ' . $e->getMessage() . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            try {
                file_put_contents(__DIR__ . '/../../runtime/logs/email_job_error.log', $errorMessage, FILE_APPEND);
            } catch (\Throwable $logError) {
                // 忽略日志记录错误
            }
            
            // 记录异常详情
            $exceptionMessage = '异常详情: ' . $e->getTraceAsString() . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            try {
                file_put_contents(__DIR__ . '/../../runtime/logs/email_job_exception.log', $exceptionMessage, FILE_APPEND);
            } catch (\Throwable $logError) {
                // 忽略日志记录错误
            }
        }
    }
}

5. 投递消息

<?php

declare(strict_types=1);

namespace App\Service;

use App\Job\SendEmailJob;
use Hyperf\AsyncQueue\Driver\DriverFactory;
use Hyperf\Di\Annotation\Inject;

/**
 * 邮件队列服务
 */
class EmailQueueService
{
    #[Inject(DriverFactory::class)]
    protected DriverFactory $driverFactory;
    
    /**
     * 将邮件发送任务加入队列
     * 
     * @param string $to 收件人邮箱
     * @param array $attachments 附件数组
     * @return bool 是否成功加入队列
     */
    public function push(string $to, array $attachments = []): bool
    {
        // 记录任务推送开始日志
        $startMessage = '邮件队列推送开始: to=' . $to . ', attachments=' . json_encode($attachments) . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
        try {
            file_put_contents(__DIR__ . '/../../runtime/logs/email_queue.log', $startMessage, FILE_APPEND);
        } catch (\Throwable $logError) {
            // 忽略日志记录错误
        }
        
        try {
            $driver = $this->driverFactory->get('default');
            $job = new SendEmailJob([
                'to' => $to,
                'attachments' => $attachments
            ]);
            
            // 记录创建任务
            $jobMessage = '创建SendEmailJob任务: ' . json_encode(['to' => $to, 'attachments' => $attachments]) . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            file_put_contents(__DIR__ . '/../../runtime/logs/email_queue.log', $jobMessage, FILE_APPEND);
            
            // 投递任务到队列,延迟0秒执行
            $result = $driver->push($job, 0);
            
            // 记录任务推送成功
            $successMessage = '邮件队列推送成功: to=' . $to . ', result=' . var_export($result, true) . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            file_put_contents(__DIR__ . '/../../runtime/logs/email_queue.log', $successMessage, FILE_APPEND);
            
            return true;
        } catch (\Exception $e) {
            // 记录详细错误日志
            $errorMessage = '邮件队列推送失败: ' . $e->getMessage() . '\n堆栈信息: ' . $e->getTraceAsString() . ' - 时间: ' . date('Y-m-d H:i:s') . PHP_EOL;
            try {
                file_put_contents(__DIR__ . '/../../runtime/logs/email_queue_error.log', $errorMessage, FILE_APPEND);
            } catch (\Throwable $logError) {
                // 忽略日志记录错误
            }
            
            return false;
        }
    }
}

6. 失败处理(失败事件监听)

<?php

declare(strict_types=1);

namespace App\Listener;

use Hyperf\AsyncQueue\Event\FailedHandle;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;

#[Listener]
class QueueFailedListener implements ListenerInterface
{
    public function listen(): array
    {
        return [
            FailedHandle::class,
        ];
    }
    
    public function process(object $event): void
    {
        $message = $event->getMessage();
        $throwable = $event->getThrowable();
        
        // 记录失败日志
        $logger = make(\Psr\Log\LoggerInterface::class);
        $logger->error('Queue job failed: ' . $throwable->getMessage(), [
            'job' => get_class($message->job()),
            'data' => $message->job()->params ?? null,
            'attempts' => $message->getAttempts(),
        ]);
        
        // 可以发送通知、存入数据库等
        // $this->notifyAdmin($message, $throwable);
    }
}

7. 启动消费者进程

# 启动所有队列的消费者
php bin/hyperf.php queue:listen

# 启动指定队列的消费者
php bin/hyperf.php queue:listen --queue=default

# 指定进程数
php bin/hyperf.php queue:listen --processes=2

8.守护进程配置

8.1 在 config/autoload/processes.php 中配置:

<?php

return [
    Hyperf\AsyncQueue\Process\ConsumerProcess::class,
];

8.2 自定义进程数

<?php

declare(strict_types=1);

return [
    // 启动 2 个消费者进程
    [
        'class' => Hyperf\AsyncQueue\Process\ConsumerProcess::class,
        'nums' => 2,
        'queue' => 'default',
    ],
    // 另一个队列的消费者
    [
        'class' => Hyperf\AsyncQueue\Process\ConsumerProcess::class,
        'nums' => 1,
        'queue' => 'another',
    ],
];

9. 队列监控

public function queueStatus()
{
    $driver = $this->driverFactory->get('default');
    
    $status = $driver->info();
    
    // 获取等待中的任务数
    $waiting = $status['waiting'] ?? 0;
    
    // 获取延迟中的任务数
    $delayed = $status['delayed'] ?? 0;
    
    // 获取失败的任务数
    $failed = $status['failed'] ?? 0;
    
    // 获取超时的任务数
    $timeout = $status['timeout'] ?? 0;
    
    return [
        'waiting' => $waiting,
        'delayed' => $delayed,
        'failed' => $failed,
        'timeout' => $timeout,
    ];
}

10. 注意事项

  1. 序列化限制:Job 中的属性必须可序列化

  2. 内存管理:避免在 Job 中保存大量数据

  3. 超时设置:根据任务复杂度合理设置超时时间

  4. 重试策略:合理设置最大重试次数,避免无限重试

  5. 监控报警:建议监控队列长度和失败任务数

  6. 幂等性:确保 Job 的 handle 方法是幂等的

11. 常见问题

11.1 Job 不执行

  • 检查消费者进程是否启动

  • 检查 Redis 连接配置

  • 查看日志文件 runtime/logs/hyperf.log

11.2 内存泄漏

  • 定期重启消费者进程

  • 使用 queue:listen 命令而不是常驻进程

11.3 性能优化

  • 根据业务量调整进程数

  • 使用连接池管理 Redis 连接

  • 批量处理相关任务

 

posted @ 2026-01-27 14:49  只要AD钙奶  阅读(0)  评论(0)    收藏  举报