归档任务应选用 cron 包(如 robfig/cron/v3),因其支持标准 cron 表达式(如“每月1号凌晨2点”)、并发控制、panic 捕获、context 超时及持久化状态,而 time.Ticker 无表达式能力、重启即丢失、易堆积且缺乏生产级容错机制。

归档任务该用 time.Ticker 还是 cron 包?
直接用 time.Ticker 做定时归档,容易在程序重启后丢失调度状态,且不支持表达式(比如“每月1号凌晨2点”)。生产环境推荐用 cron 包(如 robfig/cron/v3),它支持标准 cron 表达式、任务并发控制、错误日志钩子,还能和 Go 的 context 配合做超时退出。
常见错误:把归档逻辑写在 Ticker.C 通道的 for-select 循环里,没加 recover 或超时判断,一次数据库超时就卡死整个 ticker。
- 用
cron.New(cron.WithChain(cron.Recover(cron.DefaultLogger)))自动捕获 panic - 归档函数入口加
ctx, cancel := context.WithTimeout(ctx, 30*time.Minute),避免单次归档拖垮调度器 - 表达式别写
* * * * *(每秒执行),归档类任务建议最低粒度为分钟级,且需配合数据量判断(见下节)
归档前必须检查数据是否可删?
误删未同步完成或仍有查询依赖的数据,是归档事故最高发原因。不能只看时间戳字段是否早于阈值,得确认三件事:
- 目标表是否有正在运行的长事务(查
pg_stat_activity或 MySQLINFORMATION_SCHEMA.PROCESSLIST) - 归档目标(如 S3、冷备库)是否可写且空间充足(调
os.Stat检查本地路径,或用 SDK 调HeadBucket) - 业务层是否标记“已归档就绪”——比如订单表有
archive_status字段,值为"ready"才允许移动
示例判断逻辑:
if !isArchiveReady(orderID) || !isColdStorageAvailable() {<br> log.Warn("skip archive", "order_id", orderID)<br> return<br>}
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
用 COPY 还是 INSERT INTO ... SELECT?
从主库迁移数据到归档库时,INSERT INTO ... SELECT 在大数据量下会锁源表、占大量内存;而 COPY(PostgreSQL)或 LOAD DATA INFILE(MySQL)走文件中转,更高效但需额外磁盘空间和权限。
性能差异明显:100 万行数据,INSERT ... SELECT 可能耗时 8 分钟并引发主库 CPU 尖峰;COPY 通常 90 秒内完成,且对主库影响小。
- PostgreSQL 场景优先用
pgx.CopyFrom+ 临时 CSV 文件,避免拼 SQL 注入风险 - MySQL 若无法用
LOAD DATA(权限受限),改用分批次SELECT ... FOR UPDATE SKIP LOCKED+ 批量INSERT,每次最多 5000 行 - 无论哪种方式,归档后必须校验行数:
SELECT COUNT(*) FROM archived_table WHERE archive_time = '2024-06'对比源表删除数
框架集成时如何避免定时任务堆积?
用 Gin/Echo 启动 cron 时,如果归档任务执行时间超过调度间隔(比如设了每 5 分钟跑一次,但某次归档花了 7 分钟),默认会并发执行新实例——导致两个归档进程同时操作同一张表,轻则报唯一键冲突,重则数据错乱。
- 给 cron job 加
cron.WithSingletonMode(cron.SkipIfStillRunning),跳过未结束的旧任务 - 在归档函数开头尝试获取分布式锁(如 Redis
SET archive:orders:202406 NX EX 3600),失败直接 return - 记录上次成功归档时间戳到数据库(非内存变量!),下次启动先读它,跳过已处理的周期,防止重启后重复归档
最容易被忽略的是锁的释放时机:必须用 defer + redis.Del,且确保在所有 return 路径前执行,否则锁永远不释放。

















