多实例直接运行gocron或robfig/cron/v3会导致任务重复执行,因其缺乏节点间状态同步与选主机制;必须通过etcd lease+watch实现“谁有资格执行”的分布式共识,并解耦触发与执行逻辑。

直接用 gocron 或 robfig/cron/v3 启动多个实例,无法整合集群资源——它们各自为政,不协商、不选举、不感知其他节点,结果就是任务在所有节点上重复执行。
为什么“多跑一个实例”不等于“分布式调度”
根本问题在于:调度器没做节点间状态同步。你部署 5 个 Pod,每个都调用 s.Every(1).Minutes().Do(sendReport),那 sendReport 就真会被每分钟触发 5 次。这不是负载分担,是资源浪费加业务错乱。
- 数据库标记法(如
UPDATE tasks SET status='running' WHERE id=? AND status='pending')在无事务隔离或超时处理缺失时,会因竞态导致双跑或卡死 -
time.Ticker+ 自己写 for-loop 轮询,节点宕机后任务永久丢失,且无法动态扩缩容 - 所有纯内存/本地时间驱动的方案,都绕不开「谁有资格执行」这个分布式共识问题
用 etcd lease + watch 实现轻量级节点协同
etcd 是目前 Go 生态中对「选主 + 保活」支持最干净的组件。它不靠轮询,而是用租约(lease)绑定 key 的生命周期,并通过 watch 实时感知变更——这才是真正响应快、故障自愈的集群协同方式。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 每个节点启动时,尝试为固定路径(如
/scheduler/leader)创建带 TTL 的 lease 并写入自身 ID:client.Put(ctx, "/scheduler/leader", nodeID, clientv3.WithLease(leaseID)) - 只有第一个成功写入的节点成为 leader;其余节点
client.Watch(ctx, "/scheduler/leader", clientv3.WithPrevKV()),一旦发现值被覆盖或 lease 过期,立刻重试抢占 - TTL 建议设为任务最大耗时的 2–3 倍(例如任务最长 15s,TTL 设 45s),太短易误判离线,太长会导致故障恢复延迟
- 续租必须用独立 goroutine 调用
lease.KeepAlive(),并监听其返回 channel 关闭事件——这是判断 lease 是否被主动 revoke 的唯一可靠方式
任务分发层必须与执行层解耦
别让 cron 包直接执行业务逻辑。它的角色应降级为「本地精准闹钟」,只负责在本机某个时刻触发一次函数调用;真正的任务拉取、锁抢占、幂等执行,全由该函数内部完成。
- 在
cron.AddFunc("@every 1m", tryAcquireAndRun)中,tryAcquireAndRun应先检查自己是否是当前 leader(读/scheduler/leader),再从 Redis 或数据库拉取待执行任务列表 - 对每个任务,用
SET task:lock:<code>jobID1 NX EX 120 抢锁,失败则跳过——注意 EX 必须大于任务实际执行时间,否则锁提前释放会引发重复 - 任务 payload 序列化统一用
json.Marshal,避免gob在 Go 版本升级后反序列化失败 - 所有任务 handler 必须设计为幂等:同一
taskID被多次投递时,结果一致且副作用可控(比如用UPSERT替代INSERT)
Redis 和 etcd 到底怎么选
不是“哪个更好”,而是“哪个更匹配你的故障容忍边界”。Redis 简单快,但网络分区时可能多个节点同时认为自己持锁;etcd 的 lease 绑定 session,自动续租+失效,强一致性更高,代价是代码量多 3–4 倍、需处理 context.DeadlineExceeded 等网络错误。
- 已有 Etcd 集群且任务不能容忍一次重复(如金融扣款),必须用
etcd/client/v3的 CAS 操作 - 只是定时发通知、生成报表这类场景,Redis +
SETNX+ 合理 TTL 已足够,别为一致性过度设计 - 切记:无论选哪个,锁的释放不能依赖“执行完才删”,而必须靠超时自动失效;程序内不手动
DEL锁,防止 panic 导致锁残留
最容易被忽略的一点:任务调度的可靠性,80% 取决于存储层配置,而不是 Go 代码写了多少行。Redis 没开 AOF、PostgreSQL 没装 pg_cron 扩展、etcd 没配好心跳间隔——这些底层细节出问题,上层再精巧的调度逻辑都会静默失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










