应优先使用 file_put_contents() 替代 fopen()+fwrite(),因其默认原子写入、内置缓冲优化、代码简洁且避免句柄泄漏;覆盖小文件时性能高10%–20%。

直接用 file_put_contents() 替代 fopen() + fwrite()
php 写文件时,file_put_contents() 默认就是原子写入(在大多数 linux 文件系统上),且底层做了缓冲优化。手动用 fopen() 打开再 fwrite() 写入,不仅代码多、易出错,还可能因忘记 fclose() 导致句柄泄漏或内容未刷盘。
- 如果只是覆盖整个文件内容,
file_put_contents($path, $content)比手写流操作快 10%–20%,尤其在小文件( - 加
FILE_APPEND或LOCK_EX标志时要小心:前者破坏“替换”语义,后者会阻塞其他进程,不是提速而是保序 - 避免在循环里反复调用
file_put_contents()写同一文件——它每次都会触发 open/write/close 系统调用,应先拼好完整内容再一次性写入
替换前先检查目标文件是否存在和可写,别等 file_put_contents() 报错才处理
PHP 的 file_put_contents() 在路径不存在或权限不足时,会返回 false 并触发 warning。但 warning 不等于异常,容易被静默吞掉,导致后续逻辑误判文件已更新。
- 用
is_writable($path)判断前,先确认dirname($path)存在且可写(否则创建文件会失败) - 对关键路径,建议提前用
touch($path)创建空文件并设好权限,避免运行时首次写入触发 umask 或 SELinux 等额外开销 - 不要依赖
@file_put_contents()抑制错误——它掩盖了真实问题,比如磁盘满、inode 耗尽,这些必须主动捕获并记录
大文件替换用 rename() 做原子切换,别直接覆盖
当新内容来自生成过程(如导出 CSV、渲染模板),直接 file_put_contents($path, $content) 会中途产生“半成品”状态。并发请求读取该文件,可能拿到截断或损坏数据。
- 正确做法:先写到临时路径(如
$tmp = $path . '.tmp'),再用rename($tmp, $path)切换——Linux 下rename()是原子操作,毫秒级完成,且无竞态 - 注意:
rename()跨文件系统会失败,确保临时文件和目标文件在同一挂载点(可用realpath()+stat()对比dev字段验证) - 临时文件写完后,务必用
clearstatcache(true, $tmp)清缓存,防止后续file_exists()误判
启用 OPcache 并禁用 opcache.file_update_protection(仅限开发环境)
如果被替换的文件是 PHP 脚本(如配置文件、路由映射),且被 OPcache 缓存,即使文件内容变了,PHP 进程仍可能继续执行旧字节码。
立即学习“PHP免费学习笔记(深入)”;
- 生产环境:靠
opcache.validate_timestamps = 1(默认)+ 合理的opcache.revalidate_freq(如 2)即可,无需关闭校验 - 开发环境频繁热更时,可临时设
opcache.file_update_protection = 0,避免 OPcache 因文件 mtime 变化过快而拒绝重载(但它不提升写入速度,只影响加载行为) - 切记:
opcache_reset()是全局操作,高并发下慎用;更安全的做法是重启 php-fpm 子进程,而非指望 OPcache 即时刷新
真正卡住性能的,往往不是 PHP 函数本身,而是磁盘 I/O 调度、NFS 延迟、或者没意识到 rename() 和 file_put_contents() 的语义差异。替换成原子切换后,再看是否真需要上 mmap 或异步写入——多数时候,不用。