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

PHP分支任务汇总脚本月中/月末触发500内部服务器错误排查

排查与解决PHP脚本数据量增大时的500错误问题

咱们先梳理下你的场景:月初数据少的时候脚本跑的好好的,到月中月末任务数据、分支数据堆起来就触发500内部错误,结合你给出的代码,我来拆解可能的原因和对应的解决办法:

一、核心原因分析

1. 数据库N+1查询灾难

看你的summariseEverything函数,里面嵌套了多层循环:分支 → 任务 → 日期,每循环一次就调用taskCompleted去查数据库。比如有10个分支、20个日常任务、30天,那光日常任务的查询就有10×20×30=6000次!数据量上来后,数据库连接、查询的开销会直接把脚本拖垮,要么超时要么数据库拒绝连接,最终触发500。

2. 内存占用飙升

多层循环里不断往$instance数组里塞各种统计数据,当月数据越多,数组体积越大,很容易超过PHP的内存限制,导致脚本直接崩溃。

3. 执行时间超限

虽然你设置了max_execution_time=3600,但多层嵌套循环的计算量是指数级增长的,月中月末的计算量可能远超月初,就算没到3600秒,服务器的Web服务(比如Apache/Nginx)本身也有超时限制,到点就会直接返回500。

4. 未初始化数组导致的隐性错误

比如代码里的$instance["tasks"]["daily"][$task["department"]]["total"],如果某个部门的数组没有提前初始化,PHP会抛出警告,要是你的服务器配置把警告当作错误处理(比如error_reporting=E_ALL且display_errors=Off),也会触发500。

二、针对性解决办法

1. 干掉N+1查询,改用批量查询

把所有需要判断的任务完成情况一次性从数据库拉取,用数组缓存起来,循环时直接查缓存就行,不用每次碰数据库。举个具体的优化示例:

