Kratos 的 cron 和 hptimer 均为单实例调度器,无分布式协调能力,多 Pod 部署必然重复执行;防重复需自行实现 Redis 分布式锁+数据库唯一约束双保险。

Kratos 框架本身不内置分布式锁或全局任务去重能力,cron 和 hptimer 都是单实例调度器 —— 只要部署多个 Pod 或进程,就必然重复执行。这不是配置没写对,而是设计如此。
kratos-cron 多实例重复执行的典型现象
本地调试只跑一次,上线后日志里同一时间戳出现多条「开始执行日报生成」记录,且 hostname 或 pod-name 不同;数据库里生成了多份相同日期的报表;第三方接口被并发调用,触发限流告警。
根本原因不是 Cron 表达式错了,也不是 cron.Every(1).Day().Do(...) 写了两遍,而是每个 Kratos 实例都独立解析并触发了这个任务。Kratos 的 cron 中间件没有跨节点协调机制,它只管“本机该不该跑”,不管“别处是不是已经在跑了”。
- 检查方式:在任务入口加一行日志,打印
os.Getenv("HOSTNAME")或os.Getenv("POD_NAME") - 验证手段:临时把 replicas 设为 1,观察是否还重复 —— 如果不重复,基本锁定是多实例问题
- 注意:
cron的max_instances类参数不存在,它不提供单机并发控制,这点和 APScheduler 不同
kratos-hptimer 同样不解决跨实例重复
hptimer 是毫秒级精度的轻量调度器,适合高频、低延迟场景(如心跳检测、采样上报),但它和 cron 一样,只在当前进程内生效。哪怕你用 hptimer.AddFunc("cleanup", ...) 注册了一个每 500ms 执行一次的任务,三个 Pod 就会各自每 500ms 执行一次,完全互不感知。
它的优势在于资源占用低、动态增删安全,但劣势也明确:不带任何分布协调语义。如果你的任务逻辑涉及写 DB、发消息、调外部 API,hptimer 必须配合外部锁使用,否则重复风险比 cron 更高(频率更高,窗口更小)。
- 适用场景:
hptimer适合无状态、幂等、或仅影响本机内存/缓存的操作 - 误用信号:任务体里出现
db.Exec(...)、redis.Set(...)、http.Post(...)等副作用操作,却没加锁 - 性能提示:
hptimer在万级任务下仍保持 O(log n) 调度开销,但锁竞争会成为新瓶颈,别只盯调度器本身
必须自己补上的两层防护
在 Kratos 里实现防重复,不能只靠框架,得组合落地:调度层 + 数据层双保险。
调度层用 Redis 分布式锁最直接。比如用 redis.SetNX("task:report:20261019", "pod-a", "EX", 3600),只有抢到 key 的实例才真正执行,其他跳过。注意 TTL 要明显大于任务最大耗时,避免锁自动释放后被另一个实例误抢。
数据层兜底更关键。例如日报表加联合唯一索引:UNIQUE KEY `uk_date` (`trade_date`);行情更新任务用 INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或 INSERT IGNORE(MySQL)。即使锁失效或网络分区,DB 层也能拦住脏数据。
- 别依赖「锁成功就万事大吉」:Redis 锁可能因网络抖动丢失,必须搭配超时重试和幂等标识(如任务 ID + 日期)
- 避免用本地内存锁(
sync.Mutex)或文件锁:它们只对单进程有效,在容器环境毫无意义 - 如果已用 Asynq,它自带任务去重(
asynq.UniquePeriod),但那是另一套调度体系,和cron/hptimer并不兼容
真正容易被忽略的是:Kratos 的定时器中间件从不承诺“仅执行一次”,它只承诺“按规则触发”。要不要去重、怎么去重、重试时怎么判断失败类型——这些决策权全在业务代码手里。别等线上出三份日报才想起查 redis-cli keys "task:*"。


















