直接用time.ticker在微服务多实例下必然重复执行,因其纯本地内存调度、无全局协调能力;必须通过redis分布式锁或etcd租约机制实现执行权仲裁,并辅以任务幂等性设计。

为什么直接用 time.Ticker 在微服务里会出问题
多个实例同时拉起定时任务,不是“每分钟执行一次”,而是“每分钟执行 N 次”——这是最常见也最隐蔽的问题。根本原因在于 time.Ticker 是纯本地内存调度,不感知其他节点,也不做任何协调。
典型表现:日志里看到同一任务在 09:00:00 几乎同时被 3 台机器各打印一行;数据库里出现重复订单、重复发券、重复清理;甚至因并发写冲突触发 unique constraint violation 错误。
- 别指望靠加锁或 DB 唯一索引“兜底”——它只解决数据一致性,不解决资源浪费和逻辑误触发
- 不要在 init 或 main 中直接启动
time.Ticker—— 容器重启、滚动发布时极易造成任务重叠 -
time.Sleep+ for 循环更危险:无法优雅停止,panic 后调度丢失,且同样无分布式语义
用 etcd 实现租约抢占式调度
核心思路是把“谁来执行”这个决策从代码里抽出来,交给一个外部协调者(etcd),每个服务实例竞争一个带 TTL 的 key,抢到的才真正运行任务。
Go 生态里最轻量、最可控的方式是用 clientv3 + Lease + CompareAndSwap(CAS)组合实现。不用引入 heavyweight 的调度框架,也能保证强一致性。
- 租约 TTL 设为任务周期的 1.5~2 倍(例如任务每 30s 执行一次,lease TTL 设为 45s),避免网络抖动导致频繁漂移
- 每次执行前必须先
client.Grant新 lease,再client.Put带 lease ID 的 key,不能复用旧 lease——否则节点宕机后 lease 过期,key 自动删除,但别的节点可能还没来得及抢到 - 务必监听
ctx.Done()并主动client.Revokelease,否则滚动发布时旧进程残留 lease,新实例抢不到 key,任务就停了
// 示例:抢到 /scheduler/job-cleanup 才执行
leaseResp, err := client.Grant(ctx, 45)
if err != nil { return }
_, err = client.Put(ctx, "/scheduler/job-cleanup", "running", clientv3.WithLease(leaseResp.ID))
if err != nil { return } // 抢占失败,跳过本次执行
用 redis + SETNX 做降级方案要注意什么
如果团队已重度依赖 Redis,且对 CP 要求不高(允许极短时间内的双跑),可以用 SETNX + 过期时间模拟租约。但它不是线性一致的,需接受小概率冲突。
关键不是“能不能用”,而是“怎么用才能把风险框死”。重点防的是 key 永久卡住、任务彻底失活,而不是偶尔双跑。
- 永远用
SET key value EX seconds NX一条命令完成设置+过期+原子判断,不要拆成EXISTS+SET—— 中间有竞态窗口 - value 必须是唯一标识(如
hostname:pid),后续可通过GET校验是否自己设的,避免误删别人 key - 执行完任务后,用 Lua 脚本原子地
GET+DEL,防止自己没删掉、别人又抢到了却误删 - 不要依赖 Redis Cluster 的哈希槽路由——
SETNX是单 key 操作,但跨 slot 时会报CROSSSLOT错误,务必确保 key hash tag 一致(如{job-cleanup})
任务执行失败后要不要自动重试
答案取决于任务类型:幂等型任务(如清理过期缓存)可以立即重试;状态推进型任务(如调用支付回调接口)必须人工介入或走补偿流程。
自动重试本身不难,难的是重试边界。很多团队踩坑是因为没区分“临时失败”和“业务失败”,把 HTTP 400 当成可重试错误,结果重复扣款。
- 只对明确的 transient error 重试:如
context.DeadlineExceeded、io.EOF、redis: connection refused - 对业务返回码(如
payment_failed)绝不重试——它们是最终态,重试只会放大问题 - 重试间隔建议用指数退避(
time.Second * (2 ^ attempt)),最大不超过原任务周期,否则会堆积 - 记录失败详情到结构化日志(含
attempt_count、error_type、task_id),否则下次排查时连“是不是重试过”都不知道
分布式定时调度真正的复杂点不在选哪个工具,而在于你能否清晰定义“这个任务到底允许多大程度的不精确、不一致、不及时”。越早画清这条线,后面踩的坑就越少。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











