hyperf 3.0 默认 redis 异步队列不支持任务优先级,需通过切换 amqp、自定义 redis zset/双 list 或协程抢占实现;其中 amqp 原生支持 priority(0–255),redis 双 list 方案需注意 brpop 阻塞与超时配置,协程抢占仅适用于 cpu 密集型任务。

Hyperf 3.0 默认异步队列不支持任务优先级,提交的 SendEmailJob、OrderProcessJob 等任务一律按插入顺序消费,高优订单超时关闭可能被低优日志归档任务阻塞,导致业务 SLA 失效。
确认当前队列驱动与限制
进入项目根目录,执行:
php bin/hyperf.php gen:publish async-queue
打开 config/autoload/async_queue.php,检查 'driver' 字段值——若为 Hyperf\AsyncQueue\Driver\RedisDriver,说明你用的是默认 Redis 驱动,它底层基于 LPUSH/BRPOP,【天然不支持优先级排序】,所有任务在 list 中严格 FIFO,改配置参数无法绕过此限制。
运行 php bin/hyperf.php list | grep queue,确认无 delay-queue 或 amqp 相关命令,排除已启用延时队列或 AMQP 的干扰场景。
方案一:切换至 AMQP(RabbitMQ)实现原生优先级
AMQP 协议本身支持 priority 字段,RabbitMQ 官方文档明确声明:priority 范围 0–255,Broker 会按数值降序投递(高数值优先)。
第一步:安装组件
composer require hyperf/amqp
第二步:修改 config/autoload/amqp.php,启用优先级支持:
在 default 连接配置中添加 'qos' => ['prefetch_size' => 0, 'prefetch_count' => 1, 'global' => false],并在 queues 数组中为对应队列显式声明 'arguments' => ['x-max-priority' => 255]
第三步:生产消息时注入 priority
在 Producer 类的 __construct 方法中,将 $this->properties = ['priority' => 10]; 加入消息属性(注意:必须是整数,且 ≤ x-max-priority 值);消费者无需改动,AMQP 扩展自动按 priority 取任务。
⚠️ 注意:RabbitMQ 优先级是“尽力而为”,非强保证。当高优消息到达时,若当前正在处理一个已拉取但未 ack 的低优消息,该低优消息会继续执行完再处理高优消息——这是协议层设计,无法规避。
方案二:自定义 Redis 优先级队列(兼容现有架构)
不引入新中间件,复用现有 Redis 连接池,在 async-queue 基础上替换底层数据结构和消费逻辑。
方法一:用 ZSET 替代 LIST,以 score 表达优先级
创建新类 App\Queue\PrioritizedRedisDriver,继承 Hyperf\AsyncQueue\Driver\RedisDriver;重写 put() 方法:不再 LPUSH,改为 ZADD queue_name (timestamp + priority_offset) json_task,其中 priority_offset = 1000000 - priority(让小数值对应大 score,ZREVRANGE 取最大 score 即最高优);重写 pop() 方法:用 ZREVRANGEBYSCORE queue_name +inf (timestamp_max_limit WITHSCORES LIMIT 0 1 获取最高优任务,再 ZREM 删除。
方法二:双 List 分级(轻量、免改数据结构)
在 Redis 中维护两个 list:high_priority_queue 和 default_queue;修改 consumer 启动逻辑:每次循环先 BRPOP high_priority_queue 0,仅当返回 nil 时才 BRPOP default_queue 0;提交任务时,根据业务标识(如 $job instanceof OrderTimeoutJob)选择 LPUSH 到对应 list。
这一步操作起来很简单,直接把 job 实例传给不同 channel 就行,但要注意:双 list 方案下,high_priority_queue 若长期为空,BRPOP 会阻塞整个消费者进程,必须确保 timeout=0 且连接 read_timeout ≥ 5,否则阻塞被中断会导致任务丢失。
方案三:协程内动态抢占(无存储依赖)
适用于任务生成后可预判优先级、且执行耗时可控的场景,例如风控拦截类任务。
① 在任务提交入口处,不调用 $container->get(AsyncQueue::class)->push(),而是启动一个独立协程:
go(function () use ($job, $priority) {
while (true) {
if ($priority > 50) {
co::sleep(0.001); // 主动让出,但比普通任务多抢 100 次调度机会
continue;
}
break;
}
$container->get(AsyncQueue::class)->push($job);
});
② 修改 consumer 的 task 协程池配置:在 config/autoload/process.php 中,将 AsyncQueueProcess 的 enable_coroutine 设为 true,并调大 concurrent.limit 至 20+,否则高优协程会让低优任务饿死。
③ 关键限制:此法仅对 CPU 密集型任务有效,若 $job 内部含 await I/O(如 DB 查询),co::sleep(0.001) 无法抢占——I/O 期间控制权已交还事件循环,优先级失效。











