必须用cron.new(cron.withseconds()),因默认cron.new()仅支持5字段(分时日月周),不支持秒级精度;加withseconds()后启用6字段表达式(如"/10 "表示每10秒执行),否则6字段表达式会报错“expected exactly 5 fields, found 6”。

直接用 robfig/cron/v3 就行,别自己套 time.Ticker 写轮询逻辑——它不支持 cron 表达式、重启即丢任务、没 panic 捕获、也没并发控制,生产环境一跑就出问题。
为什么必须用 cron.New(cron.WithSeconds()) 而不是默认构造
默认的 cron.New() 只解析「分 时 日 月 周」5 字段(如 "0 */2 * * *"),不支持秒级精度。而多数后台任务(比如每10秒拉一次第三方状态、每30秒检查一次队列积压)都需要秒级触发。
- 加
cron.WithSeconds()后,表达式变成6字段,例如"*/10 * * * * *"表示“每10秒执行一次” - 不加这个选项,
"*/10 * * * * *"会直接报错:expected exactly 5 fields, found 6 - 如果你的任务确实只需要分钟级(比如每天凌晨2点归档),那可以不用
WithSeconds(),但得确保所有表达式严格是5字段
AddFunc 和 AddJob 选哪个?业务逻辑复杂时必须用 AddJob
AddFunc 接收一个无参函数,适合简单打印、发通知这类轻量操作;一旦任务要传参、要访问数据库连接池、要带 context 控制超时,就必须封装成结构体实现 cron.Job 接口。
-
AddFunc示例:c.AddFunc("0 2 * * *", func() { log.Println("daily cleanup") })—— 没法传*sql.DB或context.Context -
AddJob要求结构体有Run()方法,你可以把依赖全塞进结构体字段里:type ArchiveJob struct { db *sql.DB; timeout time.Duration },然后在Run()里用ctx, cancel := context.WithTimeout(context.Background(), j.timeout) - 漏掉 context 超时控制,归档任务卡住会导致整个 cron 调度器线程阻塞,后续所有任务全延迟
多实例部署时任务重复执行?robfig/cron/v3 本身不解决这个问题
robfig/cron/v3 是单机调度器,没有分布式锁机制。如果你的 Gin 服务起了3个 Pod,每个都会独立执行 "0 2 * * *" 的归档任务——结果就是同一份数据被删三次,或报表生成三份。
- 临时缓解:用环境变量或配置中心控制只让某台实例启用定时任务,比如
if os.Getenv("ENABLE_CRON") == "true" { c.Start() } - 长期方案:换
go-quartz(原生支持分布式调度)或接入xxl-job-executor-go,由中心调度器统一分配执行节点 - 千万别用 Redis SETNX 自己写锁——cron 任务启动时机不可控,容易出现锁残留或误释放
panic 导致整个调度器停摆?必须加 cron.Recover 链
一个未捕获的 panic(比如数据库连接失败、空指针解引用)会让当前 goroutine 退出,而 robfig/cron/v3 默认不 recover,后果是该任务永久消失,且不会报错到日志。
- 正确初始化方式:
c := cron.New(cron.WithSeconds(), cron.WithChain(cron.Recover(cron.DefaultLogger))) - 这样即使
Run()函数 panic,也会被拦截并打日志,调度器继续运行下一个任务 - 如果用自定义 logger,注意它必须实现
cron.Logger接口,否则Recover链会静默失效
真正难的不是注册几个任务,而是让它们在服务滚动发布、实例扩缩容、数据库抖动时不堆积、不重复、不静默失败——这些都得靠调度器选项、任务封装方式和外部协调机制一起兜住,光靠 AddFunc + Start() 远远不够。











