该用cron还是hptimer取决于任务粒度和表达式需求:秒级周期且需Cron表达式(如“每月1号凌晨1点”)必须用cron;毫秒级精度、高频动态增删、上万任务则选hptimer。

定时任务该用 cron 还是 hptimer?看粒度和表达式需求
秒级周期、需要 Cron 表达式(比如“每月1号凌晨1点”或“每周一9点”)——必须用 cron;毫秒级精度、高频动态增删(如连接超时清理、限流窗口刷新)、任务量上万——选 hptimer。cron 最小粒度是秒,硬切毫秒会不准;hptimer 不支持表达式,写 "0 0 1 1 * ?" 直接 panic。
常见误用:拿 hptimer.Every(1 * time.Second, fn) 模拟 cron,结果因系统负载导致实际间隔飘到 1.2s 或 1.8s,业务逻辑出偏移。这不是 bug,是设计使然——它不承诺“准时”,只保证“不早于”。
注册时机错一步,任务就永不执行
cron 和 hptimer 都必须在 kratos.New() 之后、app.Run() 之前调用 app.Handle(c),否则调度器不会被纳入 App 生命周期管理。早于 kratos.New()(比如在 init() 或包变量里 new 实例),会因日志/配置组件未初始化而 panic;晚于 app.Run(),App 已启动,Handle 调用无效,控制台无报错但任务静默失效。
正确顺序示例:
func main() {
app := kratos.New(
kratos.Name("shop-service"),
kratos.Version("v1.0.0"),
)
// ✅ 此处注册
c := cron.New()
_, _ = c.AddJob("0 0 1 * * ?", cron.NewFuncJob(func() { data.CleanExpiredCoupons() }))
app.Handle(c)
<pre class='brush:php;toolbar:false;'>// ❌ 这里再 new 或 app.Handle 就晚了
app.Run()}
任务函数里不能有阻塞 IO,否则整个 cron 协程池卡死
cron 内部共享一个 goroutine 池(默认 3 个),所有 job 共用。如果某个任务里写了没设 timeout 的 http.Get("https://xxx") 或未加 context 的 db.QueryRow(),一次慢请求就能拖垮全部定时任务。
- 必须把 IO 操作封装进
biz方法,并传入带超时的context.Context - 避免在 job 函数里直接调用
time.Sleep、sync.WaitGroup.Wait等同步等待逻辑 - 日志打点用
log.Helper,别用全局 logger,防止因日志组件未就绪导致 panic
分布式场景下重复执行?Kratos 默认不解决,得自己加锁
Kratos 的 cron 是单实例行为,K8s 部署 3 个 Pod,每个都会跑一遍 “每天凌晨1点”的任务。这不是框架缺陷,而是设计定位:它不负责分布式协调,只管“本机准时调度”。要防重复,你得自己加分布式锁。
最轻量做法是数据库唯一索引:
INSERT INTO task_lock (task_name, instance_id, expire_at)
VALUES ('daily_report', 'shop-pod-123', NOW() + INTERVAL 30 MINUTE)
ON CONFLICT (task_name) DO NOTHING;插入成功才执行业务,失败则跳过。注意 expire_at 必须设,否则节点宕机后锁永远不释放。Redis SETNX + EXPIRE 也可,但需确保原子性(用 Lua 或 RediSQL)。
真正容易被忽略的是:锁校验和业务执行之间存在时间窗口。哪怕加了锁,如果业务耗时超过锁过期时间,另一个实例可能在中途抢锁并重入。所以锁有效期要明显长于业务最大预期耗时,且业务内部要做幂等判断(比如对账任务先查当日是否已生成报表)。


















