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

AJAX异步调用PHP长任务致页面阻塞的解决方案

问题根因

你遇到的页面阻塞问题是两个核心原因导致的:

  • Session文件锁阻塞:PHP默认以文件形式存储Session,请求调用session_start()后会独占锁定Session文件,直到当前请求完全执行结束才会释放锁。你的原代码在整个长耗时任务执行期间都持有Session锁,其他页面请求要读写Session必须等锁释放,自然会出现长时间加载。
  • 长任务同步占用Web进程:批量补单逻辑需要循环调用第三方cURL接口,本身耗时极长,你直接把这段逻辑放在HTTP请求的同步响应流程里执行,就算解决了Session锁问题,也会长期占用PHP-FPM/Apache的工作进程,并发量上来后会直接占满所有Web进程,导致站点无法访问。
解决方案

核心思路是把长耗时的补单逻辑和Web请求响应流程解耦,Web接口只负责触发任务、立刻返回结果,补单逻辑完全在后台运行,同时提前释放Session锁,彻底避免阻塞其他请求。


第一步:快速临时修复(仅解决Session阻塞,不推荐长期用)

如果你暂时不想调整架构,可以先通过提前释放Session锁解决页面卡住的问题:

注意:这个方案只是让其他页面能正常打开,长任务依然会占用Web进程,订单量较大时依然会拖慢站点。

  1. 所有需要读写Session的逻辑(比如管理员权限校验)全部放在代码最前面执行,执行完立刻调用session_write_close()释放Session锁,后续执行数据库查询、cURL请求的长逻辑时,就不会阻塞其他请求的Session访问。
  2. 不要把任务执行结果存在$_SESSION里,改成存到数据库、临时文件或者缓存中,避免需要重新打开Session锁。

第二步:后台异步任务方案(彻底解决问题,生产环境推荐)

这个方案下AJAX请求触发任务后会立刻得到响应,补单逻辑完全在后台CLI进程中运行,完全不占Web服务进程,任务执行期间站点访问完全不受影响。

前置准备

首先新建一张后台任务表,用来记录任务状态和执行结果,执行以下SQL:

CREATE TABLE `background_tasks` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `task_id` varchar(32) NOT NULL DEFAULT '',
  `task_type` varchar(32) NOT NULL DEFAULT '',
  `status` varchar(16) NOT NULL DEFAULT 'pending',
  `result` text,
  `create_time` datetime DEFAULT NULL,
  `finish_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `task_id` (`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

然后新建独立的CLI执行脚本run_refill_task.php,这个脚本只允许命令行运行,专门处理补单逻辑:

<?php
// 禁止Web访问
if (php_sapi_name() !== 'cli') {
    exit('Access denied');
}
// 引入你项目的初始化文件(加载数据库连接、SMMApi类等,按实际路径修改)
require __DIR__ . '/init.php';

// 接收命令行传入的任务ID、站点ID
$task_id = $argv[1];
$web_id = $argv[2];
$error = 0;
$success = 0;
$smmapi = new SMMApi();

// 标记任务为处理中
$stmt = $conn->prepare("UPDATE background_tasks SET status = 'processing' WHERE task_id = :tid");
$stmt->execute(['tid' => $task_id]);

try {
    // 原补单查询逻辑
    $orders = $conn->prepare("SELECT * FROM orders INNER JOIN service_api ON service_api.id=orders.order_api WHERE orders.website_id=:web_id ");
    $orders->execute(array("web_id"=> $web_id));
    $orders = $orders->fetchAll(PDO::FETCH_ASSOC);

    foreach($orders as $order){
        $result =  $smmapi->action(array('key' =>$order['api_key'],'action' =>'refill','order' =>$order['api_orderid']),$order['api_url']);
        $result = json_decode( json_encode($result), true);
      
        if(!empty($result['error'])){
            $error++;
        }else{
             if(!empty($result['refill'])){
                $conn->beginTransaction();
                $insert= $conn->prepare("INSERT INTO refill SET refill_order_id=:order_id, refill_api_order_id=:api_order_id, refill_client_id=:client_id, refill_service_id=:service_id, refill_mode=:mode, refill_status=:status, last_updated=:updated, refill_created=:created, website_id=:web_id ");
                $insert_res = $insert->execute(array("order_id"=>$order["order_id"],"api_order_id"=>$result['refill'],"client_id"=>$order['client_id'],"service_id"=>$order['service_id'],"mode"=>"auto","status"=>"pending","updated"=>date("Y-m-d H:i:s"),"created"=>date("Y-m-d H:i:s"),"web_id"=>$web_id ));
                if( $insert_res ){
                  $conn->commit();
                  $success++;
                }else{
                  $conn->rollback();
                  $error++;
                }
              }else{
                  $error++;
              }
        }
    }

    if($orders){
        $result_text = "<strong>Command executed successfully</strong> <br> <strong>Failed refills:</strong> $error <br> <strong>Successful refills:</strong> $success";    
    }else{
        $result_text = "<strong>Command executed successfully</strong> <br> <strong>No refillable orders found</strong>";
    }
    // 标记任务完成,写入结果
    $stmt = $conn->prepare("UPDATE background_tasks SET status = 'done', result = :res, finish_time = NOW() WHERE task_id = :tid");
    $stmt->execute(['tid' => $task_id, 'res' => $result_text]);
} catch (Exception $e) {
    // 标记任务失败
    $stmt = $conn->prepare("UPDATE background_tasks SET status = 'failed', result = :res, finish_time = NOW() WHERE task_id = :tid");
    $stmt->execute(['tid' => $task_id, 'res' => 'Task error: ' . $e->getMessage()]);
}

