直接用 session 存导出进度在 ThinkPHP 里必然失败,因 CLI 与 Web 不共享 session 生命周期,$_SESSION 写入后前端无法读取;必须用 Redis 等外部存储,配合初始化、节流更新、强制补全 100%、加锁防重、统一响应结构(status/progress/message)及合理键名与清理策略。

直接用 session 存导出进度在 ThinkPHP 里必然失败——CLI 进程和 Web 请求不共享 session 生命周期,$_SESSION 写进去,前端根本读不到。必须用 Redis 这类外部存储做中转,且要控制更新节奏、加锁兜底、统一响应结构。
为什么不能用 session 或数据库直写进度
CLI 模式(如 php think export:run)下 session_start() 默认无效;即使手动开启,也无持久会话 ID,Web 端无法关联。数据库直写更危险:Db::table('export_log')->where('id', $task_id)->update(['progress' => $p]) 在高并发导出时容易覆盖、丢更新、锁表卡顿。Redis 的原子操作(INCR、SET 带过期)才是可靠选择。
导出任务中如何安全更新 Redis 进度
关键不是“能不能写”,而是“什么时候写、怎么写才不崩”。实操要点:
- 初始化必须前置:
Cache::set('export_123_progress', 0, 3600),显式设过期时间(别依赖默认) - 避免每行数据都写一次:
if ($i % 10 === 0) { Cache::set('export_123_progress', $i / $total * 100); },降低 Redis 压力 - 结束前强制补全:
Cache::set('export_123_progress', 100); Cache::set('export_123_status', 'success');,防止卡在 99% - 防重复执行:用
Cache::store('redis')->lock('export_123_lock', 60)->block(5)包裹整个导出逻辑
前端轮询接口该返回什么结构
别只返回一个数字或简单数组,JS 容易因字段缺失报错。固定三字段结构最稳妥:
立即学习“PHP免费学习笔记(深入)”;
-
status:必须是'running'/'success'/'failed',不能靠progress推断状态 -
progress:强制整数,min(max($val, 0), 100)处理后返回,避免负值或超 100 -
message:失败时填具体原因(如"查询超时:user 表无权限"),成功时留空字符串
示例响应:
{"status":"running","progress":45,"message":""}
Redis 键名设计与清理时机
键名不带业务硬编码,用可预测的命名规则,方便批量清理:
- 进度键:
export_{task_id}_progress - 状态键:
export_{task_id}_status - 错误信息键:
export_{task_id}_error - 清理动作:在任务完成或超时后,用
Cache::deleteMultiple(['export_123_progress', 'export_123_status'])主动删,别等过期——Redis 内存有限,积压太多键会影响性能
最容易被忽略的是:前端轮询频率和后端缓存过期时间必须错开。比如前端每 2 秒查一次,Redis 进度键就别设 1 秒过期,否则可能刚写入就失效,导致进度条“跳变”或卡住。



















