PHP 7.4.27 中 file() 读取日志文件时出现内容截断的解决方案

11次阅读

PHP 7.4.27 中 file() 读取日志文件时出现内容截断的解决方案

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) 的“默契配合”。生产环境多进程日志访问,必须显式同步(方案一)或采用原子写入(方案二)。二者均可立即落地,零风险修复截断问题。

text=ZqhQzanResources