| 常量 | 名稱 | 類型 | 適用場景 |
LOCK_SH | 共享鎖(讀鎖) | 非排他 | 多進程同時讀取,互不阻塞;寫入被阻塞 |
LOCK_EX | 獨占鎖(寫鎖) | 排他 | 寫入操作;阻塞其他讀/寫 |
LOCK_UN | 釋放鎖 | — | 手動釋放(腳本結束自動釋放) |
LOCK_NB | 非阻塞模式 | 修飾符 | 配合 LOCK_SH\|LOCK_EX 使用,無法獲取鎖立即返回 false |
二、核心特性與系統級保障
- 建議性鎖:依賴進程合作遵守,不強制阻止未調用
flock() 的進程讀寫。
- 進程級隔離:同一進程內多次
flock() 同一文件會覆蓋前一把鎖。
- 自動釋放:文件關閉(
fclose())或腳本結束時自動釋放所有鎖。
- 原子性追加:
fopen($file, 'a') 在類 Unix 系統上利用 O_APPEND 保證寫入位置原子性,配合 flock() 實現安全並發追加。
三、實戰模式對比:無鎖 vs 獨占鎖
❌ 無鎖並發寫入(數據交錯)
<?php
// process_a.php
$fp = fopen('data.txt', 'a');
for ($i = 0; $i < 5; $i++) {
fwrite($fp, "A{$i}\n");
usleep(100000); // 模擬處理延遲
}
fclose($fp);
// process_b.php 同時運行
$fp = fopen('data.txt', 'a');
for ($i = 0; $i < 5; $i++) {
fwrite($fp, "B{$i}\n");
}
fclose($fp);
// 結果:A0 B0 B1 B2 B3 B4 A1 A2 A3 A4 (數據交錯)
?>
✅ 獨占鎖串行寫入(數據完整)
<?php
// 兩個進程均使用此模式
$fp = fopen('data.txt', 'a');
if (flock($fp, LOCK_EX)) { // 獲取獨占鎖(阻塞等待)
for ($i = 0; $i < 5; $i++) {
fwrite($fp, "A{$i}\n");
usleep(100000);
}
flock($fp, LOCK_UN); // 顯式釋放(良好習慣)
}
fclose($fp);
// 結果:A0 A1 A2 A3 A4 B0 B1 B2 B3 B4 (完整串行)
?>
四、進階模式:非阻塞嘗試與超時控制
<?php
$fp = fopen('lock.txt', 'c+'); // 'c+' 打開讀寫,不截斷,不存在則創建
$maxWait = 5; // 最大等待 5 秒
$start = time();
while (true) {
// 非阻塞嘗試獲取獨占鎖
if (flock($fp, LOCK_EX | LOCK_NB)) {
echo "獲取鎖成功\n";
// 執行關鍵業務...
sleep(2); // 模擬業務耗時
flock($fp, LOCK_UN);
break;
}
// 未獲取到鎖
if (time() - $start >= $maxWait) {
echo "獲取鎖超時\n";
break;
}
usleep(100000); // 等待 100ms 重試
}
fclose($fp);
?>
五、經典應用場景
1. 計數器文件(高併發安全遞增)
<?php
function incrementCounter(string $file): int {
$fp = fopen($file, 'c+');
if (!$fp) throw new Exception("無法打開文件");
if (!flock($fp, LOCK_EX)) throw new Exception("獲取鎖失敗");
$count = (int)fread($fp, filesize($file) ?: 1);
$count++;
ftruncate($fp, 0); // 清空文件
rewind($fp);
fwrite($fp, (string)$count);
fflush($fp); // 強制刷新到磁盤
flock($fp, LOCK_UN);
fclose($fp);
return $count;
}
?>
2. 日誌文件並發寫入
<?php
function writeLog(string $logFile, string $message): void {
$fp = fopen($logFile, 'a');
if (flock($fp, LOCK_EX)) {
fwrite($fp, date('Y-m-d H:i:s') . " {$message}\n");
flock($fp, LOCK_UN);
}
fclose($fp);
}
?>
3. 單實例腳本執行(防重複運行)
<?php
$lockFile = '/tmp/my_script.lock';
$fp = fopen($lockFile, 'c');
if (!flock($fp, LOCK_EX | LOCK_NB)) {
die("腳本已在運行中\n");
}
// 註冊關閉函數釋放鎖
register_shutdown_function(function() use ($fp) {
flock($fp, LOCK_UN);
fclose($fp);
});
// 執行長時間任務...
sleep(60);
?>
六、Session 鎖機制揭秘
PHP 默認 Session 處理器(files)內部使用 flock():
session_start() 時以 LOCK_EX 鎖定 Session 文件。
- 同一用戶並發請求會串行等待,前一請求結束(或
session_write_close())才釋放鎖。
- 優化建議:只讀請求盡早調用
session_write_close() 釋放鎖,提升並發響應。
七、常見陷阱與避坑指南
| 陷阱 | 原因 | 解決方案 |
| NFS/網絡文件系統鎖失效 | 建議性鎖依賴本地文件系統鎖管理器 | 避免在 NFS 上使用 flock;改用 Redis/Memcached 分布式鎖 |
| Windows 下強制性鎖差異 | Windows 對 flock 實現為強制性鎖,行為略有不同 | 測試驗證跨平台行為;關鍵業務考慮跨平台鎖方案 |
| fopen 模式導致截斷 | 使用 'w' 模式會在獲取鎖前截斷文件 | 讀寫用 'c+',追加用 'a',先開文件再加鎖 |
| 死鎖 | 多資源加鎖順序不一致 | 統一全局加鎖順序;使用超時機制 |
| 鎖文件殘留 | 進程崩潰未釋放鎖 | 腳本結束自動釋放;或使用 register_shutdown_function |
八、現代替代方案:何時超越 flock()
- 分布式系統:Redis Redlock、etcd、ZooKeeper、Consul 分布式鎖
- 高頻計數:Redis INCR、MySQL 自增 ID、原子操作
- 複雜事務:資料庫行鎖(
SELECT ... FOR UPDATE)、樂觀鎖(版本號)
- 跨平台高可靠:
symfony/lock、laminas/laminas-lock 等成熟庫
flock() 是單機文件級並發控制的基石,理解其機制、模式與局限,能讓你在適合的場景下輕鬆解決數據競爭問題。但在分布式、高併發、跨平台的現代架構中,應果斷轉向專業的分布式協調組件。
- 上一篇文章: PHP 接收表單提交數據完整指南:$_GET、$_POST、$_REQUEST 詳解與安全實踐
- 下一篇文章: PHP 陣列與 JSON 互轉完全指南:json_encode/json_decode 深度解析與最佳實踐
Copyright ©
程式碼之家 All Rights Reserved