robfig/cron/v3在多实例下必然重复执行,因其纯内存调度、无节点协调机制;必须通过redis+lua实现抢占式分布式锁,或改用原生支持分布式的go-quartz。

robfig/cron/v3 在多实例下必然重复执行
直接用 robfig/cron/v3 启动定时任务,10 台 Gin 实例就会触发 10 次——它根本不知道其他节点的存在。所有调度逻辑都在内存里,cron.New() 每个进程一份,没有锁、没有协调、没有状态同步。
常见错误现象包括:日志里看到同一任务在不同机器上几乎同时打印;数据库里某条统计记录被累加多次;HTTP 接口被并发调用导致幂等失效。
- 别指望靠「统一配置」或「相同 cron 表达式」来避免重复——这只会让问题更隐蔽
- 如果任务本身不幂等(比如发短信、扣库存),后果是线上事故,不是日志噪音
-
robfig/cron/v3的秒级支持(cron.WithSeconds())和高精度调度,反而会放大重复风险
必须引入外部协调:Redis + Lua 是最轻量可行解
Go 语言没有内置分布式调度能力,time.Ticker 和 time.AfterFunc 都只作用于当前进程。要让多个 Gin 实例达成「有且仅有一个执行」,就得靠外部系统做裁定——Redis 是最常用、最低侵入的选择。
核心逻辑是抢占式锁:每个节点尝试用 SET key value NX EX ttl 去争一个带过期时间的任务锁;抢到的节点才真正执行,执行中还要用 Lua 脚本原子续期,防止超时被其他节点误抢。
- 锁 key 命名建议含任务 ID + 环境标识,例如
task:send_daily_report:prod - TTL 必须明显长于单次任务最大耗时(比如任务最长 30s,TTL 至少设为 60s)
- 不要用
redis.SetNX+redis.Expire两步走——中间存在竞态窗口,必须用单条SET命令的NX EX参数保证原子性
go-quartz 是少数能开箱即用的分布式方案
如果你不想自己封装 Redis 锁逻辑,go-quartz 是目前 Go 生态中为数不多原生支持分布式的轻量级调度库。它不像 gocron 那样强依赖 Redis 作为后端存储,而是把协调逻辑内建在调度器中,只需传入一个实现了 quartz.JobStore 接口的存储层(比如基于 Redis 的实现)。
它和 Gin 集成很简单:启动时初始化一个全局 quartz.Scheduler,注册 CronTrigger,任务体实现 quartz.Job 接口即可。所有节点共用同一套元数据,自动处理抢占、失败重试、执行中检测。
- 注意
go-quartz默认不带持久化实现,需自行对接 Redis 或数据库——它的设计就是“零依赖核心 + 可插拔存储” - 它的
SimpleTrigger和RunOnceTrigger在分布式场景下同样受控,不是单机行为 - 相比自己写 Redis 锁,它帮你屏蔽了续期、心跳、节点掉线恢复等细节,但学习成本略高
Gin 中集成调度器的关键陷阱
Gin 是 HTTP 框架,不是调度平台。把调度器塞进 gin.Engine 或挂到 gin.Context 里,是典型误用。调度器生命周期必须独立于请求周期,否则每次 HTTP 请求都可能意外触发新调度实例。
正确做法是:在 main() 函数里初始化调度器(如 quartz.NewStdScheduler() 或自建 Redis 抢占循环),用 go 启动 goroutine 运行,再通过 Gin 的路由暴露管理接口(如添加/暂停任务)。
- 别在中间件里初始化或操作调度器——中间件是 per-request 的,极易造成资源泄漏或并发冲突
- 调度器实例必须全局唯一,不能按路由或 controller 创建多个副本
- 如果用数据库存任务元数据,Gin 接口修改任务时,要确保更新 DB 后主动通知调度器(比如通过 channel 或事件广播),否则内存状态会滞后
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











