time.ticker仅适用于开发/测试环境或非关键归档任务,因其无崩溃恢复、去重及分布式协调能力;归档逻辑必须幂等,如delete from logs where created_at
用
time.Ticker做简单归档任务,但别在生产环境直接裸用单机轻量归档(比如每天压缩旧日志、清理 30 天前的测试数据),
time.Ticker最快上手。但它不处理崩溃恢复、重复执行、分布式抢占——一旦进程挂了,当天归档就丢了;如果程序重启两次,可能触发两次归档。实操建议:
- 只用于开发/测试环境,或对精度和可靠性无要求的辅助任务
- 归档逻辑必须幂等:
DELETE FROM logs WHERE created_at 这类带状态标记的条件比单纯按时间删更安全- 启动时加一次立即执行(
ticker.C ),避免首次等待整周期- 务必用
context.WithTimeout包裹归档操作,防止某次归档卡死拖垮整个 ticker
robfig/cron/v3是最常用选择,但要注意时区和并发控制它支持类 Unix cron 表达式,自动管理调度、错误重试(可配)、支持
func() error类型任务。但默认是本地时区,且多个相同任务实例会同时触发——这在归档场景下极易导致双删、重复压缩。实操建议:
- 显式设置时区:
cron.New(cron.WithLocation(time.UTC)),避免服务器时区变更引发错乱- 用
cron.WithChain(cron.Recover(cron.DefaultLogger), cron.DelayIfStillRunning(cron.DefaultLogger))防止上次没跑完,下次又进来了- 归档前先尝试获取分布式锁(哪怕只是用 Redis
SET lock:archive NX EX 300),否则多实例部署时必出问题- 不要把数据库连接、文件句柄写死在全局变量里——
cron会复用同一个函数实例,连接可能超时失效归档 SQL 要分批执行,不然容易 OOM 或锁表
直接
DELETE FROM events WHERE created_at 看似简洁,实际可能扫全表、占满事务日志、阻塞其他写入,甚至让 MySQL 主从延迟飙升。实操建议:
- 永远用主键或自增 ID 分页删:
DELETE FROM events WHERE id ,循环直到影响行为 0- 归档前先
SELECT MIN(id), MAX(id) FROM events WHERE created_at ,预估范围,避免无限循环- 如果目标表有大字段(
TEXT/BLOB),考虑先UPDATE ... SET archived = true, data = NULL标记,再异步清空,降低单次事务体积- PostgreSQL 用户注意:
DELETE不释放磁盘空间,后续要手动VACUUM;MySQL 的 InnoDB 则依赖 purge 线程,别指望立刻回收真正可靠的归档系统得绕过“定时”本身
靠时间驱动的任务本质脆弱:时钟跳变、节点漂移、Cron 表达式歧义(
0 0 * * *到底是 UTC 还是本地?)、K8s Pod 重建后调度延迟……归档的核心诉求其实是“数据达到生命周期阈值后被处理”,不是“每天凌晨两点执行”。实操建议:
- 把归档逻辑下沉到写入路径:插入新记录时,顺便检查并触发老数据归档(适合低频写+高确定性生命周期的场景)
- 用消息队列做延迟归档:写入时发一条
archive_after_30d延迟消息,由消费者真正执行归档——天然解耦、可追溯、失败可重投- 对超大数据量(TB 级),放弃 SQL 归档,改用导出为 Parquet + 对象存储 + 生命周期策略(如 AWS S3 桶策略自动转 Glacier),Go 只负责触发导出和元数据更新
归档最难的部分从来不是“怎么定时”,而是“怎么确保这次删的是该删的、只删一次、删完还能查到痕迹”。时间只是入口,状态、幂等、可观测性才是落地关键。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!












