使用 Codex 给项目增加定时任务时,开发环境通常很简单。

例如每天凌晨生成一次报表:

cron.schedule("0 0 * * *", async () => {
  await generateDailyReport();
});

本地只有一个服务进程时,这段代码完全正常。

但真正部署到生产环境后,如果服务运行了:

instance-1
instance-2
instance-3

那么到了凌晨0点,三个实例都会执行同一个 Cron。

结果可能变成:

  • 日报生成了3份;

  • 同一批订单被同步3次;

  • 用户收到多封相同通知;

  • 数据清理任务重复运行;

  • 第三方接口被重复调用;

  • 一个实例还没执行完,另一个实例已经开始第二次处理;

  • 扩容以后任务执行次数突然增加。

这不是 Cron 本身有问题,而是单机定时任务被直接放进了多实例架构中


一、为什么本地永远发现不了?

本地通常只有:

1个Node进程

所以:

Cron触发一次
=
任务执行一次

生产环境为了高可用,往往部署:

3个
5个
甚至几十个实例

如果每个实例都有:

@Cron(...)

那么实际执行次数就是:

实例数量 × 1

也就是说:

扩容

会无意中变成:

增加定时任务执行次数

因此定时任务上线前一定要先问:

这个任务属于某个实例,还是属于整个系统?

绝大多数业务定时任务属于整个系统。


二、最简单的方法:使用分布式锁

可以在任务执行前先抢锁。

例如 Redis:

lock:daily-report:2026-08-14

伪代码:

const lockKey =
  "lock:daily-report:2026-08-14";

const acquired = await redis.set(
  lockKey,
  instanceId,
  {
    NX: true,
    EX: 600
  }
);

if (!acquired) {
  return;
}

await generateDailyReport();

三个实例同时触发:

instance-1 → 抢锁成功
instance-2 → 失败
instance-3 → 失败

最后只有:

instance-1

真正执行任务。


三、锁一定要设置过期时间

错误写法:

await redis.set(
  lockKey,
  instanceId,
  {
    NX: true
  }
);

如果抢到锁的实例突然崩溃:

锁永远存在

后续所有实例都会认为:

任务正在执行

然后这个定时任务可能永久停止。

因此分布式锁必须有:

TTL

例如:

预计任务运行2分钟
锁TTL设置10分钟

即使进程异常退出,锁最终也会自动释放。


四、TTL也不能随便设

如果任务正常需要:

15分钟

但锁只设置:

5分钟

就会发生:

instance-1获得锁
↓
任务执行中
↓
5分钟后锁过期
↓
instance-2重新获得锁
↓
同一个任务同时执行两份

所以锁时间必须覆盖合理的任务执行周期。

如果任务耗时不稳定,可以考虑:

锁续期

例如 Worker 每隔一段时间刷新 Lease。


五、释放锁时不能直接DEL

假设:

instance-A获得锁
TTL=30秒

A执行很慢。

30秒后锁过期。

然后:

instance-B

获得了新的锁。

此时 A 执行完毕:

await redis.del(lockKey);

它会把:

B正在持有的锁

删除。

这就会产生严重竞态。

因此锁应该带唯一值:

lock value =
instance-A + unique-token

释放时先判断:

当前锁仍然属于自己吗?

只有属于自己才能删除。

实际项目可以通过 Lua Script 保证“检查 + 删除”的原子性。


六、分布式锁不是业务幂等的替代品

即使分布式锁写得很好,也不能完全假设任务永远只执行一次。

例如:

任务业务执行成功
↓
还没记录完成状态
↓
服务崩溃
↓
锁最终过期
↓
任务再次执行

所以关键任务还需要幂等。

例如日报任务可以使用唯一键:

report_date = 2026-08-14

数据库:

UNIQUE(report_date)

再次执行时:

发现当天日报已存在
→ 不重复创建

更可靠的设计是:

分布式锁
降低重复执行概率

业务幂等
保证重复执行也不会产生错误结果

七、任务要有业务Execution ID

例如每日账单任务:

billing:2026-08-14

可以生成:

executionId =
daily-billing:2026-08-14

