Hyperf Crontab的onOneServer必须显式设为true才能启用跨节点Redis锁,否则多实例下任务重复执行;需配合mutex={"type":"redis"}、合理设置mutexExpires(大于任务最大耗时),并确保CrontabDispatcherProcess进程正确注册。

Hyperf Crontab 的 onOneServer 必须显式开启
默认情况下,onOneServer 是 false,哪怕你开了 singleton = true,在 Kubernetes 多副本或多个物理机部署时,每个实例都会独立执行任务。这不是“锁没生效”,而是根本没启用集群级互斥逻辑。
必须在任务类里明确设置:
/**
* @Crontab(
* name="sync_order_status",
* rule="*/30 * * * *",
* singleton=true,
* onOneServer=true,
* mutex={"type": "redis"}
* )
*/-
onOneServer=true才会触发跨进程/跨节点的 Redis 锁判定 -
mutex必须指定"type": "redis",且确保config/autoload/redis.php中已配置好对应连接池(默认用default池) - 若用
mutex={"type": "file"},在多 Pod 场景下完全无效——文件锁只对单机进程有效
Redis 锁过期时间 mutexExpires 要大于任务最长执行耗时
任务执行时间超过 mutexExpires(默认 60 秒),锁会提前释放,其他实例可能同时抢到锁,导致重复执行。这不是并发 bug,是配置失配。
例如一个清理日志的任务平均耗时 85 秒,但 mutexExpires 没改:
- 第 0 秒:Pod A 拿锁开始执行
- 第 60 秒:锁自动过期
- 第 62 秒:Pod B 成功加锁并启动同一任务
解决方式是在注解里显式延长:
@Crontab(
...
mutex={"type": "redis", "expires": 120}
)注意:expires 单位是秒,且必须 > 任务实际最大耗时 + 网络延迟余量(建议至少 +10 秒)。
任务类不能依赖未序列化的对象或资源句柄
Hyperf Crontab 在分布式锁校验和任务分发阶段,会尝试序列化任务上下文。如果 execute() 方法里隐式用了 $this->db 这类 PDO 实例、Closure、resource 或未标记 #[\Serializable] 的自定义对象,会导致:
- 锁判定失败(
serialize()报错静默跳过) - 任务在某实例上执行成功,另一实例因反序列化失败直接跳过,看起来像“有时执行有时不执行”
正确做法是:所有依赖通过容器注入,且只在 execute() 内部调用;避免在属性里存连接或资源:
// ❌ 错误:$pdo 是 resource,无法序列化
private $pdo;
<p>// ✅ 正确:每次执行时从容器取新实例
public function execute(): void {
$db = $this->container->get(\Hyperf\Database\Connection::class);
$db->table('logs')->where('created_at', '<', now()->subHour())->delete();
}K8s 环境下要确认 CrontabDispatcherProcess 真正运行了
很多人以为只要装了 hyperf/crontab、写了注解就完事,却忽略了进程注册这步。在 K8s 中,如果 config/autoload/processes.php 没配,或者配错成字符串而非类名,CrontabDispatcherProcess 根本不会启动,所有定时任务形同虚设。
检查方式:
- 启动后看日志是否有
[INFO] Crontab manager started with X tasks - 进容器执行
ps aux | grep crontab,应看到类似php bin/hyperf.php crontab:dispatch的进程 - 若没有,确认
processes.php返回的是数组,且含完整类名:
return [
\Hyperf\Crontab\Process\CrontabDispatcherProcess::class,
];漏掉 \ 或写成 'CrontabDispatcherProcess' 都会导致进程注册失败——这个点在滚动更新后最容易被忽略。


















