
php 升级至 7.4.27 后,`file()` 函数并发读取被 `file_put_contents(…, lock_ex)` 写入的日志文件时偶现行内截断,根本原因在于 `lock_ex` 为**建议性锁**,`file()` 不感知也不等待该锁,导致读写竞争。本文提供两种生产就绪的修复方案:显式文件锁同步与原子化写入。
在 php 多进程环境中(尤其是 windows Server 下长期运行的后台控制台进程),日志写入与分析常面临典型的竞态条件(Race Condition)问题。虽然 file_put_contents($path, $data, FILE_appEND | LOCK_EX) 确实会对文件加独占锁,但该锁仅对主动调用 flock() 的进程有效——而 file() 函数底层直接调用系统 read(),完全忽略已有锁状态,因此读取进程可能在写入进程尚未完成一行(甚至未刷盘)时就切入读取,造成某行被截断(如 “Error: timeout occurredn” 只读到 “Error: timeout oc”)。
✅ 方案一:读写双方统一使用 flock() 显式同步(推荐用于高实时性场景)
修改读取逻辑,用 fopen() + flock() 替代 file(),确保读取前获得与写入端兼容的锁:
$fp = fopen($filePath, 'r'); if (!$fp) { throw new RuntimeException("Failed to open log file: $filePath"); } // 阻塞式获取共享锁(LOCK_SH),等待写入端释放 LOCK_EX if (!flock($fp, LOCK_SH)) { fclose($fp); throw new RuntimeException("Failed to acquire shared lock on $filePath"); } try { $logLines = []; while (($line = fgets($fp)) !== false) { $logLines[] = rtrim($line, "rn"); // 安全去除换行符 } $logLines = array_reverse($logLines); // 按原逻辑倒序 // ... 执行日志分析逻辑 } finally { flock($fp, LOCK_UN); // 必须释放锁 fclose($fp); }
⚠️ 注意事项: 写入端保持原样(file_put_contents(…, FILE_APPEND | LOCK_EX)),因其内部已使用 flock(); 读取端必须用 LOCK_SH(共享锁),它与写入端 LOCK_EX(排他锁)天然互斥; 务必使用 try/finally 或显式 flock(…, LOCK_UN),避免锁泄漏导致后续进程永久阻塞。
✅ 方案二:原子化写入(推荐用于高吞吐、低延迟敏感场景)
彻底规避读写冲突,让写入变为“全有或全无”的原子操作:
// 写入端:先写临时文件,再原子重命名 $tempFile = $logsFileNamePath . '.tmp'; file_put_contents($tempFile, $fileContents, FILE_APPEND | LOCK_EX); rename($tempFile, $logsFileNamePath); // windows 下 rename 是原子操作
// 读取端:容忍临时缺失,仅处理已完整写入的文件 if (!file_exists($filePath)) { // 文件可能正在重命名中,稍后重试或跳过本次分析 return; } $logContent = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $logContent = array_reverse($logContent); // ... 分析逻辑
✅ 优势:
立即学习“PHP免费学习笔记(深入)”;
- rename() 在 Windows 和 POSIX 系统上均为原子操作,无中间态;
- 读取端无需加锁,性能更高,且天然避免截断;
- 兼容所有 PHP 版本,不依赖锁机制行为一致性。
? 根本原因澄清
PHP 7.4.20 → 7.4.27 属于补丁版本升级,官方保证向后兼容。此次现象并非 PHP 引擎锁机制变更所致,而是旧有代码隐含的竞争条件在新版本中因底层 I/O 调度、缓冲策略或时序微调而更易暴露。LOCK_EX 始终是建议性锁(advisory lock),其有效性完全取决于所有参与进程是否主动协作检查——file() 从未设计为锁感知函数。
总结:不要依赖 file() 与 file_put_contents(…, LOCK_EX) 的“默契配合”。生产环境多进程日志访问,必须显式同步(方案一)或采用原子写入(方案二)。二者均可立即落地,零风险修复截断问题。