
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 occurred\n" 只读到 "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, "\r\n"); // 安全去除换行符
}
$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) 的“默契配合”。生产环境多进程日志访问,必须显式同步(方案一)或采用原子写入(方案二)。二者均可立即落地,零风险修复截断问题。











