多进程下ThinkPHP日志丢失或错乱是因文件写入未加锁导致的POSIX行为,需用flock()加锁或Windows下改用进程ID分片日志。

为什么多进程下 think\Log 会丢日志或内容错乱
ThinkPHP 默认日志驱动是文件写入,底层用 fopen + fwrite,没加锁。多进程同时写同一个日志文件时,fseek 位置不可控,会出现 A 进程刚写一半,B 进程覆盖写入,最终日志缺失、时间戳错位、JSON 格式损坏——这不是 ThinkPHP 的 bug,是 POSIX 文件 I/O 的天然行为。
常见错误现象:log/20240512.log 里突然出现两行日志挤在同一行、JSON 日志解析失败、某次请求完全没留下痕迹。
- 默认配置下,
'type' => 'File'不做任何并发保护 - 即使启用了
'apart_level' => ['sql', 'error'],同一类日志(如debug)仍写入同一个文件 -
file_put_contents($file, $content, FILE_APPEND)看似安全,但 PHP 在高并发下仍可能因系统缓存或 write() 原子性边界导致截断
用 flock() 实现文件独占写入的最小改动方案
不用重写整个日志驱动,只需在 think\log\driver\File::write() 方法末尾插入锁逻辑。核心是:打开文件后立即 flock($fp, LOCK_EX),写完再 flock($fp, LOCK_UN),并确保 fclose() 前释放。
实操建议:
- 不要用
FILE_APPEND模式配合flock——它和锁不兼容,必须用'a'模式打开,再手动fseek($fp, 0, SEEK_END) - 加锁前检查
$fp是否有效,避免flock(false, ...)报 Warning - 设置超时:用
flock($fp, LOCK_EX | LOCK_NB)避免死等,配合重试(最多 3 次,每次usleep(10000)) - 示例关键片段:
$fp = fopen($filename, 'a'); if ($fp && flock($fp, LOCK_EX | LOCK_NB)) { fseek($fp, 0, SEEK_END); fwrite($fp, $log . "\n"); flock($fp, LOCK_UN); } fclose($fp);
ThinkPHP 6.1+ 中替换原生 File 驱动的注意事项
TP6.1 开始支持日志驱动继承机制,但直接 extends think\log\driver\File 会因内部私有属性(如 $config)访问受限而报错。必须用组合而非继承,且需绕过 think\log\driver\File::getMasterFileName() 的硬编码路径拼接。
关键点:
- 新驱动类不能叫
File,否则容器自动绑定冲突;推荐命名如FlockFile - 重写
write()时,先调用父类format()获取格式化字符串,再自行控制文件打开与写入 - 别动
$this->config['time_format']等字段——它们在父类构造中已初始化,直接读即可 - 性能影响:单次写入增加约 0.3~0.8ms(实测 4 核 CPU),但比日志丢失或排查成本低得多
Windows 下 flock() 失效怎么办
Windows 对 flock() 支持不完整,尤其在 NTFS 上,LOCK_EX 可能不阻塞其他进程。此时不能依赖 flock(),得降级为「文件名分片 + 进程 ID 打标」。
可行做法:
- 把日志文件名从
debug.log改成debug_{getmypid()}.log,每个进程独占一个文件 - 配合定时合并脚本(如每天凌晨用
find log/ -name "debug_*.log" -exec cat {} \; >> debug_merged.log) - 注意清理:进程退出前删掉自己的日志临时文件,或加
register_shutdown_function()处理 - TP 配置中改
'file' => 'runtime/log/debug_' . getmypid() . '.log'即可,无需改驱动
真正麻烦的不是锁本身,而是你以为加了 flock() 就万事大吉——Windows 和某些 NFS 存储上它根本不起作用,得提前验证运行环境。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










