生产环境应避免使用 go 原生 time/ticker 和 robfig/cron v3/v4,因其不支持分布式协调;轻量场景可用 redis 锁手动防重;核心业务推荐 asynq 或 machinery(需 redis sentinel);etcd 因 lease 延迟高、watch 易丢事件,不适合作为高精度调度底座。

Go 里选 cron 还是 robfig/cron?别用原生 time/ticker
Go 标准库没有分布式 Cron 支持,time/ticker 只能本地单实例跑,微服务一扩缩容就丢任务。生产环境必须用支持持久化、去重、分片的方案。robfig/cron(v3)虽常用,但它是单机设计,没内置分布式协调能力——多个实例会重复触发同一任务。
实操建议:
- 直接跳过
robfig/cronv3;v4(github.com/robfig/cron/v4)加了WithChain和WithLogger,但仍不解决分布式冲突 - 若只是轻量级、无高可用要求的内部工具,可配 Redis 锁手动防重:每次执行前
SETNX cron:job:xxx-lock 1 EX 30,成功才进逻辑 - 真正要跑核心业务调度,得选从底层支持分布式的库,比如
asynq或machinery,它们把 Cron 当作一种触发器,任务本身走消息队列
Asynq 的 EnqueueScheduled 怎么配 cron 表达式?注意时区和精度
asynq 不是传统 Cron 库,它把定时任务转成“未来某个时间点投递的任务”,靠 Redis ZSET 排序 + 后台扫描线程拉取。所以它不解析 * * * * *,而是用 time.Time 计算下次执行时间,再调用 client.EnqueueScheduled(task, time.Now().Add(...))。
常见错误现象:
立即学习“go语言免费学习笔记(深入)”;
- 用
time.Now().In(loc).Hour()手动算下一次触发时间,结果漏掉跨天/跨月边界(比如每月 31 日在 2 月不存在) - 忽略时区:服务部署在 UTC,但业务要求按北京时间(CST)执行,
time.Now()默认是本地时区,Docker 容器里常为空,变成 UTC - 扫描间隔默认 1s,但 Redis 延迟高时可能错过毫秒级精度任务(不过 Cron 本来就不该依赖毫秒级)
正确做法是封装一个 NextCronTime(expr string, from time.Time, loc *time.Location) time.Time,用 github.com/robfig/cron/v3 的 Parser 解析表达式并计算下一次时间,再传给 EnqueueScheduled。
为什么 machinery 的 RegisterPeriodicTask 要搭配 Redis Sentinel?
machinery 的周期任务本质是 Worker 启动时注册一个 goroutine,每秒检查是否到执行时间。它靠 Redis 的 SETNX 实现 leader 选举:所有 Worker 尝试 SETNX machinery:periodic:leader job-name,只有拿到锁的 Worker 才真正触发任务。所以它不是“调度中心”,而是“带协调的多副本定时器”。
这意味着:
- Redis 单点故障 = 全部周期任务停摆;必须用 Redis Sentinel 或 Cluster,否则主节点挂了,
SETNX永远失败,任务不再触发 -
RegisterPeriodicTask的schedule参数是标准 cron 字符串,但底层仍用robfig/cron/v3解析,兼容性没问题 - 任务函数必须幂等:网络抖动可能导致锁续期失败,另一个 Worker 抢到锁又执行一遍
配置示例中,broker 和 resultBackend 都指向同一个 Sentinel 地址组,且 defaultQueue 名字需统一,否则不同服务注册的任务无法被同一批 Worker 消费。
etcd + lease 实现的 Cron 系统,Watch 事件为什么经常延迟 100ms+?
用 etcd 的 Watch 监听 key 过期事件来驱动 Cron(比如写 /cron/jobs/send-report,TTL=3600,watch 到 delete 事件就执行),看似优雅,实际问题很多。
根本原因在于 etcd 的 lease 机制不是实时通知:lease 到期后,etcd server 需轮询清理,再触发 watch event,这个延迟通常在 100ms–1s 之间,且随集群压力增大而波动。
- Cron 表达式如
*/5 * * * *(每 5 分钟)还能忍,但* * * * *(每分钟)就大概率漏触发或堆积 - Watch 连接断开时事件会丢失,需要客户端做全量 list + compare 恢复状态,代码复杂度陡增
- etcd 的 QPS 限制比 Redis 严得多,高频写入(比如上千个子任务)容易触发限流,
PUT返回etcdserver: request timed out
结论:etcd 适合做分布式锁或配置中心,不适合做高精度、高频率的调度底座。真要用,至少加一层本地 ticker 每 10s 主动 Get 所有未过期 job,作为 watch 的 fallback。
分布式 Cron 最难的从来不是“怎么跑一次”,而是“怎么确保只跑一次 + 出错了怎么补 + 扩容缩容时状态怎么同步”。这些细节藏在锁续约逻辑、存储一致性模型、以及你选的那个库的测试覆盖率里——别只看 README 里的 hello world 示例。










