不能直接用goroutine做长期异步任务,因为goroutine无持久化机制,进程重启、oom或panic会导致任务永久丢失;必须通过redis stream、sqlite等持久化存储实现跨进程恢复和“至少一次”语义。

为什么不能直接用 goroutine 做长期异步任务
goroutine 退出后任务就丢了,进程重启、OOM 或 panic 都会导致正在跑的 go func() 永久丢失。这不是“偶尔丢”,而是“必然丢”——只要没落盘,就不算执行过。
真正需要持久化的场景:支付回调重试、邮件发送、报表生成、第三方 API 轮询。这些任务必须满足「至少一次」语义,且能跨进程恢复。
- 别把
time.AfterFunc或select { case 当持久化方案——它们纯内存,不抗重启 - 别在 HTTP handler 里起 goroutine 后就
return——客户端收到 200 不代表任务已开始,更不代表会完成 - 数据库事务提交前不能发任务(否则事务回滚,任务却已入队)
用数据库表 + 定时轮询实现最小可行方案
不用引入 Redis/Kafka,仅靠 PostgreSQL/MySQL 就能起步。核心是建一张 async_tasks 表,字段至少包含:id、type(如 "send_email")、payload(JSON)、status("pending"/"processing"/"succeeded"/"failed")、attempts、next_run_at、created_at、updated_at。
worker 启动后,用固定间隔(比如 1s)查:SELECT * FROM async_tasks WHERE status = 'pending' AND next_run_at ,然后用 <code>UPDATE ... SET status = 'processing' WHERE id = ? AND status = 'pending' 做乐观锁抢占。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- UPDATE 必须带
status = 'pending'条件,否则并发时多个 worker 可能处理同一任务 -
next_run_at初始设为NOW(),失败后按退避策略更新(如NOW() + INTERVAL '2^attempts SECOND') - worker 处理完要 UPDATE 成
succeeded或failed,不能只改status忘记写updated_at
如何避免任务重复执行与漏执行
关键不在“怎么发”,而在“怎么确认发成功了”。重复和漏的本质都是状态跃迁缺失或不可原子。
- 插入任务时用
INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或INSERT IGNORE(MySQL),靠唯一键(如task_type + business_id)防重入 - worker 抢占后,必须先
UPDATE状态再执行逻辑;若执行中 panic,靠定时任务扫描status = 'processing'且updated_at 的记录,重置为 <code>pending - 不要依赖外部系统回调来更新状态——比如等邮件服务商 webhook 再改 DB,而应本地执行完再调 API;失败则记
failed并写error_message字段,方便人工介入
什么时候该换消息队列
当单机轮询撑不住时:任务量 > 1k/s、延迟要求
换之前先确认:DB 轮询不是性能瓶颈,而是设计瓶颈——比如无法做优先级队列、无法按 tag 过滤、无法跨服务共享任务流。
- 用 Kafka 时,务必开启
enable.idempotence=true,consumer 处理完再commitoffset,别 auto-commit - 用 RabbitMQ 时,用
acknowledgement mode = manual,且requeue=false失败时,让 DLX 转投死信队列 - 无论哪种队列,payload 仍建议存 JSON,但不要塞大文件——把实际数据放对象存储,队列里只传
bucket/key
最易被忽略的一点:所有异步任务入口(HTTP/API/定时触发)必须自带幂等 key,并透传到底层 worker。没有这个,换再强的队列也救不了重复消费问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