function summariseEverything($month, $year) {
    global $conn;
    $now = new DateTime();
    $data = array();
    $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
    $start = sprintf("%s-%s-%s", $year, $month, "01");
    $end = sprintf("%s-%s-%s", $year, $month, $daysInMonth);

    // 获取所有分支、任务
    $branches = getBranches();
    $dailyTasks = getAllTasks(1, $start, $end);
    $weeklyTasks = getAllTasks(2, $start, $end);
    $monthlyTasks = getAllTasks(3, $start, $end);

    // 收集所有需要查询的ID
    $branchIds = array_column($branches, 'id');
    $allTaskIds = array_merge(
        array_column($dailyTasks, 'id'),
        array_column($weeklyTasks, 'id'),
        array_column($monthlyTasks, 'id')
    );

    // 批量查询所有已完成的任务记录,缓存到数组
    $completedCache = [];
    if (!empty($branchIds) && !empty($allTaskIds)) {
        $branchPlaceholders = implode(',', array_fill(0, count($branchIds), '?'));
        $taskPlaceholders = implode(',', array_fill(0, count($allTaskIds), '?'));
        $query = $conn->prepare("
            SELECT branch_id, task_id, 
                   DATE(start_time) AS task_date,
                   DATE_FORMAT(start_time, '%Y-%m-%d %H:%i:%s') AS full_start
            FROM task_completions
            WHERE branch_id IN ($branchPlaceholders)
              AND task_id IN ($taskPlaceholders)
              AND start_time BETWEEN ? AND ?
        ");
        // 绑定参数:先分支ID(int)、任务ID(int),再时间范围(string)
        $paramTypes = str_repeat('i', count($branchIds) + count($allTaskIds)) . 'ss';
        $params = array_merge($branchIds, $allTaskIds, ["{$start} 00:00:00", "{$end} 23:59:59"]);
        $query->bind_param($paramTypes, ...$params);
        $query->execute();
        $result = $query->get_result();
        while ($row = $result->fetch_assoc()) {
            // 按分支→任务→日期缓存
            $completedCache[$row['branch_id']][$row['task_id']][$row['task_date']] = true;
            // 周任务需要范围判断,额外缓存周起始日期
            $weekStart = date('Y-m-d', strtotime($row['task_date'] . ' Monday this week'));
            $completedCache[$row['branch_id']][$row['task_id']]['week_' . $weekStart] = true;
        }
    }

    // 提前计算当月所有日期的工作日/开放情况
    $dailyDateStatus = [];
    $dailyDates = Calendar::getDaysBetween($start, $end);
    foreach ($dailyDates as $date) {
        $dateObj = new DateTime($date);
        if ($dateObj > $now) {
            $dailyDateStatus[$date] = ['is_valid' => false];
            continue;
        }
        $isWorkingDay = true;
        Calendar::isBankHoliday($date, $Colour, $Msg, $isWorkingDay);
        Calendar::isWeekend($date, $Colour, $Msg, $isWorkingDay);
        $dailyDateStatus[$date] = [
            'is_valid' => $isWorkingDay,
            'day_name' => $dateObj->format("l")
        ];
    }

    // 处理每个分支
    foreach($branches as $branch) {
        // 初始化分支的统计数组,避免未定义键警告
        $instance = array(
            "id" => $branch["id"], 
            "name" => $branch["name"], 
            "area" => $branch["area"], 
            "finance_area" => $branch["finance_area"], 
            "company" => $branch["company"], 
            "icb" => $branch["icb"], 
            "tasks" => array(
                "completed" => 0,
                "total" => 0,
                "daily" => array(
                    "completed" => 0,
                    "total" => 0
                ),
                "weekly" => array(
                    "completed" => 0,
                    "total" => 0
                ),
                "monthly" => array(
                    "completed" => 0,
                    "total" => 0
                )
            )
        );
        // 提前初始化部门的统计结构
        $departments = array_unique(array_merge(
            array_column($dailyTasks, 'department'),
            array_column($weeklyTasks, 'department'),
            array_column($monthlyTasks, 'department')
        ));
        foreach ($departments as $dept) {
            $instance["tasks"]["daily"][$dept] = ["completed" => 0, "total" => 0];
            $instance["tasks"]["weekly"][$dept] = ["completed" => 0, "total" => 0];
            $instance["tasks"]["monthly"][$dept] = ["completed" => 0, "total" => 0];
            foreach ($dailyTasks as $task) {
                if ($task['department'] == $dept) {
                    $instance["tasks"]["daily"][$dept][$task['id']] = ["completed" => 0, "total" => 0];
                }
            }
            // 同理初始化周、月任务的部门内任务统计
        }

        // 处理日常任务
        foreach($dailyTasks as $task) {
            foreach($dailyDates as $date) {
                $status = $dailyDateStatus[$date];
                if (!$status['is_valid']) break;
                $isOpen = branchIsOpen($branch, $status['day_name']);
                if ($isOpen) {
                    // 累加总数
                    $instance["tasks"]["total"] += 1;
                    $instance["tasks"]["daily"]["total"] += 1;
                    $instance["tasks"]["daily"][$task["department"]]["total"] += 1;
                    $instance["tasks"]["daily"][$task["department"]][$task["id"]]["total"] += 1;
                    // 从缓存判断是否完成
                    $completed = isset($completedCache[$branch["id"]][$task["id"]][$date]);
                    if ($completed) {
                        $instance["tasks"]["completed"] += 1;
                        $instance["tasks"]["daily"]["completed"] += 1;
                        $instance["tasks"]["daily"][$task["department"]]["completed"] += 1;
                        $instance["tasks"]["daily"][$task["department"]][$task["id"]]["completed"] += 1;
                    }
                }
            }
        }

        // 周任务、月任务同理,用缓存判断完成情况,这里省略重复代码...

        $data[] = $instance;
    }

    // 最后把结果存入缓存表
    $jsonData = json_encode($data);
    $query = $conn->prepare("REPLACE INTO branchtasksummarydata (month, year, data_json, last_updated) VALUES (?, ?, ?, NOW())");
    $query->bind_param('iis', $month, $year, $jsonData);
    $query->execute();

    return ["data_json" => $jsonData, "last_updated" => date('d/m/Y H:i')];
}

2. 调整内存与执行时间配置

在脚本开头不仅设置执行时间,还要增加内存限制:

ini_set('max_execution_time', 3600);
ini_set('memory_limit', '512M'); // 根据实际情况调整,比如1G

同时检查服务器的Web服务配置:

  • Apache:修改httpd.conf里的Timeout为3600
  • Nginx:修改配置里的proxy_read_timeout 3600;

3. 开启错误日志定位具体问题

500错误只是表面现象,开启错误日志能让你看到真正的原因(比如内存耗尽、SQL语法错误):

error_reporting(E_ALL);
ini_set('display_errors', 0); // 线上环境不要显示错误
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php/branch_task_error.log'); // 改成你的日志路径

4. 优化缓存策略

你的代码已经有缓存逻辑,但可以再优化:

  • 不管update参数是否为1,先检查缓存是否存在且未过期(比如缓存超过1小时再更新),减少重复计算
  • 可以用Redis/Memcached替代数据库缓存,读取速度更快,适合高并发场景

三、额外建议

  • 尽量把统计逻辑放到数据库层面,用SQL的聚合函数(比如COUNT、SUM)直接计算每个分支的任务完成数,减少PHP的循环计算量
  • 对数据库的查询字段建立索引,比如task_completions表的branch_id、task_id、start_time字段,能大幅提升批量查询的速度

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 09:08:47