PHP 無原生線程,多進程併發寫入同一文件時,會發生數據覆蓋、截斷、甚至文件損壞。核心難點:**原子性**與**可見性**保證。
原生 flock() 在高併發下易死鎖,需加入**超時機制**與**隨機退避**:
<?php
function safeWriteWithLock(string $file, string $data, int $timeoutMs = 1000): bool {
$fp = fopen($file, 'c+'); // 'c+' 不截斷,指針在開頭
if (!$fp) return false;
$start = microtime(true);
$locked = false;
do {
$locked = flock($fp, LOCK_EX | LOCK_NB); // 非阻塞嘗試
if (!$locked) {
usleep(random_int(100, 500)); // 隨機退避 0.1-0.5ms
}
} while (!$locked && (microtime(true) - $start) * 1000 < $timeoutMs);
if (!$locked) {
fclose($fp);
return false; // 超時放棄
}
// 寫入數據
ftruncate($fp, 0); // 清空文件
fwrite($fp, $data);
fflush($fp); // 強制刷新到磁盤
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
?>
優點:標準、跨平台、語義清晰。
缺點:NFS/網絡文件系統不可靠;高併發下等待時間不可控。
利用 rename() 在同一文件系統上**原子性**替換文件(POSIX 標準保證):
<?php
class AtomicFileWriter {
private string $tempDir;
public function __construct(string $tempDir = null) {
$this->tempDir = $tempDir ?? sys_get_temp_dir() . '/atomic_write_';
@mkdir($this->tempDir, 0755, true);
}
public function write(string $targetFile, string $content): bool {
$tempFile = $this->tempDir . bin2hex(random_bytes(8)) . '.tmp';
// 1. 寫入臨時文件
if (file_put_contents($tempFile, $content) === false) {
@unlink($tempFile);
return false;
}
// 2. 同步刷新(關鍵!確保數據落盤)
$fp = fopen($tempFile, 'r');
flock($fp, LOCK_SH);
fclose($fp);
// 3. 原子重命名(同分區下瞬間完成)
clearstatcache(true, $targetFile);
return @rename($tempFile, $targetFile);
}
}
// 使用
$writer = new AtomicFileWriter();
$writer->write('/var/data/config.json', json_encode(['version' => 2]));
?>
優點:無死鎖、讀寫不阻塞、天然支持「寫入失敗不影響原文件」。
缺點:跨文件系統失效(需同分區);Windows 下目標存在會報錯(需先 unlink)。
將寫入分散到 N 個文件,併發衝突概率降為 1/N:
<?php
class ShardedLogWriter {
private string $baseDir;
private int $shards;
public function __construct(string $baseDir, int $shards = 256) {
$this->baseDir = rtrim($baseDir, '/');
$this->shards = $shards;
@mkdir($this->baseDir, 0755, true);
}
public function write(string $message): void {
$shard = random_int(0, $this->shards - 1);
$file = "{$this->baseDir}/log_{$shard}.txt";
// 追加寫入(FILE_APPEND + LOCK_EX 保證單文件原子性)
file_put_contents($file, $message . PHP_EOL, FILE_APPEND | LOCK_EX);
}
// 合併分片(異步定時任務執行)
public function merge(string $outputFile): int {
$total = 0;
$outFp = fopen($outputFile, 'w');
for ($i = 0; $i < $this->shards; $i++) {
$file = "{$this->baseDir}/log_{$i}.txt";
if (file_exists($file)) {
$content = file_get_contents($file);
fwrite($outFp, $content);
$total += substr_count($content, PHP_EOL);
unlink($file); // 清理
}
}
fclose($outFp);
return $total;
}
}
// 極高併發訪問日誌記錄
$logger = new ShardedLogWriter('/var/log/myapp', 512);
$logger->write(json_encode(['uid'=>123, 'action'=>'click', 'ts'=>time()]));
?>
適用:訪問日誌、埋點數據、審計日誌 — **寫多讀少、允許延遲合併**。
不適用:配置文件、用戶資料 — **需強一致性、即時可讀**。
將寫入請求推入隊列,由單一消費者串行處理:
<?php
// 生產端(Web 進程)
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->lPush('file_write_queue', json_encode([
'file' => '/data/config.json',
'data' => ['version' => 2, 'time' => time()],
'callback' => 'write_config' // 消費端路由標識
]));
// 消費端(常駐進程 / Supervisor 管理)
while (true) {
$job = $redis->brPop('file_write_queue', 5);
if (!$job) continue;
$task = json_decode($job[1], true);
$fp = fopen($task['file'], 'c');
flock($fp, LOCK_EX);
ftruncate($fp, 0);
fwrite($fp, json_encode($task['data']));
flock($fp, LOCK_UN);
fclose($fp);
}
?>
優點:完全消除併發衝突、寫入邏輯集中維護、支持重試/熔斷/監控。
缺點:引入外部依賴(Redis/RabbitMQ)、增加架構複雜度、有毫秒級延遲。
| 維度 | flock+超時 | 臨時文件+rename | 隨機分片 | 隊列單寫 |
|---|---|---|---|---|
| 一致性 | 強 | 強 | 最終一致 | 強 |
| 併發性能 | 低(串行等待) | 中(磁盤IO) | 極高(無鎖) | 高(異步) |
| 實現複雜度 | 低 | 中 | 低 | 高 |
| 跨機器支持 | 差(需分布式鎖) | 差 | 好(共享存儲) | 優(天然分布式) |
| 適用場景 | 配置、計數器 | 關鍵文件更新 | 高併發日誌 | 核心業務數據 |
| 推薦指數 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐(特定場景) | ⭐⭐⭐⭐⭐(架構級) |
AtomicFileWriter(方案二),簡單可靠flock + timeout(方案一),語義匹配ShardedLogWriter(方案三),性能最優file_put_contents($file, $data, LOCK_EX) — **無超時,極易死鎖**clearstatcache(true, $file) — 避免緩存導致判斷錯誤fsync()(PHP 8.1+)或 fflush() + flock($fp, LOCK_UN) — 確保持久化0644,目錄 0755,避免權限導致寫入失敗選對策略,文件併發不再是噩夢。核心原則:**場景決定方案,簡單優於複雜,數據庫優於文件**。