ThinkPHP无内置归档机制,须手动实现:建结构一致(字段名/类型/长度/NULL性/主键)且无外键的归档表,为高频字段建索引;用INSERT INTO...SELECT+DELETE分批迁移并校验ROW_COUNT();定时脚本需archive_log表实现互斥锁、断点续传与状态追踪;统一归档逻辑至ArchiveService服务层,保障跨表原子性与后续可查性。

ThinkPHP 没有内置的 archive() 方法或自动归档机制,所有归档动作必须手动编码实现——要么用原生 SQL 分批迁移,要么在模型层封装逻辑,但底层仍依赖 MySQL 的 INSERT INTO ... SELECT 和 DELETE。别指望配置开关就能完成。
建归档表必须字段一致、去外键、加索引
归档不是复制粘贴表结构,稍有偏差就会导致后续迁移失败或查询变慢:
-
created_at、id、state等字段名、类型(如DATETIMEvsTIMESTAMP)、长度(VARCHAR(255)不能缩成VARCHAR(191))、是否允许NULL必须和原表完全一致 - 主键保留,否则
SELECT后无法高效DELETE或做唯一校验 - 外键约束一律删除——归档表不参与业务关联,加上会拖慢插入,且 MySQL 在
INSERT SELECT时可能因外键检查报错Cannot add or update a child row: a foreign key constraint fails - 为高频查询字段(如
created_at、user_id)单独建索引,否则SELECT * FROM article_archive WHERE created_at 会全表扫描 - 引擎建议用
ARCHIVE(仅写入、高压缩),但注意它不支持UPDATE和DELETE;若需后续修正,改用压缩版InnoDB或MyISAM
用 Db::execute() 分批执行 INSERT SELECT,别用 Db::select() + 循环插入
常见错误是先 Db::name('article')->where(...)->select() 拿出全部数据,再 foreach 插入。这会导致 PHP 内存爆满、超时,尤其当待归档数据超 10 万行时。
正确做法是把计算和搬运交给 MySQL 原生执行:
立即学习“PHP免费学习笔记(深入)”;
Db::execute("INSERT INTO article_archive SELECT * FROM article WHERE created_at < '2025-01-01' LIMIT 1000");
Db::execute("DELETE FROM article WHERE created_at < '2025-01-01' LIMIT 1000");
- 每次只处理 1000 行,避免锁表太久,也降低事务日志压力
- 删完立刻执行
ANALYZE TABLE article(更新统计信息,防后续查询走错索引) - 大删后建议对 InnoDB 表执行
OPTIMIZE TABLE article(回收空闲页,但生产环境慎用,会锁表) - 务必用
ROW_COUNT()校验迁移数量是否一致,例如:Db::query("SELECT ROW_COUNT() as cnt")[0]['cnt']
定时归档脚本必须加互斥锁和断点续传
直接丢个 PHP 脚本进 Linux Cron,跑着跑着就重复归档、漏数据、甚至删错时间范围——这不是小概率事件,而是必然发生。
- 不加锁:两个 cron 同时触发,同一段数据被插两次、删两次,归档表主键冲突或原表数据丢失
- 无状态:脚本中途崩溃(如 MySQL 连接中断),下次从头跑,重复操作已处理过的数据
- 没日志:无法追溯哪次失败、影响了哪些记录、是否需要人工补偿
- 解决方案:建一张
archive_log表,字段含table_name、condition_time、last_id(或最后处理的created_at)、status(running/success/failed)、start_time、end_time - 每次执行前查最近一条
status = 'running'的记录,若有,直接退出;否则插入新记录并设为 running;成功后更新为 success 并写入end_time
DAO 层不要硬写 archiveOldData(),而要统一收口到迁移服务
有人在 ArticleModel 里加个 archiveOldData(),控制器一调就跑——看似简单,实则埋雷:
- 多个模型各自实现,逻辑分散,未来改归档条件(比如从“6个月前”变成“订单完成30天后”)得改十几处
- 没有统一事务边界,跨表归档(如订单+订单项)无法保证原子性
- 缺乏统一的进度上报、异常重试、失败告警能力
- 推荐做法:抽一个
ArchiveService类,接收表名、时间字段、阈值、批次大小等参数,内部统一调用Db::execute()和日志写入,对外提供run(string $table, string $timeField, string $cutoff)接口 - 控制器或命令行只负责传参,不碰 SQL;后续加 Kafka 异步归档、对象存储导出,也都只改这个服务,不影响上层
真正难的不是写几行 SQL,而是归档过程中的状态一致性、失败可恢复、以及冷数据后续还能不能被查到——比如用户按订单号查三年前的单,你得确保那个订单号在归档表里有索引,且应用层能自动路由过去。这些细节一旦忽略,归档就从优化变成隐患。



















