安全替换文件需三重防护:先用file_exists()和md5_file()(或hash_file/filesize+filemtime)校验内容是否真需更新;再通过临时文件+rename()实现原子替换;最后用flock()加锁防并发冲突,并记录各环节fallback日志。

直接覆盖文件前不加校验,是导致重复操作和数据错乱的最常见原因。PHP本身没有“原子替换”内置函数,必须靠组合判断+临时文件+重命名来实现安全替换。
用 file_exists() 和 md5_file() 双重校验是否真需要替换
仅靠文件存在与否判断不够——可能内容已更新但文件名没变。先检查目标文件是否存在,再比对源文件与目标文件的哈希值:
- 如果目标文件不存在,直接
copy() - 如果存在但
md5_file($source) !== md5_file($target),才执行替换 - 如果哈希一致,跳过操作,避免无谓 I/O 和时间戳变更
注意:md5_file() 对大文件有性能开销,若文件常超 10MB,可改用 hash_file('xxh128', $path)(需 PHP ≥ 7.4)或只比对 filesize() + filemtime() 作快速预筛。
用 rename() 替代 copy() + unlink() 实现原子替换
copy() 写入中途失败会导致目标文件残缺;unlink() 删除后再 copy() 则存在空窗期。正确做法是:
立即学习“PHP免费学习笔记(深入)”;
- 将新内容写入临时文件(路径用
sys_get_temp_dir()+ 唯一后缀,如$temp = tempnam(sys_get_temp_dir(), 'replace_') . '.tmp') - 写完后调用
rename($temp, $target)—— 在同一文件系统下,该操作是原子的 - 失败时只清理临时文件,原文件不受影响
关键点:rename() 跨文件系统会退化为复制+删除,务必确认 $temp 和 $target 在同一挂载点(可用 realpath() + dirname() 检查)。
替换前加锁防止并发写冲突
多个请求同时替换同一文件时,rename() 仍可能因竞态导致覆盖丢失。必须加锁:
- 用
fopen($lock_file, 'c')打开锁文件('c'确保不截断) - 立即
flock($fp, LOCK_EX)获取独占锁 - 锁内完成校验 → 写临时文件 →
rename() - 最后
flock($fp, LOCK_UN)+fclose($fp)
注意:锁文件路径需固定且可写,不能放在 /tmp 这类可能被定时清理的位置;也不建议用 mkdir() 模拟锁(NFS 下不可靠)。
真正麻烦的不是替换动作本身,而是校验逻辑是否覆盖了“内容未变但时间戳变了”“临时目录磁盘满”“NFS 锁失效”这些边缘情况。每层防护(哈希、原子重命名、flock)都得留好 fallback 日志,否则出问题时连哪步挂了都不知道。










