PHP长时CSV导入刷新续跑时如何安全存储进度变量
原始问题
我编写了用于读取CSV文件并将数据导入数据库的PHP脚本,每次脚本启动时会通过$_POST获取客户名称,完整导入12万行数据约需7分钟,但当前主机对PHP脚本的最长运行时间限制为165秒。
为此我计划在脚本运行达到165秒时限前主动刷新页面,从上一次中断的行位置重新开始导入流程。目前我已实现页面自动刷新逻辑,但无法在刷新后持久化保存记录中断行位置的变量,供后续续跑时调用。
我原本考虑使用$_POST或$_SESSION存储该进度值,但由于该脚本可能被同时触发多次,每次运行导入的CSV文件各不相同,我担心并发运行的脚本修改这两类超全局变量时会产生冲突,导致变量值被非预期篡改。
核心疑问:
- 上述担忧是否成立?
- 如果担忧成立,应如何存储脚本刷新前记录的中断行号?
- 我目前想到的方案是新建独立文件存储相关信息再读取使用,存储格式如下,请问是否存在更简便高效的解决方案?
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
相关产品推荐
相关产品推荐

