gocron多实例必然重复执行,因其无分布式协调能力;真正防重需用etcd lease+watch实现选主与保活,配合cron仅触发调度请求、do内做抢锁和幂等执行。

直接用 time.Ticker 或 gocron 多实例部署,任务必然重复执行——它不是调度中心,只是本地闹钟。
为什么 gocron.NewScheduler() 在 K8s 里一扩就双跑
每个 Pod 启动一个 gocron.NewScheduler(),各自维护独立的 timer heap 和 job 列表;Every(1).Hour().Do(sendReport) 在所有实例整点同时触发,无任何节点间协商机制。日志里看到 send_report executed 出现 3 次?那是 3 台机器各跑了一次,不是高可用,是高重复。
WithDistributedLocker 接口只是空壳,文档没写清它不带实现,容易误判为“开了锁就安全了”。真正要防重,必须自己在 Do() 里做分布式抢锁,而不是依赖框架内置。
- 别把业务逻辑写进
AddFunc,否则一上多副本立刻崩 - 所有实例必须共用同一套定时表达式(如每分钟 tick),只做“触发请求”,不直接执行
- 时钟偏移会影响判断——别用
time.Now().Unix()生成锁 key 或判断是否该执行
用 etcd lease + watch 实现轻量选主与保活
etcd 的 lease 是目前 Go 生态最稳妥的去中心化协调手段:自动过期、事件驱动、跨节点可见。关键不在“存任务”,而在“抢执行权”。
典型流程:所有节点尝试为 /scheduler/jobs/send_daily_report 绑定带 TTL 的 lease;只有第一个成功者获得写入权并持续续租;其余节点 watch 该 key,失效即抢。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
clientv3.Grant创建 lease 时,TTL 设为任务最长耗时的 2–3 倍(如任务通常 15s 完成,TTL 设 45s) - 抢占必须用
clientv3.OpPut配合clientv3.LeaseID,不能靠 GET+SET 模拟原子操作 - 续租要单独 goroutine 跑,并监听
lease.KeepAlivechannel 关闭(说明 lease 已被 revoke) -
watch必须加clientv3.WithPrevKV(),才能区分“是自己删的”还是“被别人抢走的”
robfig/cron/v3 和分布式锁怎么协同工作
把 robfig/cron/v3 当作本地定时器,只负责精准触发「调度请求」,真正的执行决策交给外部协调层。这是解耦的关键一步。
推荐结构:cron.Every(1).Minutes().Do(tryAcquireAndRun),其中 tryAcquireAndRun 内部完成三件事:查 etcd 确认是否被选为主、用 SET task:lock:sync_user_20240609 1 NX EX 180 抢粒度锁、锁成功才调真实函数。
- 锁的
EX时间必须 ≥ 任务最长可能耗时 × 3,否则锁提前释放,其他节点并发进入 - 锁 value 写入唯一标识(如
os.Getenv("HOSTNAME")),出问题时能快速定位谁在执行 - 任务需幂等重试时,锁 key 要包含
execID,不能只用jobName,否则重试会失败 - 禁止在
Do()里写阻塞 IO 或耗时逻辑,否则 tick 积压,后续调度延迟
Asynq 和 Temporal 哪个更适合你的场景
Asynq 基于 Redis,适合延迟队列、周期性重试类任务(如订单关单、邮件补发);Temporal 自带服务端,适合长时运行、需审计追溯的复杂流程(如支付对账、审批流)。
二者都要求任务函数幂等,因为网络分区或 worker crash 可能导致同个 taskID 被多次投递。Asynq 最小可行链路缺一不可:定义 asynq.NewTask、注册 mux.HandleFunc、分发时指定 asynq.ProcessIn 或 asynq.ScheduleAt,漏掉任一环节都会静默失败。
- Asynq 的 Redis DB 必须和业务隔离,避免 key 冲突或误清空
- Temporal 的 PostgreSQL 方案需确保
pg_cron扩展已安装,且 crontab 条目调的是 HTTP 接口而非直接跑 SQL - 所有 payload 序列化用
json.Marshal,别用gob,否则 Go 版本升级后反序列化失败
最容易被忽略的是时钟偏移和锁超时设置——差几秒就可能导致漏触发或双触发,而锁时间设短了,任务还没跑完锁就没了,设长了又拖慢故障恢复。真正在意一致性时,优先用 etcd lease + watch,而不是靠时间戳硬判断。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










