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

