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

PHP长时进程执行时网站无法加载的原因及解决办法

问题分析与解决方案:长耗时PHP任务导致浏览器无法访问站点

用户问题

这段PHP代码能正常运行,但执行耗时极长,运行期间当前浏览器完全无法加载该网站,甚至新标签页打开也不行,必须更换浏览器才能访问。需要明确原因及解决办法。

代码片段

public function deep_cleaning_proccess()
{
    validate_ajax_session($this);
    ini_set('max_execution_time', 86400); // 24 horas en segundos
    ini_set('memory_limit', '2G'); // 2 GB de memoria

    ignore_user_abort(true);

    $this->db->query("DELETE FROM logs WHERE id <> 1 AND tipo like 'borrar%'");
    $obj = $this->input->get('objeto');
    switch ($obj) {
        case 'all':
            break;
        case 'obras':
            break;
        case 'publicaciones':
            $directorio = "/av_web_disk_1.3/srv/av/assets/uploads/publicaciones";
            $archivos = scandir($directorio);
            sort($archivos);
            $progreso = array(
                "estado" => 0,
                "total" => count($archivos),
                "terminado" => false
            );
            $this->db->query("INSERT INTO logs VALUES (1, 'borrar_publicacion', '" . json_encode($progreso) . "')
                            ON DUPLICATE KEY UPDATE tipo = 'borrar_publicacion', mensaje = '" . json_encode($progreso) . "'");
            for ($i = 0; $i < count($archivos); $i++) {
                if (is_dir($directorio . '/' . $archivos[$i])) {
                    $subcarpeta = $archivos[$i];
                    $result = $this->db->query("SELECT COALESCE(imagen, 'NullValue') AS imagen, COALESCE(url, 'NullValue') AS url, 
                                                COALESCE(pub_upload_folder, 'NullValue') AS pub_upload_folder, COALESCE(pub_upload_folder2, 'NullValue') AS pub_upload_folder2 
                                                FROM publicaciones p INNER JOIN imagenes i ON p.id = i.id_padre 
                                                WHERE p.id = '$subcarpeta;' AND i.estado <> 'papelera';")->result_array();
                    $lista_archivos = scandir($directorio . '/' . $subcarpeta);
                    if (count($result) > 0) {
                        $this->borrarImagenes($lista_archivos, $directorio . '/' . $subcarpeta, $result);
                        if (file_exists($directorio . '/' . $subcarpeta . '/pub_mid')) {
                            $lista_archivos = scandir($directorio . '/' . $subcarpeta . '/pub_mid');
                            $this->borrarImagenes($lista_archivos, $directorio . '/' . $subcarpeta . '/pub_mid', $result);
                        }
                        if (file_exists($directorio . '/' . $subcarpeta . '/pub_small')) {
                            $lista_archivos = scandir($directorio . '/' . $subcarpeta . '/pub_small');
                            $this->borrarImagenes($lista_archivos, $directorio . '/' . $subcarpeta . '/pub_small', $result);
                        }
                        if (file_exists($directorio . '/' . $subcarpeta . '/viewer')) {
                            $lista_archivos = scandir($directorio . '/' . $subcarpeta . '/viewer');
                            foreach ($lista_archivos as $arch_borrar) {
                                if ($arch_borrar != "." && $arch_borrar != ".." && is_dir($directorio . '/' . $subcarpeta . '/viewer/' . $arch_borrar)) {
                                    $borrar = true;
                                    if ($result[0]['pub_upload_folder'] == $arch_borrar || $result[0]['pub_upload_folder2'] == $arch_borrar) {
                                        $borrar = false;
                                    }
                                    if ($borrar) {
                                        if ($this->deleteDirectory($directorio . '/' . $subcarpeta . '/viewer/' . $arch_borrar)) {
                                            $this->db->query("INSERT INTO logs (tipo, mensaje) 
                                            VALUES ('borrar_publicacion', 'Borrado: $directorio/$subcarpeta/viewer/$arch_borrar')");
                                        } else {
                                            $this->db->query("INSERT INTO logs (tipo, mensaje) 
                                            VALUES ('borrar_publicacion', 'Error: $directorio/$subcarpeta/viewer/$arch_borrar')");
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                $progreso['estado'] = $i + 1;
                $this->db->query("UPDATE logs SET mensaje = '" . json_encode($progreso) . "'
                                    WHERE id = 1");
            }
            $progreso['terminado'] = true;
            $this->db->query("UPDATE logs SET mensaje = '" . json_encode($progreso) . "'
                                    WHERE id = 1");
            break;
        case 'articulos':
            break;
        case 'productos':
            $directorio = "/av_web_disk_1.3/srv/av/assets/uploads/productos";
            $archivos = scandir($directorio);
            sort($archivos);
            $progreso = array(
                "estado" => 0,
                "total" => count($archivos),
                "terminado" => false
            );
            $this->db->query("INSERT INTO logs VALUES (1, 'borrar_productos', '" . json_encode($progreso) . "')
                            ON DUPLICATE KEY UPDATE tipo = 'borrar_productos', mensaje = '" . json_encode($progreso) . "'");
            for ($i = 0; $i < count($archivos); $i++) {
                if (is_dir($directorio . '/' . $archivos[$i]) && is_numeric($archivos[$i])) {
                    $subcarpeta = $archivos[$i];
                    $result = $this->db->query("SELECT COALESCE(imagen, 'NullValue') AS imagen, COALESCE(imagen_vertical, 'NullValue') AS imagen_vertical, 
                                                COALESCE(imagen_listados, 'NullValue') AS imagen_listados, COALESCE(url, 'NullValue') AS url, 
                                                COALESCE(cuerpo, 'NullValue') AS cuerpo, COALESCE(cuerpo_frontend, 'NullValue') AS cuerpo_frontend 
                                                FROM productos p INNER JOIN imagenes i ON p.id = i.id_padre WHERE p.id = $subcarpeta and i.estado<>'papelera';")->result_array();
                    $lista_archivos = scandir($directorio . '/' . $subcarpeta);
                    if (count($result) > 0) {
                        foreach ($lista_archivos as $arch_borrar) {
                            if (!is_dir($directorio . '/' . $subcarpeta . '/' . $arch_borrar)) {
                                $ext = strtolower(pathinfo($arch_borrar, PATHINFO_EXTENSION));
                                if ($ext == 'jpg' || $ext == 'jpeg' || $ext == 'png' || $ext == 'webp') {
                                    $borrar = true;
                                    foreach ($result as $arch) {
                                        $nombre1 = strtolower(pathinfo($arch["imagen"], PATHINFO_FILENAME));
                                        $nombre2 = strtolower(pathinfo($arch["imagen_vertical"], PATHINFO_FILENAME));
                                        $nombre3 = strtolower(pathinfo($arch["imagen_listados"], PATHINFO_FILENAME));
                                        $nombre4 = strtolower(pathinfo($arch["url"], PATHINFO_FILENAME));
                                        $texto1 = strtolower($arch["cuerpo"]);
                                        $texto2 = strtolower($arch["cuerpo"]);
                                        if (
                                            strpos($arch_borrar, $nombre1 . '.') !== false
                                            || strpos($arch_borrar, $nombre2 . '.') !== false
                                            || strpos($arch_borrar, $nombre3 . '.') !== false
                                            || strpos($arch_borrar, $nombre4 . '.') !== false
                                            || strpos($texto1, $arch_borrar) !== false
                                            || strpos($texto2, $arch_borrar) !== false
                                        ) {
                                            $borrar = false;
                                            break;
                                        }
                                    }
                                    if ($borrar) {
                                        if (unlink($directorio . '/' . $subcarpeta . '/' . $arch_borrar)) {
                                            $this->db->query("INSERT INTO logs (tipo, mensaje) 
                                                VALUES ('borrar_productos', 'Borrado: $directorio/$subcarpeta/$arch_borrar')");
                                        } else {
                                            $error = error_get_last();
                                            $error = $error['message'];
                                            $this->db->query("INSERT INTO logs (tipo, mensaje) 
                                                VALUES ('borrar_productos', 'Error: $directorio/$subcarpeta/$arch_borrar - $error')");
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                $progreso['estado'] = $i + 1;
                $this->db->query("UPDATE logs SET mensaje = '" . json_encode($progreso) . "'
                                                    WHERE id = 1");
            }
            $progreso['terminado'] = true;
            $this->db->query("UPDATE logs SET mensaje = '" . json_encode($progreso) . "'
                                                    WHERE id = 1");
            break;
    }
}

核心原因

  1. 会话锁阻塞:PHP默认会话机制会在请求启动时锁定会话文件,直到请求结束才释放。你的代码通过validate_ajax_session($this)初始化了会话,长耗时任务会一直持有这个锁。同一浏览器的所有请求共享同一个会话ID,后续请求会因为等待锁释放被完全阻塞,导致网站无法加载;换浏览器会生成新会话,不受锁的影响。
  2. 同步长任务占用连接:Ajax请求同步执行整个清理流程,服务器连接被长时间占用,同时会话锁持续持有,进一步加剧阻塞。
  3. 频繁数据库IO:循环中每次迭代都执行UPDATE logs写入进度,大量重复的数据库操作拉长了任务执行时间,延长了锁的持有周期。

解决办法

1. 尽早释放会话锁

在完成会话验证后立即释放锁,让其他请求可以正常使用会话:

validate_ajax_session($this);
// 释放会话锁,后续无需会话操作的话无需重新启动
session_write_close();

2. 转为异步任务执行

不要通过Ajax同步执行长任务,改用后台进程或消息队列:

  • 后台脚本方式:启动独立PHP脚本执行清理任务,立即给前端返回响应:
$obj = $this->input->get('objeto');
// 启动后台进程,&符号让脚本在后台运行
exec("php " . FCPATH . "cleaning_worker.php --objeto=$obj > /dev/null 2>&1 &");
// 立即返回任务启动状态
echo json_encode(['status' => 'success', 'msg' => '清理任务已在后台启动']);
exit;

然后编写独立的cleaning_worker.php处理原有的清理逻辑。

3. 优化数据库操作

  • 减少进度更新频率:比如每处理10个目录再更新一次进度,降低IO次数。
  • 使用参数化查询:避免直接拼接SQL(存在注入风险且性能差),改用框架的参数绑定:
// CodeIgniter参数绑定示例
$result = $this->db->query(
    "SELECT ... WHERE p.id = ? AND i.estado <> 'papelera'",
    [$subcarpeta]
)->result_array();

4. 优化文件处理逻辑

  • 过滤无效目录:提前过滤scandir返回的.和..,减少循环次数:
$archivos = array_diff(scandir($directorio), ['.', '..']);
  • 预加载保留文件列表:一次性从数据库查询出所有需要保留的文件名,存入数组,避免循环中重复查询数据库。

内容的提问来源于stack exchange,提问作者Luis Miguel Molina Betancourt

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 08:43:09