直接用gocron、cron/v3或time.ticker多实例部署必然重复执行,因其仅为本地定时器;需结合etcd lease+watch实现选主与保活,将定时器仅用于触发调度请求,执行权由分布式锁控制。

直接用 Gocron、cron/v3 或 time.Ticker 多实例部署,任务必然重复执行——它们只是本地定时器,不是分布式调度器。
gocron.NewScheduler() 为什么一上 K8s 就多跑
每个 Pod 启动一个 gocron.NewScheduler(),就各自维护一套 timer heap 和 job 列表;Every(1).Hour().Do(sendReport) 在三台机器上同时解析、同时触发,中间零协调。日志里看到 send_report executed 出现三次/小时,不是高可用,是高重复。
WithDistributedLocker 接口只是空方法,文档没写清,容易误以为开箱即用。它不提供任何锁实现,也不参与调度决策。
- 所有节点必须共用同一套外部协调机制(Redis/Etcd),
gocron只能当“本地闹钟”用 - 业务逻辑不能写在
Do()里,否则锁还没抢到,耗时操作已开始拖慢整个 tick 周期 - 每分钟
Every(1).Minutes().Do(tryRunJob)是安全节奏;整点触发类任务需靠锁内部判断时间窗口,而非依赖 cron 表达式精度
etcd lease + watch 是目前最稳妥的选主方案
比起 Redis 的 SETNX,etcd 的 clientv3.Grant 租约天然支持自动过期、事件驱动、跨节点可见,且 watch 能拿到 prevKV,可精准区分“锁被别人释放”还是“自己主动删”。
- TTL 设为任务预期耗时的 2–3 倍(如任务通常 15s 完成,lease TTL 设 45s)
- 抢占必须用
txn.If(txn.Compare(txn.Version(...), "=", 0)).Then(...),避免竞态覆盖 - 续租要起独立 goroutine,并监听
lease.KeepAlivechannel 关闭信号,及时退出执行逻辑 - watch 必须带
clientv3.WithPrevKV(),否则无法判断 key 是被谁删的
robfig/cron/v3 怎么和分布式锁安全配合
robfig/cron/v3 本身无并发控制能力,但可以解耦“触发”与“执行”:它只负责准时调用一个轻量函数,真正的执行权由外部锁决定。
- 不要在
cron.AddFunc("@hourly", doSendDailyReport)里写业务逻辑 - 改用
cron.AddFunc("@every 1m", func() { tryAcquireAndRun("send_daily_report") }) -
tryAcquireAndRun内部做三件事:查 etcd 当前 leader、尝试绑定 task-specific lease、抢到才调doSendDailyReport - 时钟偏移敏感操作(如用
time.Now().Unix()生成锁 key)必须规避;真正在意一致性时,优先用 lease ID 或 etcd revision 替代本地时间
RabbitMQ 方案里最容易漏掉的三个硬性约束
用 RabbitMQ 做任务分发看似简单,但生产环境出问题往往卡在这三点:
-
amqp.Connection必须全局单例,channel 按用途隔离;每次 HTTP 请求都amqp.Dial会导致端口耗尽 - publish 前必须检查
ch.IsClosed() == false,否则 panic 后重连逻辑可能失效 - consumer 端绝不能设
autoAck: true;必须用ch.Consume+ 手动delivery.Ack(false),且执行逻辑要绑定context.WithTimeout
任务结构体字段必须加 json:"field_name,omitempty",空 map 或 nil interface{} 会引发 consumer 解析 panic;关键字段如 ID、Type 不设 omitempty,并在 Unmarshal 后立刻校验。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