修改原Web接口

把AJAX请求的补单接口里的长耗时逻辑全部删掉,改成触发后台任务后立刻返回:

// 权限校验等需要Session的逻辑全部放在最前
session_start();
// 这里放你原有的管理员权限判断逻辑
if (!is_admin()) {
    exit(json_encode(['code' => 403, 'msg' => 'No permission']));
}
session_write_close(); // 提前释放Session锁,不阻塞其他请求

// 生成唯一任务ID
$task_id = uniqid('refill_', true);
// 写入待执行任务记录
$stmt = $conn->prepare("INSERT INTO background_tasks (task_id, task_type, status, create_time) VALUES (:tid, 'refill', 'pending', NOW())");
$stmt->execute(['tid' => $task_id]);

// 拼接后台执行命令,按服务器实际情况修改php路径、脚本路径
// Linux环境PHP路径一般为/usr/bin/php,Windows环境替换为实际php.exe路径
$php_path = '/usr/bin/php';
$script_path = __DIR__ . '/run_refill_task.php';
$cmd = sprintf("%s %s %s %s > /dev/null 2>&1 &", $php_path, $script_path, $task_id, WEBSITE_ID);
// 后台启动CLI脚本,不等待脚本执行完成
exec($cmd);

// 立刻返回任务ID给前端,接口响应时间仅几毫秒
echo json_encode([
    'code' => 0,
    'msg' => 'Task started',
    'task_id' => $task_id
]);
exit;

新增任务状态查询接口

新建一个轻量接口供前端查询任务执行状态,比如路径为admin/orders/get-task-status:

session_start();
if (!is_admin()) {
    exit(json_encode(['code' => 403]));
}
session_write_close();
$task_id = $_GET['task_id'];
$stmt = $conn->prepare("SELECT status, result FROM background_tasks WHERE task_id = :tid");
$stmt->execute(['tid' => $task_id]);
$task = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
    'code' => 0,
    'data' => $task
]);
exit;

修改前端AJAX逻辑

把原来等待任务执行完成的逻辑改成触发任务后轮询状态:

$(".refill_all").click(function(){
    var site_url  = $('head base').attr('href');
    $.LoadingOverlay("show");
    $.ajax({
        url: site_url+"admin/orders/multi-action?bulkStatus=orders_refill",
        method: "GET",
        dataType: 'json',
        success: function(res){
            if(res.code != 0){
                alert(res.msg);
                $.LoadingOverlay("hide");
                return;
            }
            var task_id = res.task_id;
            // 每2秒轮询一次任务状态
            var pollTimer = setInterval(function(){
                $.get(site_url+"admin/orders/get-task-status", {task_id: task_id}, function(res){
                    if(res.data.status == 'done' || res.data.status == 'failed'){
                        clearInterval(pollTimer);
                        $.LoadingOverlay("hide");
                        $("#bulk_refill_ajax").hide();
                        $("#bulk_refill_ajax_result").show();
                        $("#bulk_refill_ajax_result").html(res.data.result);
                    }
                }, 'json');
            }, 2000);
        }
    });
});

额外优化建议

  • 给cURL请求加上超时限制,避免第三方接口无响应拖慢整个任务,在SMMApi类的curl配置中新增两行:
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 连接超时10秒
    curl_setopt($ch, CURLOPT_TIMEOUT, 30); // 整个请求最长等待30秒
    
  • 如果单次补单的订单量超过1000条,可以在循环中每处理20个订单加100毫秒的休眠,避免瞬间打挂第三方接口,也降低数据库压力。
  • 如果服务器禁用了exec函数无法启动CLI进程,可以退而求其次使用fastcgi_finish_request()方案:在接口中把响应返回给前端后,再继续执行补单逻辑,注意必须提前调用session_write_close()释放Session锁,这个方案的缺点是长任务依然会占用PHP-FPM进程,仅适合订单量不大的场景。

内容的提问来源于stack exchange,提问作者Ashvin Bhagat

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 23:12:22