time.ticker适用于轻量周期同步,但需显式调用stop()防goroutine泄漏;复杂场景应选robfig/cron/v3,配合context、错误策略与断点续传机制保障可靠性。

用 time.Ticker 做简单周期同步,但要注意 goroutine 泄漏
如果只是每 5 秒拉一次 API 同步少量数据,time.Ticker 是最轻量的选择。它不依赖外部调度器,启动快、无依赖。
常见错误是把 ticker.C 直接塞进 for range 却没做退出控制,导致程序关闭后 goroutine 一直挂着:
func startSync() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop() // 必须有,否则泄漏
for range ticker.C {
syncOnce()
}
}
- 必须显式调用
ticker.Stop(),尤其在服务 graceful shutdown 场景下 - 别在
for range ticker.C里直接写重试逻辑——一旦syncOnce()panic 或阻塞,整个 ticker 就卡死 - 建议包装一层
select+ctx.Done(),方便统一控制生命周期
需要错峰/失败重试/并发限流?用 robfig/cron/v3 更稳
robfig/cron/v3(现维护分支为 github.com/robfig/cron/v3)不是 Linux cron 的 Go 移植版,而是专注 Go runtime 内部调度的库,支持 context.Context、任务超时、单实例锁等关键能力。
典型同步任务容易踩的坑:
- 多个实例同时跑,导致重复写 DB 或发重复请求 → 用
cron.WithChain(cron.Recover(), cron.SkipIfStillRunning()) - 某次同步耗时突增,下一轮被跳过 → 改用
cron.WithChain(cron.Recover(), cron.DelayIfStillRunning()) - 想按「每小时第 3 分钟」执行,但写成
"3 * * * *"→ 实际是「每小时的第 3 分钟」,正确 crontab 表达式是"3 * * * *";若要「每天 3:03 执行」,得写"3 3 * * *"
示例:
func setupCron() *cron.Cron {
c := cron.New(cron.WithChain(
cron.Recover(),
cron.DelayIfStillRunning(cron.DefaultLogger),
))
c.AddFunc("0 */2 * * *", func() { // 每两小时同步一次
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
syncWithCtx(ctx)
})
return c
}
同步任务失败后怎么补?靠时间窗口 + 最后同步点位记录
定时任务不是万能的,网络抖动、DB 临时不可用、上游接口限流都可能导致某次失败。硬靠「重试 N 次」解决不了断点续传问题。
真正可靠的方案是:每次同步前查上一次成功更新的 updated_at 或 sync_cursor,然后拉取该时间之后的增量数据。
- 点位存储别用内存变量,必须落盘(如写入本地
sync_state.json或 DB 的sync_metadata表) - 更新点位和写业务数据必须在同一个事务里(或用两阶段提交),否则出现「数据写了,点位没更新」就会重复同步
- 首次运行没历史点位?不要默认设为
time.Now().Add(-24h),而应查上游接口支持的最早可拉时间,或配置一个安全兜底值(如"2024-01-01T00:00:00Z")
高并发同步场景下,sync.Once 和 sync.Map 用错地方反而拖慢性能
当同步任务本身要并发处理几百个子资源(比如批量拉取 500 个用户的 profile),容易下意识用 sync.Once 初始化 HTTP client 或 DB 连接池——这是错的。因为 sync.Once 是全局单次,而连接池本就该复用,不需要靠它保护。
更常见的误用是拿 sync.Map 存临时中间结果(如「已处理 ID 集合」):
-
sync.Map适合读多写少、key 分布广的长期缓存,不适合一次同步内高频增删的临时集合 - 这种场景直接用
map[int64]struct{}+sync.RWMutex,或者干脆用goroutine局部变量 + channel 收集结果,性能高一个数量级 - 真正需要并发安全的,是共享状态:比如当前正在同步的总进度、失败计数器,这时才该用
atomic.Int64或带锁结构
同步任务的复杂度从来不在调度本身,而在于你怎么定义「一次同步成功」——是 HTTP 状态码 200?是所有目标记录都写进 DB?还是下游回调确认?这个边界没理清,再准的定时器也白搭。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











