本文目录导读:

在 PHP 中实现数据归档,通常是为了处理海量数据、优化数据库性能或满足法规要求,下面从策略、技术到代码实现,系统性地给你梳理一下。
归档策略选择
首先明确要归档什么、归档到哪里,以及归档后怎么查询。
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 软删除(逻辑归档) | 数据量小,需频繁恢复 | 简单、可逆 | 查询性能下降,数据未真正减少 |
| 分区表归档 | 时间序列数据(日志、订单) | 删除分区极快,管理方便 | 数据库需支持分区(MySQL 8.0+/PG) |
| 历史表迁移 | 业务明确区分热/冷数据 | 主表保持轻量 | 查询需跨表,需处理关联 |
| 文件/对象存储导出 | 合规审计、长期保存 | 节省数据库空间 | 查询必须通过外部工具 |
推荐组合:生产库用 分区表 + 定期把老分区迁移到 归档库/对象存储,同时保留软删除标记用于业务逻辑。
核心实现方案
方案 1:基于分区表的自动归档(MySQL 8.0+)
// 1. 创建分区表(按月分区)
CREATE TABLE `orders` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL,
`status` TINYINT NOT NULL DEFAULT '0',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`, `created_at`) // 注意:主键必须包含分区键
) PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
PARTITION p202401 VALUES LESS THAN (202402),
PARTITION p202402 VALUES LESS THAN (202403),
PARTITION p202403 VALUES LESS THAN (202404)
// 后续可动态添加
);
// 2. 归档调度:每天凌晨3点执行
// 用 cron 执行:0 3 * * * php /path/to/archive.php
// 3. PHP 执行归档逻辑
<?php
class OrderArchiver {
private $pdo;
public function __construct() {
$this->pdo = new PDO(
'mysql:host=localhost;dbname=main_db;charset=utf8mb4',
'root', 'password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
/**
* 归档 3 个月前的数据到归档库
*/
public function archiveOldData(): int {
$archiveMonth = date('Ym', strtotime('-3 months'));
$nextMonth = date('Ym', strtotime('-3 months +1 month'));
try {
// 开启事务
$this->pdo->beginTransaction();
// 创建归档库中的分区(如果不存在)
$this->createArchivePartitionIfNotExists($archiveMonth);
// 步骤1:从主表 SELECT 并 INSERT 到归档库
$sql = "INSERT INTO archive_db.orders (
id, order_no, status, created_at
) SELECT
id, order_no, status, created_at
FROM main_db.orders
WHERE created_at >= ? AND created_at < ?
LIMIT 10000"; // 分批处理防止内存溢出
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
date('Y-01-01 00:00:00', strtotime("-$archiveMonth month")),
date('Y-m-01 00:00:00', strtotime("-$archiveMonth month +1 month"))
]);
$moved = $stmt->rowCount();
// 步骤2:删除主表中已经归档的数据(快速删除)
$deleteSql = "DELETE FROM main_db.orders
WHERE created_at < ?
LIMIT 10000"; // 关联相同条件
$deleteStmt = $this->pdo->prepare($deleteSql);
$deleteStmt->execute([
date('Y-m-01 00:00:00', strtotime('-3 months'))
]);
// 步骤3:手动 DROP 或 TRUNCATE 掉主表中该月分区(最快,推荐)
$this->dropPartition($archiveMonth);
$this->pdo->commit();
return $moved;
} catch (Exception $e) {
$this->pdo->rollBack();
throw $e;
}
}
private function createArchivePartitionIfNotExists($archiveMonth) {
$sql = "ALTER TABLE archive_db.orders ADD PARTITION (
PARTITION p_{$archiveMonth} VALUES LESS THAN (?)
)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$archiveMonth . '01']);
}
private function dropPartition($archiveMonth) {
$sql = "ALTER TABLE main_db.orders DROP PARTITION p_{$archiveMonth}";
$this->pdo->exec($sql);
}
}
// 执行
$archiver = new OrderArchiver();
$count = $archiver->archiveOldData();
echo "归档完成,移动 {$count} 条记录";
方案 2:业务触发式软删除 + 定期批量迁移
适用于业务逻辑需要保留记录,但主表数据量变大需要归档。
<?php
class DataArchiver {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
* 标记软删除(业务层面归档)
*/
public function softArchive(int $userId): bool {
$sql = "UPDATE users SET status = 2, archived_at = NOW() WHERE id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$userId]);
}
/**
* 定时任务:将 archived_at 超过90天的数据物理迁移到 archive.users
*/
public function hardMigration(int $days = 90): int {
$threshold = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$this->db->beginTransaction();
try {
// 1. 批量选取待归档数据
$fetchSql = "SELECT * FROM users WHERE archived_at < ? AND archived_at IS NOT NULL LIMIT 5000";
$fetchStmt = $this->db->prepare($fetchSql);
$fetchStmt->execute([$threshold]);
$rows = $fetchStmt->fetchAll(PDO::FETCH_ASSOC);
// 2. 插入归档库(假设 archive_users 表结构相同)
foreach ($rows as $row) {
$insertSql = "INSERT INTO archive.users (". implode(', ', array_keys($row)) .") VALUES (?, ?, ...)";
// 拼接参数绑定
$this->db->prepare($insertSql)->execute(array_values($row));
}
// 3. 删除原表数据(分批)
$deleteSql = "DELETE FROM users WHERE archived_at < ? AND archived_at IS NOT NULL LIMIT 5000";
$stmt = $this->db->prepare($deleteSql);
$stmt->execute([$threshold]);
$this->db->commit();
return count($rows);
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
}
// 在业务代码中调用软归档
$archiver = new DataArchiver($pdo);
$archiver->softArchive(123);
// 在 cron 中每天调用硬迁移
// $moved = $archiver->hardMigration(180);
方案 3:导出到 CSV/JSON + 压缩存储(适合合规审计)
<?php
class FileArchiver {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
/**
* 将超过一年的日志导出为 CSV 并压缩存储
*/
public function exportLogsToFile(int $olderThanDays = 365): string {
$threshold = date('Y-m-d 00:00:00', strtotime("-{$olderThanDays} days"));
$filePath = '/var/data/archive/' . date('Y-m-d') . '_logs.csv.gz';
// 打开 gzip 写入流
$fp = gzopen($filePath, 'w');
// 写入表头
fputcsv($fp, ['id', 'user_id', 'action', 'ip', 'created_at']);
// 分批查询写入
$offset = 0;
$batchSize = 1000;
while (true) {
$sql = "SELECT id, user_id, action, ip, created_at
FROM activity_logs
WHERE created_at < ?
LIMIT ?, ?";
$stmt = $this->db->prepare($sql);
$stmt->bindValue(1, $threshold);
$stmt->bindParam(2, $offset, PDO::PARAM_INT);
$stmt->bindParam(3, $batchSize, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) break;
foreach ($rows as $row) {
fputcsv($fp, $row);
}
$offset += $batchSize;
}
gzclose($fp);
// 删除原表数据
$deleteSql = "DELETE FROM activity_logs WHERE created_at < ?";
$stmt = $this->db->prepare($deleteSql);
$stmt->execute([$threshold]);
return $filePath;
}
}
最佳实践建议
-
分批处理:永远不要一次性 SELECT 全部数据再归档,用
LIMIT N循环处理。 -
索引优化:
- 归档条件字段(如
created_at)务必加索引 - 软删除字段加复合索引:
(status, archived_at)
- 归档条件字段(如
-
事务控制:数据迁移必须用事务,确保主库和归档库数据一致。
-
失败重试机制:在定时任务中加入日志记录,失败后自动重试或人工介入。
-
归档验证:迁移完成后执行
COUNT(*)对比,确保没有数据丢失。 -
备份保护:归档文件定期做异地备份(OSS/S3/磁带库)。
-
监控:观察归档任务执行时间、归档数据量、磁盘空间变化,设置告警。
扩展方案:归档到 ClickHouse 做分析
如果归档数据还需要进行大数据分析,可以将归档数据直接写入 ClickHouse:
class ClickHouseArchiver {
private $client;
public function __construct() {
// 使用 clickhouse-php-client
$this->client = new ClickHouseClient([
'host' => 'localhost',
'port' => 8123,
'database' => 'analytics'
]);
}
public function archiveOrdersToCH(int $months = 6): void {
// 从 MySQL 导出
$sql = "SELECT * FROM orders WHERE created_at < NOW() - INTERVAL {$months} MONTH";
$stmt = $this->pdo->query($sql);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 批量插入 ClickHouse
$this->client->insert('orders_archive', $rows);
// 删除 MySQL 中记录
$this->pdo->exec("DELETE FROM orders WHERE created_at < NOW() - INTERVAL {$months} MONTH");
}
}
用哪个方案取决于:
- 数据量:百万级用分区+迁移;千万级用分区+定期删除分区;海量需上分布式。
- 查询需求:需要在线查询历史数据就保留在归档库;只需审计就导出文件。
- 基础设施:是否有足够的磁盘/备份空间,是否用云数据库(RDS 自带归档功能)。
基本流程:软删除(业务标记) → 定期迁移(到归档库/文件) → 物理删除(清空主表)= 闭环归档。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播