打开 cron_schedule,看到同一个 job_code 连续几十行,很容易判断为“Cron 重复生成”。先别删表。Magento 会提前为未来时间窗口创建 schedule,同一任务按分钟运行时,本来就会有多条 pending 记录。真正需要处理的是同一计划时间被重复执行,或业务结果被重复写入。

用时间字段区分正常排程

SELECT schedule_id, job_code, status, created_at,
       scheduled_at, executed_at, finished_at, messages
FROM cron_schedule
WHERE job_code = 'vendor_sync_orders'
ORDER BY scheduled_at DESC
LIMIT 40;

scheduled_at 每分钟递增、状态分布为 pending/success,通常正常。两个行具有不同 schedule_id 但完全相同的 scheduled_at,需要再看它们是否真的都进入 running。若只是一条 success、一条 missed,往往是部署或系统 Cron 在临界时间切换造成。

确认操作系统有没有启动两套入口

常见事故是 crontab 和 systemd timer 同时运行,或者旧 release 与新 release 各保留一条命令。分别检查运行用户的 crontab、系统级 cron、容器定时任务与平台调度器:

crontab -l
systemctl list-timers --all
ps -ef | grep '[m]agento cron:run'
grep -R 'cron:run' /etc/cron.d /etc/crontab 2>/dev/null

Magento 的 Cron 组会使用锁防止同组不安全地并行调度,但锁依赖正确的部署配置与共享资源。多节点分别使用本地文件锁、容器时钟偏差较大、任务运行时间超过锁预期,都可能造成争用。

不要忽略 cron group 配置

cron_groups.xml 中的 schedule_generate_everyschedule_ahead_forschedule_lifetime 决定多久生成一次、提前生成多远、迟到多久算 missed。把 ahead_for 设得很大只会制造更多未来记录,不会提高可靠性。把 lifetime 设得过短,机器短暂繁忙就会产生大量 missed。

如果自定义 job 使用单独 group,系统 Cron 必须实际运行这个组。只配置 default 与 consumers,新增 group 可能永远 pending;为“补救”而再加一条宽泛的 cron:run,又会制造并发入口。

业务重复不等于调度重复

即使 schedule 只运行一次,任务也可能在调用外部系统成功后、本地标记前崩溃。下一次重试会再次发送订单或开票。因此同步任务必须有幂等键,例如 Magento 订单实体 ID 加业务动作,并在数据库中以唯一索引保护。

CREATE UNIQUE INDEX UNQ_VENDOR_EXPORT_ACTION
ON vendor_export_log (entity_id, action_code);

不要直接在生产库执行示例索引。应先检查已有重复数据、字段长度和部署声明,再通过模块 schema patch 发布。外部 API 也支持幂等键时,两端都要使用同一业务键。

卡死任务的判断

大量 running 且没有 finished_at,可能是 PHP 进程被杀、数据库连接中断或代码没有捕获致命错误。对照系统进程与任务执行时间;确认进程已不存在后,才考虑把陈旧记录改为 error,并修复根因。直接把所有 running 更新为 success 会掩盖未完成业务,直接 truncate 则失去诊断证据。

最终应回答三个问题:是否存在两个系统级入口;相同 scheduled_at 是否被两个进程领取;业务动作是否有幂等保护。只有第三个问题也解决,才不会在重试、部署和网络抖动时再次出现“重复任务”的表象。