归档任务不能直接用time.Ticker跑在主goroutine里,因其阻塞或panic会拖垮整个服务;归档常涉及数据库批量操作、文件写入、网络调用,易超时或失败,需用context.WithTimeout封装、返回可区分的error、避免在HTTP handler中启动goroutine,并通过cron框架实现可恢复、可监控的后台任务。

归档任务为什么不能直接用 time.Ticker 跑在主 goroutine 里
因为 time.Ticker 阻塞或 panic 会拖垮整个服务,尤其归档逻辑常涉及数据库批量操作、文件写入、网络调用——这些都可能超时或失败。框架(如 Gin、Echo、Gin+GORM)本身不提供归档生命周期管理,得自己封装可恢复、可监控的后台任务。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
context.WithTimeout包裹每次归档执行,避免单次卡死 - 归档函数必须返回 error,且错误要区分「临时失败」(如 DB 连接抖动)和「永久失败」(如表结构变更未同步)
- 不要在
http.HandlerFunc里启动归档 goroutine——HTTP 请求结束,goroutine 可能被意外回收
GORM + Cron 实现按天归档的最小可行代码
假设你有一张 orders 表,想把创建时间早于 90 天的数据迁入 orders_archive,并从原表删除。关键不是“怎么写 SQL”,而是“怎么确保原子性 + 可重入”。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
cron.New(cron.WithChain(cron.Recover()))启动 cron,自动捕获 panic - 归档前先查
SELECT MIN(created_at) FROM orders WHERE created_at ,确认有数据再操作,避免空跑 - 用事务包裹「INSERT INTO ... SELECT」和「DELETE」,但注意:MySQL 的
INSERT ... SELECT ... DELETE在同一个事务里可能触发锁升级,建议分两步,中间加WHERE id IN (SELECT id ... LIMIT 5000)控制批次 - 每次只处理 5000 行,用
for rowsAffected > 0循环,防止单次事务过大
func archiveOrders(db *gorm.DB) error {
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if tx.Error != nil {
return tx.Error
}
<pre class="brush:php;toolbar:false;">var ids []uint64
if err := tx.Raw(`SELECT id FROM orders WHERE created_at < ? ORDER BY id LIMIT 5000`, time.Now().AddDate(0,0,-90)).Scan(&ids).Error; err != nil {
tx.Rollback()
return err
}
if len(ids) == 0 {
return nil // 无数据,正常退出
}
if err := tx.Table("orders_archive").Select("*").Where("id IN ?", ids).CreateInBatches(&[]Order{}, 5000).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Where("id IN ?", ids).Delete(&Order{}).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error}
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
归档后如何安全清理索引与统计信息
很多人只做数据迁移,忽略后续影响:旧表 DELETE 后 B+ 树碎片高、ANALYZE TABLE 没触发、分区表元数据没更新——导致后续查询变慢,甚至归档任务自身越来越慢。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- PostgreSQL 用户:归档完成后立即执行
VACUUM orders(非 FULL),并手动ANALYZE orders - MySQL 用户:归档完跑
OPTIMIZE TABLE orders(注意会锁表,建议在低峰期或用ALGORITHM=INPLACE) - 如果用了分表(如
orders_202310),归档完记得删物理表,并从分表路由配置中移除该表名,否则下个月还往里写 - 所有 DDL 操作必须加超时,比如用
db.ExecContext(ctx, "ANALYZE TABLE orders"),ctx 设为 30 秒
线上环境必须加的三个防护开关
归档是“温柔的破坏性操作”,没开关就等于没刹车。以下三个 flag 必须通过配置中心或命令行传入,不能硬编码。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
-
--dry-run=true:只打印将要归档的 ID 范围和 SQL,不执行任何写操作 -
--max-delete=10000:单次归档最多删 1w 行,超出则跳过本次,打告警日志 -
--enable-archive=false:默认关,上线后人工确认后再打开,避免发布即归档
真正容易被忽略的,是归档窗口和业务低峰期的对齐粒度——别只看“凌晨 2 点”,要结合你的监控看 CPU/IO/DB QPS 曲线,找到那个真实的“谷底 30 分钟”,把 cron.Every(30m) 改成精确到分钟的 cron.At("0 2 * * *"),再加个随机 offset 避免集群雪崩。

















