不能直接用 time.ticker 做分布式定时任务,因为它是单机内存态,扩缩容或宕机导致任务丢失或重复,多实例会反复触发同一任务;必须依赖 redis setnx 或 etcd lease 等外部机制实现全局唯一执行权、故障自动漂移和状态可追溯。

为什么不能直接用 time.Ticker 做分布式定时任务
因为 time.Ticker 是单机内存态的,节点一扩缩容或宕机,任务就丢失或重复;多个实例同时跑,同一任务会被反复触发。微服务场景下必须靠外部一致性协调——比如用 Redis 的 SETNX + 过期时间做选主,或借助 Etcd 的 lease + watch 机制实现租约抢占。
常见错误是把本地定时器封装成“调度服务”,结果上线后发现每台机器都在执行同一条 cron 表达式任务。真正要的是:全局唯一执行权 + 故障自动漂移 + 任务状态可追溯。
- 用
redis.SetNX加锁时,务必设置EX(过期时间),避免死锁;锁值建议用 UUID + 实例标识,便于排查谁持锁 - Etdc 方案中,
lease.Grant返回的 lease ID 必须绑定到后续所有Put操作,否则续租失败会导致任务被其他节点抢走 - 不要在加锁成功后才解析 cron 表达式——应提前校验合法性,否则锁住了却因语法错退出,造成资源空占
asynq 和 machinery 在 Golang 微服务里怎么选
asynq 基于 Redis,轻量、专注队列调度,自带 Web UI 和重试策略,适合中小规模任务编排;machinery 是跨语言设计,依赖消息中间件(如 RabbitMQ / Redis),强调任务链(chaining)和回调,但 Go client 对 worker 生命周期管理较弱,容易漏掉 panic 恢复。
典型误用是拿 machinery 当纯定时调度器用——它本质是异步任务框架,cron 触发需额外写 adapter 层,而 asynq 原生支持 asynq.NewScheduler 直接注册 func() error 和 cron 表达式。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
asynq的Server默认不开启Concurrency限制,高吞吐任务需手动设concurrency: 4防止 goroutine 泛滥 -
machinery的task.Signature中Args必须是 JSON 可序列化结构体,传func或chan会 panic - 两者都不内置任务依赖图谱,若需 DAG 调度(如 A 完成后触发 B、C),得自己维护状态机或引入
temporal
如何让任务执行结果可靠写入数据库而不丢
任务执行完再写 DB 是经典双写风险点:执行成功但 DB 写失败,状态不一致。正确做法是把 DB 更新作为任务本身的一部分,并利用队列的重试语义兜底——即任务函数内完成全部副作用(DB + 发通知 + 清缓存),失败则由 asynq 自动按 RetryDelay 重入。
切忌把“记录日志”和“更新状态”拆成两个独立任务——网络抖动或进程崩溃时,日志写了、状态没更,监控就会误报。
- DB 操作前先用
context.WithTimeout包裹,防止事务卡住阻塞整个 worker - PostgreSQL 场景下,可用
INSERT ... ON CONFLICT DO UPDATE替代先查后更,避免并发更新覆盖 - 如果任务涉及第三方 HTTP 调用,务必设
http.Client.Timeout,否则默认无超时,worker goroutine 会永久挂起
为什么健康检查接口返回 200 却实际无法消费任务
很多服务只检查自身 HTTP 端口是否通,却忽略 Redis 连接池是否可用、Etcd lease 是否续租成功、DB 连接是否 stale。真实故障常发生在中间件层——比如 Redis 内存满导致 LPUSH 静默失败,或连接池耗尽后 asynq.Client.Enqueue 返回 nil error 但实际没入队。
最容易被跳过的验证点是:worker 启动后是否真的注册到了调度中心(如 consul service registration)、是否监听了正确的 queue name(default vs critical)、以及 Redis 的 CLIENT LIST 里是否有对应 client 名称。
- 在
main()启动后,加一段同步校验:if _, err := rdb.Ping(ctx).Result(); err != nil { log.Fatal(err) } -
asynq.Server的Start方法不阻塞,需显式select {}或signal.Notify等待退出,否则进程秒退 - K8s liveness probe 若只调 HTTP 接口,建议同时读取 Redis 的
INFO命令返回的connected_clients,低于阈值就主动 fail
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










