hyperf 3.1 中需通过定时任务精准触发异步队列,确保 crontab 轻量无阻塞、注入容器获取 dispatcher;job 内可延迟自调度;须配置单例锁、并发限制、redis 防重及 handle_timeout 超时兜底。

在 Hyperf 3.1 中,让异步队列任务按固定节奏触发(比如每分钟拉一次第三方订单、每5秒检查一次支付状态),而不是靠外部请求或手动投递,需要把定时任务作为“触发器”,精准驱动队列投递。直接在 @Crontab 方法里 new Job() → push() 是常见做法,但若未控制并发、未设超时、未处理重复投递,极易导致任务堆积、重复消费或协程卡死。
定时任务触发异步队列投递
这一步是整个链路的起点,必须确保定时逻辑轻量、无阻塞、可重入。
第一步:在 app/Crontab/ 下新建 SyncOrderCrontab.php
第二步:使用标准注解声明,rule 设为 '*/1 * * * *' 表示每分钟执行一次
第三步:execute 方法内只做一件事——构造并投递 Job 实例,禁止任何 DB 查询、HTTP 调用或 sleep
第四步:调用 $this->container->get(JobDispatcher::class)->dispatch(new SyncOrderJob()),不要用静态方法或 new 实例
【务必注入容器获取 dispatcher,否则协程上下文丢失,Job 内无法正确解析依赖】
异步任务类中嵌套定时行为
某些场景下,单次队列任务需自行决定是否“再调度自己”,例如延迟重试失败订单、轮询等待第三方回调。这时不能依赖外部 Crontab,而要在 Job 执行逻辑中主动触发下一次投递。
方法一:使用 delay() 投递带延迟的新任务
在 handle() 结尾判断条件,满足则调用 $this->container->get(JobDispatcher::class)->delay(60)->dispatch(new self($this->orderId))
方法二:通过 Redis ZSet + 定时扫描实现可控延迟
将下次执行时间戳写入 ZADD order_delay_queue (time()+300) orderId,另起一个独立 Crontab 每秒扫描 ZRANGEBYSCORE order_delay_queue -inf (time()) 并批量 dispatch
注意:ZSet 方案需自行维护清理逻辑,避免过期数据无限堆积
防重+限频:避免定时触发引发雪崩
当定时任务频率高于队列消费能力,或多个 Worker 同时拉取同一任务时,就会出现重复执行、资源耗尽、Redis 连接打满等问题。
① 在 Crontab 类中启用单例锁:public array $singleton = ['limit' => 1];
② 配置 async-queue 的并发上限:'concurrent' => ['limit' => 3],该值必须 ≤ 单机 CPU 核数 × 1.5
③ 给每个投递的 Job 设置唯一 ID,并在 handle() 开头用 Redis SETNX 校验是否已执行过:if ($this->redis->set('job:sync_order:'.$this->orderId, 1, ['EX' => 3600, 'NX'])) { /* 执行逻辑 */ }
【SETNX 的 EX 时间必须大于任务最大执行时长,否则可能误判为未执行而重复触发】
超时兜底:防止单个任务拖垮整个队列
Hyperf 异步队列默认不设执行超时,一旦某个 Job 内部调用慢接口或陷入死循环,该协程将永久占用,后续任务全部排队等待。
第一步:在 config/autoload/async_queue.php 中设置 'handle_timeout' => 15
第二步:在 Job 的 handle() 方法开头启动协程超时控制:go(function () { Co::set(['timeout' => 10]); /* 实际业务逻辑 */ });
第三步:捕获超时异常并主动标记失败:try { /* 业务代码 */ } catch (Throwable $e) { if ($e instanceof TimeoutException) { throw new JobException('handle timeout'); } }
这一步操作起来很简单,直接把超时配置加进 config 就行,但漏掉 handle_timeout 就会导致整个协程池被个别慢任务锁死。











