robfig/cron/v3在K8s多副本下必然重复执行,因其未做节点协商,各Pod独立解析和触发定时任务;应解耦触发与执行,用etcd lease+watch实现选主,或选用XXL-JOB方案。

为什么robfig/cron/v3在K8s多副本下必然重复执行
它根本没做节点间协商,每个实例启动后都独立解析cronexpr、独立触发AddFunc。哪怕你用同一份配置,10个Pod就是10个定时器,任务逻辑跑10遍——这不是分布式,是并发误杀。
常见错误现象:send_daily_report每天发10封邮件;sync_third_party_data每5分钟同步10次,第三方接口直接限流。
- 别指望靠数据库UPDATE … WHERE status=‘pending’兜底:没加
FOR UPDATE或没处理锁超时,高并发下照样双写 - 别用
time.Now().Unix()拼Redis锁key:各节点时钟不同步,会导致key计算不一致,漏触发或冲突 - 别把cron表达式硬编码在代码里:
cron.AddFunc("0 0 * * *", job)一改就得发版,运维成本爆炸
用etcd lease + watch实现轻量级选主调度
etcd的租约(lease)+ 键监听(watch)是目前Go生态里最稳的“谁来跑”机制。它天然支持自动过期、跨节点可见、事件驱动,比轮询Redis或MySQL省资源也更实时。
关键不是存任务,而是抢执行权。典型流程:所有节点尝试为/scheduler/jobs/send_daily_report绑定带TTL的lease;只有第一个成功绑定的节点拿到写入权并持续续租;其他节点watch该key,一旦失效立刻尝试抢占。
立即学习“go语言免费学习笔记(深入)”;
-
clientv3.Grant创建lease,TTL设为任务预期执行时长的2–3倍(如任务通常10s完成,TTL设30s) - 抢占写入必须用
clientv3.OpPut配合clientv3.WithLease(leaseID),不能只靠SETNX - 续租要单独起goroutine跑,且必须捕获
lease.KeepAlive返回channel关闭(说明lease已被revoke) - watch必须用
clientv3.WithPrevKV(),否则拿不到旧值,无法判断是不是自己释放的锁
让robfig/cron/v3只负责“触发”,不负责“执行”
把“什么时候触发”和“谁来执行”彻底解耦。cron只做本地倒计时器,每分钟tick一次,调用tryAcquireAndRun(jobName string);这个函数内部才去etcd抢锁,抢到才真正调用doSendDailyReport()。
错误做法:cron.AddFunc("0 * * * *", sendEmail)——一部署多实例就双跑;正确做法:cron.AddFunc("0 * * * *", func() { tryAcquireAndRun("send_email") })。
- 抢锁时机必须在执行前,不能在服务启动时——否则节点异常退出后锁残留,后续永远无法触发
- 任务元数据必须外置:用JSON存etcd的
/scheduler/jobs/xxx路径下,而不是写死在代码里 - 执行完成后必须用Lua脚本校验value再删锁(Redis场景)或用etcd的
OpDelete配WithRev(etcd场景),防止误删别人持有的锁
XXL-JOB + xxl-job-executor-go是中小团队最快落地方案
如果你不想从零造轮子,又不愿引入Java运维负担,用Docker三分钟起XXL-JOB调度中心,Go服务只做执行器,是最省心的选择。
调度中心用Docker跑:docker run -d --name xxl-job-admin -p 8080:8080 -e PARAMS="--spring.datasource.url=jdbc:mysql://host.docker.internal:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai" xuxueli/xxl-job-admin:2.4.0;Go端用xxl-job-executor-go注册,任务逻辑写在Run方法里即可。
- Web界面可动态改cron表达式、手动触发、看日志、设失败重试次数——这些功能自己实现至少两周
- 执行器多实例部署后,调度中心自动负载均衡(轮询/随机/故障转移),宕机实例的任务秒级切走
- 注意:Go执行器必须主动上报心跳,否则会被调度中心标记为离线;心跳间隔建议设为30秒,别太短压垮中心
真正容易被忽略的是任务幂等性设计——无论调度中心发几次指令,执行器都得保证同一次任务只生效一次。锁只是防并发,业务逻辑本身得扛住重复调用。


















