定時任務、日誌記錄、數據導出、報表生成等場景常需將結構化數據以 XML 格式追加寫入文件。核心挑戰在於:原子性、併發安全、內存效率、編碼正確性、大文件性能。
| 函數 | 模式 | 行為 | 適用場景 |
|---|---|---|---|
| fopen | a / a+ | 追寫,指針至文件末尾,文件不存在則創建 | 日誌、流式寫入 |
| fopen | w / w+ | 截斷/創建,指針至開頭 | 覆蓋寫入 |
| fwrite | - | 二進制安全寫入,返回寫入字節數 | 所有寫入 |
| fflush | - | 強制刷新用戶空間緩衝區到內核 | 關鍵數據落盤 |
| fclose | - | 關閉句柄,自動刷新 | 結束寫入 |
| file_put_contents | FILE_APPEND | 單次調用原子追加 (PHP 內部加鎖) | 簡單場景、小數據 |
| SplFileObject | - | 面向對象封裝,支持 CSV/迭代/鎖 | 結構化處理 |
');
fwrite($fp, '');
for ($i = 0; $i < $count; $i++) {
$id = $result[$i]['ID'];
$serviceid = $result[$i]['SERVICEID'];
$downtime = $result[$i]['DOWNTIME'];
$visittime = $result[$i]['VISITTIME'];
// 直接拼接,無 XML 轉義,極高風險!
fwrite($fp, "");
}
fwrite($fp, '');
fwrite($fp, '');
fclose($fp);
fopen('a') 寫入,XML 結構破壞(交織的標籤)。< > & " ' 時導致 XML 解析失敗或注入。fwrite 系統調用開銷大,大數據量應批次緩衝。openMemory();
$writer->setIndent(true);
$writer->setIndentString(' ');
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('xml');
$writer->startElement('data');
foreach ($records as $rec) {
$writer->startElement('ref');
$writer->writeAttribute('id', (string)$rec['ID']);
$writer->writeAttribute('serviceid', (string)$rec['SERVICEID']);
$writer->writeAttribute('downtime', (string)$rec['DOWNTIME']);
$writer->writeAttribute('visittime', (string)$rec['VISITTIME']);
$writer->endElement(); // ref
}
$writer->endElement(); // data
$writer->endElement(); // xml
$writer->endDocument();
$xml = $writer->outputMemory(true);
if ($xml === false) throw new RuntimeException('XML 生成失敗');
// 一次性寫入,減少系統調用
if (fwrite($fp, $xml) === false) throw new RuntimeException('寫入失敗');
if (fflush($fp) === false) throw new RuntimeException('刷新失敗');
// 釋放鎖前確保數據落盤 (可選 fsync)
// if (function_exists('fsync')) fsync($fp);
} catch (Throwable $e) {
flock($fp, LOCK_UN);
fclose($fp);
@unlink($tmpFile);
error_log("XML 寫入失敗: {$e->getMessage()}");
return false;
}
flock($fp, LOCK_UN);
fclose($fp);
// 原子重命名 (POSIX 保證同文件系統原子性)
if (!@rename($tmpFile, $filePath)) {
@unlink($tmpFile);
return false;
}
return true;
}
// 使用示例
$records = [
['ID' => 1001, 'SERVICEID' => 'SVC001', 'DOWNTIME' => '2024-01-01 10:00:00', 'VISITTIME' => '2024-01-01 10:05:00'],
['ID' => 1002, 'SERVICEID' => 'SVC002', 'DOWNTIME' => '2024-01-01 11:00:00', 'VISITTIME' => '2024-01-01 11:10:00'],
];
writeXmlAtomically('/var/data/export/20240101.xml', $records);
< > & ")、編碼聲明、縮進格式化,零手工拼接風險。rename() 在同一文件系統上原子性,讀者永遠看到完整文件或舊文件,無中間態。lockd 或改用數據庫。openMemory() 將完整 XML 構建在內存,一次 fwrite 寫入,極大減少系統調用。| 策略 | 適用閾值 | 實現要點 |
|---|---|---|
| 分批寫入 + 定期 fsync | 記錄數 > 10萬 | 每 1000 條 flush,定期 fsync 確保持久化 | 流式 XMLWriter (openUri) | 記錄數 > 100萬 / 內存受限 | 直接寫入文件流,無內存堆積,但失去原子性需配合臨時文件 |
| 異步寫入隊列 | 寫入延遲敏感業務 | 生產者推入 Redis/RabbitMQ,消費者單線程順序寫入 |
| 分片文件 + 合併讀取 | 單文件 > 100MB | 按日期/業務分片,下游合併解析 |
| 遷移至數據庫/對象存儲 | 結構化查詢/高併發讀寫 | MySQL JSON 欄位、PostgreSQL XMLTYPE、S3/MinIO + 元數據索引 |
open('/var/data/export/20240101.xml');
while ($reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT && $reader->localName === 'ref') {
$id = $reader->getAttribute('id');
$serviceid = $reader->getAttribute('serviceid');
// 處理單條記錄...
}
}
$reader->close();
文件追加寫入 XML 看似簡單,實則暗藏併發安全、注入防範、原子性、性能四大挑戰。採用 XMLWriter + 臨時文件原子重命名 + flock 獨佔鎖 組合拳,可構建生產級可靠的寫入管道。隨著數據規模增長,適時遷移至數據庫或對象存儲,才是長久之計——文件系統不是數據庫,不要試圖用它解決事務、索引、併發控制問題。