主题
25 · Cron 定时任务
目标:用
robfig/cron跑通周期任务,并说清「单机 cron」与「分布式定时」边界。
前置:24 · Asynq · 06 · 并发
官方:robfig/cron · 对照 Node:node-cron/setInterval(进程内)
1. 三类「定时」别混
| 类型 | 代表 | 适用 |
|---|---|---|
| 进程内 ticker | time.Ticker / setInterval | 单实例、可丢、重启即断 |
| 库级 Cron | robfig/cron | 单机或明确「只跑一个副本」 |
| 分布式调度 | Asynq Scheduler / XXL-JOB / K8s CronJob | 多副本不重复执行 |
Node/Journal 印象:Koa 里 setInterval 清缓存——多 Pod 会跑多份。本篇先把单机 cron 写对,再谈防重。
2. 最小可跑 demo
bash
go get github.com/robfig/cron/v31
go
c := cron.New(cron.WithSeconds()) // 可选秒级;默认五段也可
_, err := c.AddFunc("0 */5 * * * *", func() {
log.Println("every 5 minutes: cleanup drafts")
})
fail(err)
_, err = c.AddFunc("0 0 3 * * *", func() {
log.Println("daily 03:00: aggregate hot notes")
})
fail(err)
c.Start()
defer c.Stop() // 优雅停:等正在跑的任务(视版本/选项)
select {} // 或接入主进程 signal1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
带依赖注入时,用 AddJob + 实现 cron.Job 接口,便于单测替换。
3. 表达式速查(六段:秒 分 时 日 月 周)
| 表达式 | 含义 |
|---|---|
0 */1 * * * * | 每分钟 |
0 0 */1 * * * | 每小时整点 |
0 30 9 * * 1-5 | 工作日 09:30 |
0 0 0 1 * * | 每月 1 号 0 点 |
时区:cron.WithLocation(loc);默认机器本地时区,容器里务必钉 TZ。
4. 多副本防重(入门)
单机 cron + 多实例 = 重复执行。任选一种心智:
text
方案 A:部署上保证只有 1 个「scheduler」副本
方案 B:任务开头 Redis SetNX lock:cron:cleanup TTL=…
方案 C:只负责 Enqueue,真正干活交给 Asynq Worker(推荐)
方案 D:K8s CronJob(32 篇)1
2
3
4
2
3
4
推荐路径:Cron 只入队 → Asynq 执行(与 24 串联)。
go
_, _ = c.AddFunc("0 0 3 * * *", func() {
task, _ := NewHotAggTask()
_, _ = asynqClient.Enqueue(task)
})1
2
3
4
2
3
4
5. 动手清单
- [ ] 每 10 秒打一条日志(验证表达式)
- [ ]
SIGINT后Stop()不留僵尸 goroutine - [ ] 模拟两进程:无锁会双跑;加 SetNX 只跑一次
- [ ] 写清:本练手仓用「单副本」还是「入队」
- [ ] 对照:为何生产不把重活塞进 cron 回调里同步跑
6. 项目驱动
| 场景 | 建议 |
|---|---|
| 清理过期草稿 / 临时文件 | 夜间 cron → Asynq |
| 热门榜重算 | 定时入队;结果写 Redis |
| 对账 / 报表 | 失败要可观测(30/31) |
| Journal 对照 | 勿在小程序端「定时」;在后端 |
7. 常见坑 + AI 审查
| 坑 | 说明 |
|---|---|
| 回调里跑 10 分钟重活 | 阻塞同一调度线程;拆 Worker |
| 忘记时区 | 云主机 UTC vs 业务「北京时间」 |
| 五段/六段混用 | WithSeconds 与表达式段数一致 |
AI 用 for { sleep } 冒充 cron | 无漂移管理、难运维 |
| 多副本无锁 | 简历翻车点 |
8. 下一篇
文件与导出落盘:
→ 26 · MinIO 对象存储
