PHP项目清理历史数据如何分批执行

wen PHP项目 21

本文目录导读:

PHP项目清理历史数据如何分批执行

  1. 最基础:LIMIT + OFFSET 循环(适合低频、后台脚本)
  2. 高效方案:基于主键游标(推荐,千万级表可用)
  3. 生产环境:队列 + 守护脚本(适合Crontab/Supervisor)
  4. 极致性能:按物理分区(MySQL Partition)
  5. 时间-数量双重控制(超时保护)
  6. 常见注意事项
  7. 选择建议

在PHP项目中清理历史数据时,分批执行是避免内存溢出、数据库锁死或超时的关键策略,以下是几种常见且可靠的分批执行方案,按场景和复杂度排序:


最基础:LIMIT + OFFSET 循环(适合低频、后台脚本)

<?php
// 每批处理 1000 条
$batchSize = 1000;
$offset = 0;
do {
    // 1. 查询需要删除的ID(避免直接删除时锁定大量行)
    $stmt = $pdo->prepare("SELECT id FROM logs WHERE created_at < :cutoff ORDER BY id LIMIT :limit OFFSET :offset");
    $stmt->execute([
        ':cutoff' => '2024-01-01 00:00:00',
        ':limit'  => $batchSize,
        ':offset' => $offset
    ]);
    $ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
    if (empty($ids)) {
        break;
    }
    // 2. 一次性删除这批ID
    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $pdo->prepare("DELETE FROM logs WHERE id IN ($placeholders)")->execute($ids);
    $offset += $batchSize;
    // 3. 可选:释放内存、记录进度
    echo "已删除至偏移量: $offset\n";
    gc_collect_cycles(); // 强制内存回收
    usleep(50000); // 每批暂停50ms,降低数据库压力
} while (true);

缺点:大表时 OFFSET 越往后越慢(需扫描已跳过的行)。


高效方案:基于主键游标(推荐,千万级表可用)

<?php
$batchSize = 1000;
$lastId = 0; // 从最小ID开始
$cutoffDate = '2024-01-01 00:00:00';
do {
    // 只查主键,且按ID升序取N条
    $ids = $pdo->prepare("SELECT id FROM logs 
                          WHERE id > :lastId AND created_at < :cutoff 
                          ORDER BY id 
                          LIMIT :limit");
    $ids->execute([
        ':lastId' => $lastId,
        ':cutoff' => $cutoffDate,
        ':limit'  => $batchSize
    ]);
    $ids = $ids->fetchAll(PDO::FETCH_COLUMN);
    if (empty($ids)) {
        break;
    }
    // 删除这批数据
    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $pdo->prepare("DELETE FROM logs WHERE id IN ($placeholders)")->execute($ids);
    // 更新游标
    $lastId = end($ids);
    echo "已处理至ID: $lastId\n";
    gc_collect_cycles();
} while (true);

优点

  • 避免 OFFSET,利用索引((id, created_at) 复合索引更佳)
  • 每批之间无重复扫描,性能稳定

生产环境:队列 + 守护脚本(适合Crontab/Supervisor)

1 分批写入任务表

<?php
// create_cleanup_tasks.php (每小时或每日执行一次)
$batchSize = 5000;
$lastId = 0;
// 找出所有需要删除的ID范围,生成任务
do {
    $ids = $pdo->prepare("SELECT id FROM logs 
                          WHERE id > :lastId AND created_at < :cutoff 
                          ORDER BY id LIMIT :limit");
    // ... 同上获取 $ids
    if (empty($ids)) break;
    // 将批次ID写入任务表(或Redis队列)
    $taskData = json_encode($ids);
    $pdo->prepare("INSERT INTO cleanup_tasks (data, created_at) VALUES (?, NOW())")
         ->execute([$taskData]);
    $lastId = end($ids);
} while (true);

2 消费者逐批执行

<?php
// worker.php (由 Supervisor 持续运行,或 Crontab 每分钟执行)
$pdo->beginTransaction();
$task = $pdo->prepare("SELECT id, data FROM cleanup_tasks WHERE processed = 0 LIMIT 1 FOR UPDATE");
$task->execute();
$row = $task->fetch();
if (!$row) {
    exit("暂无待清理任务\n");
}
$ids = json_decode($row['data'], true);
try {
    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $pdo->prepare("DELETE FROM logs WHERE id IN ($placeholders)")->execute($ids);
    $pdo->prepare("UPDATE cleanup_tasks SET processed = 1 WHERE id = ?")->execute([$row['id']]);
    $pdo->commit();
    echo "已删除 " . count($ids) . " 条\n";
} catch (Exception $e) {
    $pdo->rollBack();
    // 记录错误,可选重试
}

极致性能:按物理分区(MySQL Partition)

如果表按时间分区(如按月),一个SQL即可清理整个分区:

ALTER TABLE logs DROP PARTITION p202401, p202402;
-- 或用 TRUNCATE PARTITION
ALTER TABLE logs TRUNCATE PARTITION p202401;

配合PHP

<?php
// 获取要清除的分区列表
$partitions = $pdo->query("SELECT PARTITION_NAME FROM INFORMATION_SCHEMA.PARTITIONS 
                           WHERE TABLE_NAME = 'logs' AND PARTITION_DESCRIPTION < 20240101");
foreach ($partitions as $row) {
    $pdo->exec("ALTER TABLE logs DROP PARTITION {$row['PARTITION_NAME']}");
}

优点:瞬间清理,不影响在线业务。


时间-数量双重控制(超时保护)

<?php
$maxExecutionTime = 58; // 预留2秒退出
$batchSize = 1000;
$startTime = time();
$lastId = 0;
do {
    if (time() - $startTime >= $maxExecutionTime) {
        echo "达到最大执行时间,暂停并记录断点...\n";
        // 可选:将 $lastId 存入 Redis 或文件,下次从此继续
        file_put_contents('/tmp/cleanup_cursor.txt', $lastId);
        break;
    }
    // ... 游标方式获取 $ids 并删除 ...
} while (!empty($ids));

适用场景:Crontab 每隔几分钟执行,防止单次运行时间过长。


常见注意事项

问题 解决方案
大事务 每批单独提交(pdo->commit() / 不开启显式事务)
锁冲突 使用 DELETE ... WHERE id IN (...) 而非 DELETE ... WHERE created_at < ...(避免全表锁)
从库延迟 每批后 usleep(200000)(0.2秒)或检查主从延迟 SHOW SLAVE STATUS
慢查询 确保 WHERE 条件有索引,如 (created_at, id) 复合索引
内存泄漏 每批后 unset($ids) + gc_collect_cycles()
断点续传 将游标存入 Redis / MySQL 状态表 / 文件

选择建议

数据量 方案
< 10万 方案1(简单直接)
10万 ~ 500万 方案2(游标,最常用)
500万 ~ 1亿 方案3(队列异步)或方案4(分区)
实时业务 方案4(分区无锁)或方案2+LOW_PRIORITY

推荐默认使用方案2(基于主键游标),它是性能、复杂度、安全性的最佳平衡点。

抱歉,评论功能暂时关闭!