执行前查询:

SELECT *
FROM job_executions
WHERE execution_id = ?

如果状态已经:

completed

直接退出。

如果没有,则记录:

{
  "executionId": "daily-billing:2026-08-14",
  "status": "running"
}

成功后:

completed

失败:

failed

这样不仅防重复,还能留下完整执行记录。


八、不要只记录“任务跑过了”

一个生产级定时任务至少应该记录:

jobName
executionId
startedAt
finishedAt
status
duration
workerId
processedCount
failedCount
error

例如:

job: daily-report
executionId: daily-report:2026-08-14
workerId: api-03
duration: 128s
processed: 18250
failed: 3
status: completed_with_errors

这比日志中只出现:

cron finished

有用得多。


九、定时任务不要和Web服务强绑定

很多项目直接:

API服务启动
↓
Cron一起启动

这会导致一个问题:

API扩容
=
Cron实例增加

更清晰的方式是拆成:

Web Service

和:

Job Worker / Scheduler

例如:

api
worker
scheduler

API可以扩容10个实例。

Scheduler仍然只运行:

1个或受控数量

职责就清晰很多。


十、可以使用Leader Election

如果希望多个 Scheduler 实例保持高可用,但只有一个真正执行任务,可以使用 Leader Election。

例如:

scheduler-1
scheduler-2
scheduler-3

通过选举确定:

scheduler-2 = Leader

只有 Leader 负责:

触发定时任务

如果 Leader 崩溃:

重新选举
↓
scheduler-1成为新Leader

这样既能避免重复执行,又不会因为单个 Scheduler 故障导致定时任务彻底停止。


十一、Kubernetes环境要特别注意CronJob

如果项目已经运行在 Kubernetes 中,很多场景并不需要自己在应用里写 Cron。

可以直接使用:

Kubernetes CronJob

例如:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report

这样调度职责由 Kubernetes 管理。

同时可以设置:

concurrencyPolicy

例如:

concurrencyPolicy: Forbid

表示上一个任务还没执行完时,不允许启动新的任务。

对于纯离线任务,这种方式通常比“每个 API 实例都内置 Cron”更加清晰。


十二、要防止任务重叠执行

假设任务:

每5分钟执行一次

但某一次运行耗时:

8分钟

流程可能变成:

10:00 任务A开始

10:05 任务B开始

10:08 任务A结束

A和B之间重叠了3分钟。

如果处理的是:

订单同步
库存校准
报表聚合

就可能产生数据竞争。

所以定时任务还要明确:

上一轮没结束
下一轮怎么办?

常见策略:

跳过
排队
并发执行
终止旧任务

不能默认直接启动新一轮。


十三、不同任务不要共用一个大锁

错误做法:

lock:cron

所有任务抢同一个锁。

结果:

日报正在执行
↓
数据清理也不能执行
↓
同步任务也必须等待

即使它们完全没有冲突。

更合理:

lock:job:daily-report
lock:job:cleanup
lock:job:order-sync

如果还需要按日期:

lock:job:daily-report:2026-08-14

锁的粒度应该与业务冲突范围一致。


十四、定时任务失败后不要无限自动补跑

例如凌晨任务失败:

00:00失败

系统立即重试:

00:01
00:02
00:03
...

如果失败原因是:

第三方服务长时间不可用

可能持续制造压力。

可以采用:

最大重试次数
+
指数退避

例如:

第1次:1分钟
第2次:5分钟
第3次:15分钟

仍然失败则:

标记failed
+
告警

不要让一个失败 Cron 永久循环。


十五、时区是定时任务最容易忽略的问题之一

开发者写:

每天00:00运行

需要问:

哪个时区的00:00?

服务器可能运行在:

UTC

业务使用:

Asia/Shanghai

如果没有明确配置,就可能出现任务实际在:

北京时间08:00

执行。

尤其涉及:

日报
月结
账单
每日额度

时,必须明确业务时区。

不要直接依赖服务器系统默认时区。


十六、夏令时也可能导致任务多跑或少跑

部分地区存在 Daylight Saving Time。

例如某一天:

