time.Ticker适合轻量周期任务但不可直接用于生产,需防阻塞、recover panic、避免耗时操作;cron/v3支持秒级需显式启用,应复用单例、注意时区与表达式匹配,并强化幂等性、监控和外部调度保障。

用 time.Ticker 实现简单周期任务,但别直接上生产
如果只是每 5 秒打印一次日志、或轮询某个本地状态,time.Ticker 足够轻量且可控。它底层基于系统单调时钟,不会因系统时间跳变而错乱。
但要注意:time.Ticker 不处理 panic、不支持任务并发控制、不记录执行历史、挂了就真挂了——一旦 tick 通道被阻塞(比如任务耗时超过间隔),后续所有 tick 都会堆积在 channel 缓冲区,最终导致内存上涨甚至 OOM。
- 务必用
select+default非阻塞读取,或设合理缓冲大小(如time.NewTicker(5 * time.Second)后只取最新一个 tick) - 任务函数必须 recover panic,否则整个 ticker goroutine 会退出
- 不要在 ticker 回调里做 HTTP 请求、数据库写入等不可控延迟操作
cron 包能解析 crontab 表达式,但默认不支持秒级精度
主流选择是 github.com/robfig/cron/v3,它兼容 Unix cron 格式(0 0 * * * ? 这类带秒的表达式需显式启用秒字段)。
默认初始化 cron.New() 使用的是标准五段式(分、时、日、月、周),没有秒;要支持秒,必须用 cron.New(cron.WithSeconds())。否则你写 "*/5 * * * * *" 会被静默截断为 "* * * * *",实际变成每分钟执行一次。
立即学习“go语言免费学习笔记(深入)”;
- 使用前检查
cron.New(...)的选项是否匹配你的表达式格式 - 注意时区:默认用
time.Local,若部署在 UTC 服务器但业务按北京时间调度,得传入cron.WithLocation(time.FixedZone("CST", 8*60*60)) - 任务函数返回 error 不会重试,也不会自动告警,需自行包装日志和监控
多个定时任务共存时,用单例 cron.Cron 实例统一管理
每个 cron.Cron 实例启动一个独立 goroutine 调度器。如果为每个任务 new 一个 cron.Cron,不仅浪费 goroutine,还会因各自 tick 时间不同步导致 CPU 毛刺。
正确做法是全局复用一个实例,用 AddFunc 或 AddJob 注册多个任务:
var c = cron.New(cron.WithSeconds())
c.AddFunc("0 */2 * * * *", func() { /* 每两分钟 */ })
c.AddFunc("*/30 * * * * *", func() { /* 每 30 秒 */ })
c.Start()
// 记得 defer c.Stop() 避免进程退出时 goroutine 泄漏
- 避免在
AddFunc的回调中调用c.AddFunc或c.Remove—— 它们不是 goroutine-safe 的 - 若需动态增删任务,改用
cron.WithChain(cron.Recover(cron.DefaultLogger))增强健壮性 - 调试时开启
cron.WithLogger可看到每次触发、跳过、panic 的详细日志
生产环境必须考虑任务失败、重复、漂移三大现实问题
真实场景中,数据库连接超时、下游服务不可用、机器重启、发布期间进程 kill -9,都会让一次调度“看起来没执行”。但 Go 定时器本身不保证 exactly-once,也不保存状态。
这意味着:你不能靠“定时器触发了”就认为业务逻辑一定成功;也不能靠“没触发”就断定任务丢了——可能只是调度时间漂移了 200ms(尤其在高负载容器中)。
- 对幂等性要求高的任务(如发券、扣库存),必须在业务层加唯一 key(如
"task:send_voucher:20240520:12345")+ Redis SETNX 判断是否已执行 - 关键任务建议补一层外部调度(如 Airflow 或自建轻量调度中心),Go 进程只负责“执行”,不负责“决策何时执行”
- 监控不能只看 goroutine 数量,要埋点记录每次
StartAt、ActualRunAt、Duration,才能发现 drift 超过阈值的问题
定时逻辑越靠近业务,就越容易被当成“小功能”忽略可观测性和容错设计。真正难的从来不是怎么跑起来,而是跑歪了、跑重了、跑丢了的时候,你知道吗。










