You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP长时CSV导入刷新续跑时如何安全存储进度变量

原始问题

我编写了用于读取CSV文件并将数据导入数据库的PHP脚本,每次脚本启动时会通过$_POST获取客户名称,完整导入12万行数据约需7分钟,但当前主机对PHP脚本的最长运行时间限制为165秒。
为此我计划在脚本运行达到165秒时限前主动刷新页面,从上一次中断的行位置重新开始导入流程。目前我已实现页面自动刷新逻辑,但无法在刷新后持久化保存记录中断行位置的变量,供后续续跑时调用。
我原本考虑使用$_POST或$_SESSION存储该进度值,但由于该脚本可能被同时触发多次,每次运行导入的CSV文件各不相同,我担心并发运行的脚本修改这两类超全局变量时会产生冲突,导致变量值被非预期篡改。

核心疑问:

  1. 上述担忧是否成立?
  2. 如果担忧成立,应如何存储脚本刷新前记录的中断行号?
  3. 我目前想到的方案是新建独立文件存储相关信息再读取使用,存储格式如下,请问是否存在更简便高效的解决方案?
customer_name : Jon
row_ended : 10584

customer_name : Jane
row_ended : 11564
解决方案

你的担忧完全成立。

  • 对于$_POST:POST请求本身无状态,普通页面刷新默认是GET请求,之前携带的POST参数根本不会自动传递;就算用JS自动提交表单模拟POST刷新,多任务并行时也没有天然隔离性,很容易传错参数。
  • 对于$_SESSION:如果同一个浏览器会话(同个Cookie下)同时跑多个不同客户的导入任务,你如果把进度存在会话的同一个键下,必然会出现后写入的进度覆盖之前任务进度的情况——比如你先跑Jon的导入写到10584行,再开新标签页跑Jane的导入写到11564行,Jon的任务刷新时读到的进度会直接变成Jane的11564行,直接导错数据。

以下是按简便高效优先级排序的可行存储方案:

1. 优先用数据库存进度(最省事)

你本身就在用数据库做数据导入,完全不需要额外维护存储逻辑,建一张极简的进度表就行:

CREATE TABLE csv_import_progress (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  import_batch_id VARCHAR(32) NOT NULL, -- 每次导入生成的唯一ID
  customer_name VARCHAR(64) NOT NULL,
  csv_file_path VARCHAR(255) NOT NULL,
  row_ended INT UNSIGNED DEFAULT 0,
  file_offset INT UNSIGNED DEFAULT 0, -- 可选:存上次读CSV的字节位置,提速用
  updated_at INT UNSIGNED NOT NULL,
  UNIQUE KEY `batch_id` (`import_batch_id`)
);

使用逻辑非常简单:

  • 第一次触发导入时,先验证POST过来的客户名和上传的CSV文件,然后用uniqid('import_', true)生成一个全局唯一的import_batch_id存入进度表,后续所有续跑请求都把这个import_batch_id带上(不管是拼在跳转URL里,还是放在自动提交的表单里)。
  • 每次脚本启动时,先根据URL/POST里的import_batch_id查进度表,拿到上次的row_ended,直接从对应的行开始导,不用从头重数。
  • 脚本运行150秒左右(留15秒缓冲),把当前的row_ended、文件偏移量更新到进度表,然后带着import_batch_id跳回当前脚本续跑即可。
    方案优势:
  • 完全没有并发冲突:每个任务对应唯一的batch ID,哪怕同时跑100个导入任务,各查各的进度,根本不会串。
  • 不需要处理锁逻辑:数据库本身有行级锁,多请求更新同一条记录时会自动排队,不会写坏数据。
  • 运维成本极低:写个简单的定时任务,每天删一下超过24小时还没跑完的进度记录就行,排查问题直接查表就能看到哪个任务卡在哪。

2. 如果实在不想动数据库,优化文件存储方案

你原本想把所有进度拼在同一个文件里的写法,多并发写的时候需要加文件锁(flock),不然很容易因为多进程同时写把文件内容冲乱。其实可以更简单:

  • 同样先生成唯一的import_batch_id,每个任务单独存一个进度文件,文件名就用这个batch ID,比如storage/import_progress/progress_64f1a2b3c4d5.json。
  • 文件内容用JSON格式存储,读写都方便:
{
  "customer_name": "Jon",
  "csv_path": "/uploads/2024/jon_sales.csv",
  "row_ended": 10584,
  "file_offset": 2048000,
  "updated_at": 1716701234
}

这样每个进程只操作自己对应的那一个进度文件,别的进程根本碰不到,连文件锁都不需要加,比把所有进度堆在同一个文件里维护成本低太多。

容易踩的坑提醒

  • 绝对不要用客户名作为进度的唯一标识:同一个客户可能同时上传多个不同的CSV要导入,用客户名做标识还是会出现进度串的问题,一定要绑定本次导入专属的唯一batch ID。
  • 不要卡着165秒的超时线才存进度跳转:至少留10-15秒的缓冲,不然刚好卡到PHP强制终止的时间点,你最新的进度还没存到存储里,下次又要重跑一大段。
  • 读CSV的时候可以优化下:不要每次续跑都从头逐行数到上次的row_ended,要么直接存上次读到的文件字节偏移量,用fseek直接跳过去,能省不少启动时间。

内容的提问来源于stack exchange,提问作者Bebert Beberr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.30 17:45:03