02:00

可能被跳过。

另一时期:

01:00

可能出现两次。

如果业务运行在存在夏令时的地区,就不能简单认为:

每天固定24小时

始终成立。

对于财务或结算任务,更适合基于:

业务日期

设计 Execution ID。

而不是只相信 Cron 触发次数。


十七、任务触发和任务执行可以分离

一个更稳定的架构是:

Scheduler
↓
创建任务消息
↓
Queue
↓
Worker执行

Scheduler只负责:

到时间了
→ 创建 job

Worker负责:

真正执行

例如:

每日00:00
↓
创建 daily-report:2026-08-14
↓
进入队列
↓
Worker消费

这样:

  • 任务可以重试;

  • 可以水平扩展Worker;

  • 可以记录状态;

  • 可以做失败队列;

  • Scheduler本身非常轻量。


十八、让Codex先分析部署拓扑

遇到定时任务重复执行时,可以先要求:

请先不要修改代码。

分析当前定时任务:

1. 当前生产环境有多少服务实例;
2. 每个实例是否都会注册Cron;
3. 同一个任务能否并发执行;
4. 是否已经存在分布式锁;
5. 任务是否支持幂等;
6. 服务扩容后任务次数是否会增加;
7. 是否更适合拆成独立Scheduler;
8. 当前部署环境是否已有CronJob能力。

很多问题并不在 Cron 表达式,而在部署结构。


十九、测试一定要模拟多实例

本地单实例测试无法发现这类问题。

应该模拟:

instance-A
instance-B
instance-C

同时触发相同任务。

最终验证:

业务结果只产生一次

还应该测试:

持锁实例崩溃

确认:

其他实例最终能够接管

锁TTL过期

确认:

不会误删新Owner的锁

同一Execution ID重复执行

确认:

业务结果仍然幂等

上一轮任务未完成

确认:

下一轮按预期跳过或排队

二十、把定时任务规则写进AGENTS.md

# Scheduled Job规则

- 多实例环境禁止默认每个实例独立执行同一Cron
- 系统级任务必须评估分布式锁或Leader Election
- 分布式锁必须设置TTL
- 释放锁必须验证锁Owner
- 核心任务必须支持业务幂等
- 每次运行必须有唯一executionId
- 上一轮未完成时必须定义下一轮策略
- 定时任务必须明确业务时区
- 长任务优先考虑Scheduler + Queue + Worker
- 修改定时任务后必须执行多实例并发测试

这样 Codex 在新增 Cron 时,就不会只关注:

几点触发

而会同时考虑:

到底由谁执行。

二十一、Plus还是Pro?

如果主要使用 Codex 处理:

  • 单机 Cron;

  • 普通后台任务;

  • 简单 Redis 分布式锁;

  • 中小型后端项目;

Plus 通常已经能够覆盖多数开发任务。

如果长期维护:

  • Kubernetes 多实例服务;

  • 大量 Scheduled Job;

  • Worker 集群;

  • 多服务任务调度;

  • 长时间日志和失败恢复分析;

则可以根据实际开发强度评估 Pro。

但版本不会自动解决任务重复执行。

真正重要的是部署上线前明确:

一个定时任务,在整个系统里到底应该同时存在几个执行者?

总结

Codex 写定时任务后,本地只执行一次,上线多实例却重复执行,本质上是因为每个服务实例都拥有自己的 Cron 调度器。

通过:

分布式锁
Leader Election
业务幂等
Execution ID
独立Scheduler

可以把任务执行权从“每个实例都有一份”收敛为“整个系统只认一个有效执行”。

真正可靠的定时任务系统,不只是到了时间能够运行。

它还必须能够回答:

谁执行?如果执行者挂了谁接管?重复触发怎么办?上一轮还没结束怎么办?

这几个问题设计清楚以后,多实例扩容才不会意外变成“任务执行倍增器”。

CSDN文章描述

本文介绍 Codex 编写定时任务后在多实例部署中出现重复执行的问题,并通过分布式锁、Leader Election、业务幂等、Execution ID 和独立 Scheduler,提高 Cron 任务的可靠性。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