go语言无内置分布式定时任务能力,需用redis+lua实现抢占式调度:各节点竞争带ttl的锁(set nx),执行前校验并续期,任务幂等且元数据外置,避免时钟偏移与脑裂导致重复执行。

Go 语言本身没有内置分布式定时任务能力,time.Ticker 和 time.AfterFunc 都只作用于单进程。要支持分布式部署,核心矛盾是「避免多个实例重复执行同一任务」,必须引入外部协调机制。
为什么不能直接用 github.com/robfig/cron 或 github.com/go-co-op/gocron
这两个库默认都是纯内存调度,每个服务实例各自启动一套 cron 实例,没做节点间协商。哪怕你用相同的表达式和任务逻辑,10 台机器就会触发 10 次——这不是“分布式”,是“重复执行”。
-
robfig/cron不提供锁或选举能力,cron.AddFunc后就直接跑,无感知其他节点 -
gocron的StartBlocking或Start也是本地行为;它的WithDistributedLocker接口只是预留,不带实现 - 即使加了 Redis 锁包装,也得自己处理:抢锁时机(启动时?执行前?)、锁续期、失败回退、脑裂场景下的过期判断
用 Redis + Lua 实现任务抢占式调度
最轻量且通用的方案是让所有节点在每次触发前竞争一个带 TTL 的 Redis key,抢到才执行。关键不是“谁先注册”,而是“谁在执行窗口内持有有效锁”。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 使用
SET key value EX seconds NX命令(原子性),value 设为唯一节点 ID(如hostname:pid) - TTL 必须明显长于单次任务最大执行时间(比如任务最长 30s,TTL 设为 90s),否则可能执行中锁过期,被别的节点抢走
- 执行完成后用 Lua 脚本校验 value 再删锁,防止误删别人持有的锁:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - 不要用
time.Now().Unix()做锁 key 后缀——不同机器时钟不同步会导致 key 冲突或遗漏;改用任务名 + 下次计划时间戳(由中心化调度器生成,或各节点用 NTP 同步后共同计算)
如何让多个 Go 进程共享同一套 cron 表达式配置
把调度元数据从代码里抽出来,放到外部存储,而不是每个服务写死 cron.AddFunc("0 * * * *", job)。
- 推荐用 JSON + Redis Hash 存储:
HSET cron:jobs job:send_daily_report "{'spec':'0 0 * * *','cmd':'send_email','timeout':60}" - 每个 Go 实例起一个 goroutine,每秒查一次
KEYS pattern或用SCAN轮询当前该触发的任务(按next_fire_time字段索引更优,但需额外维护 Sorted Set) - 解析 cron 表达式用
github.com/gorhill/cronexpr,它比标准库crontab更准,支持秒级、范围、步长等完整语法 - 避免“每秒全量扫描所有 job”:可预计算每个 job 下次触发时间,存进
ZADD cron:timeline <timestamp> job:name</timestamp>,然后用ZRANGEBYSCORE cron:timeline -inf <now></now>拿出待触发列表
网络分区时怎么防止双主执行
Redis 单点故障或集群脑裂时,两个节点可能各自认为自己拿到了锁。这不是 Go 代码能解决的,得靠架构设计收口:
- 强制要求 Redis 使用强一致模式(如 Redis Cluster 的
min-replicas-to-write 1,或哨兵模式下写多数派) - 任务自身必须幂等:比如
send_daily_report先查 DB 中当天是否已有记录,有则跳过;或者用INSERT IGNORE INTO report_log (date, node_id) VALUES (?, ?)做插入判重 - 不依赖“锁一定可靠”,而依赖“执行结果可收敛”。例如用
UPDATE ... SET status='done' WHERE date='2024-06-01' AND status='pending' LIMIT 1,只有第一个 UPDATE 成功的节点才算真正获得执行权
真正的难点不在怎么写 for-loop 抢锁,而在于时钟偏移、Redis 写入延迟、任务超时与锁 TTL 的耦合关系——这些细节稍有偏差,就会在流量高峰时突然出现重复或漏执行。别省那几行 Lua 脚本。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










