
本文详解php批量向数据库写入大量数据时在20–50条记录处中断的根本原因——并非数据库连接或sql语法问题,而是外部api(如ssb统计数据接口)触发了严格的请求频率限制(http 429),并提供延迟控制、批处理与错误恢复等专业级解决方案。
在使用PHP脚本从外部API批量获取数据并写入MySQL数据库时,开发者常遇到一种“看似随机却高度规律”的失败现象:当每轮仅插入1–2列字段时脚本全程稳定;但一旦扩展至10列以上(如本例中更新11个年份的人口字段),脚本总在第20–50次循环后静默终止或报错。问题表面指向数据库,实则根源在于外部API的服务端限流机制。
? 根本原因:API请求被限频(HTTP 429)
通过复现该脚本并注入调试逻辑(如 curl_error($curl)),可明确捕获关键错误响应:
{"error":"429 - Too many requests in too short timeframe. Please try again later."}这表明目标API(本例为挪威统计局 SSB 的 /api/v0/no/table/06913/ 接口)实施了严格的速率限制(Rate Limiting)。连续发起约60次cURL请求后即触发熔断,后续请求直接返回HTTP 429状态码,而原始脚本未检查 curl_exec() 返回值或HTTP状态码,导致 $resp 为空或非法JSON,进而引发 json_decode() 失败、数组访问越界(如 $test['value'][0] 报Notice),最终使 mysqli_query() 执行空SQL或含非法值的SQL,造成静默中断或数据库报错。
⚠️ 注意:mysqli_error($conn) 中的 $conn 变量名错误(应为 $tilkobling),且未检查 curl_exec() 是否成功,这是常见调试盲区。
✅ 解决方案一:添加请求节流(简单有效)
最直接的修复是在每次API调用后加入可控延迟,避免触达限流阈值:
立即学习“PHP免费学习笔记(深入)”;
// 在 curl_close($curl); 后添加 usleep(500000); // 暂停500毫秒(0.5秒) // 或更保守地:sleep(1); // 暂停1秒
✅ 优点:实现简单,立即生效。
⚠️ 注意:需根据实际限流策略调整间隔(如每分钟60次 → 平均间隔≥1秒);生产环境建议动态探测限流头(如 Retry-After)。
✅ 解决方案二:批量请求 + 批量更新(推荐)
更优解是减少HTTP请求数量。观察SSB API文档可知,其支持单次请求聚合多区域数据。例如,将 "values": ["K_0101"] 改为 "values": ["K_0101", "K_0102", "K_0103"],即可一次获取多个市镇数据。改造后:
// 构建批量区域列表(每批最多10个,避免单请求过大)
$batchSize = 10;
$kommuneBatch = array_slice($kommune_api_data['containeditems'], $kom, $batchSize);
$regionValues = [];
foreach ($kommuneBatch as $item) {
if ($item['codevalue'] !== '2211') {
$regionValues[] = 'K_' . $item['codevalue'];
}
}
// 在JSON payload中替换Region values
"values": $regionValues随后解析响应时,按顺序匹配各市镇数据,并使用 INSERT ... ON DUPLICATE KEY UPDATE 或 REPLACE INTO 批量写入:
// 构建批量UPDATE语句(安全防注入!)
$values = [];
$params = [];
foreach ($regionValues as $idx => $regionCode) {
$kommune = str_replace('K_', '', $regionCode);
// 提取对应年份数据(需适配SSB json-stat2结构)
$yearData = extractYearValues($test, $idx); // 自定义函数
$values[] = "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$params = array_merge($params, [$kommune, ...$yearData]);
}
$sql = "INSERT INTO kommuneinfo (kommunenummer, befolkning1970, ..., befolkning2020)
VALUES " . implode(', ', $values) . "
ON DUPLICATE KEY UPDATE
befolkning1970 = VALUES(befolkning1970),
befolkning1975 = VALUES(befolkning1975),
-- ... 其他字段
befolkning2020 = VALUES(befolkning2020)";
$stmt = $tilkobling->prepare($sql);
$stmt->bind_param(str_repeat('i', count($params)), ...$params);
$stmt->execute();✅ 优势:请求量降至1/10,执行速度提升数倍,显著降低超时与限流风险。
? 关键:必须使用预处理语句(prepare/bind_param)防止SQL注入,严禁字符串拼接。
✅ 解决方案三:健壮性增强(必备实践)
无论采用哪种方案,都需补充以下防护措施:
-
检查cURL执行结果:
$resp = curl_exec($curl); if ($resp === false) { error_log("cURL error: " . curl_error($curl) . " for kommune $kommune"); continue; // 跳过本次,避免阻塞 } $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); if ($httpCode !== 200) { error_log("API returned $httpCode for $kommune"); if ($httpCode === 429) usleep(2000000); // 遇429延长休眠 continue; } -
启用MySQL事务与错误回滚(对批量更新尤其重要):
$tilkobling->autocommit(FALSE); try { // 执行批量INSERT/UPDATE $tilkobling->commit(); } catch (Exception $e) { $tilkobling->rollback(); error_log("Transaction failed: " . $e->getMessage()); } -
设置脚本超时与内存限制(避免被服务器强制终止):
set_time_limit(0); // 取消执行时间限制 ini_set('memory_limit', '512M'); // 根据数据量调整
总结
PHP批量数据写入失败,90%以上源于对外部服务的过度调用而非数据库本身。诊断时务必:
- 开启完整错误报告(error_reporting(E_ALL); ini_set('display_errors', 1););
- 验证每一步返回值(cURL、JSON解码、数据库查询);
- 优先优化网络IO(合并请求、添加节流),再优化数据库操作;
- 始终使用预处理语句与事务保障数据一致性与安全性。
通过上述组合策略,即可稳定、高效地完成数千条记录的全量同步任务。











