file_put_contents($filename, $data, FILE_APPEND | LOCK_EX) 是追加写入的最简原子方案,但仅适用于纯追加;读-改-写需 fopen()+flock() 手动控制,推荐 'c+' 模式并用非阻塞锁+有限重试,且必须显式解锁。

file_put_contents() 加 LOCK_EX 是最简方案,但只适用于追加写入
直接用 file_put_contents($filename, $data, FILE_APPEND | LOCK_EX) 就能原子化完成“打开→加锁→写入→关闭”全过程,不用手动管理句柄和锁。它底层调用了 flock(),且自动处理错误返回。
但注意:这个组合只保证追加行为的原子性,不能用于读-改-写场景(比如读取 JSON、修改字段、再写回)。一旦涉及先读再写,就必须自己用 fopen() + flock() 控制整个流程。
-
FILE_APPEND必须带上,否则LOCK_EX在'w'模式下会清空文件,锁没意义 - 不支持自定义超时或非阻塞;失败直接返回
false,没法重试或降级 - Windows 和 Linux 行为一致,但 NFS 或某些容器卷上可能失效
flock() 必须配合 fopen() 句柄,不能对路径直接调用
flock() 的第一个参数必须是 fopen() 返回的有效资源,传字符串路径会静默失败(返回 false),很多人卡在这一步还去查函数文档。
推荐打开模式:'c+'(不存在则创建、不截断、可读写),比 'a+' 更稳妥——后者在某些系统上对新文件的读位置不可靠。
立即学习“PHP免费学习笔记(深入)”;
- 别用
'r'模式加LOCK_EX,只读句柄无法获得独占锁 - 写日志这类纯追加场景,
'a'足够;但需读取内容再写入时,必须用'c+'或'c+' - Windows 下若文件被其他程序以不兼容方式打开(如记事本未关闭),
flock()也会失败
非阻塞锁 + 有限重试比死等更可靠
默认 flock($fp, LOCK_EX) 是阻塞的,一个慢请求可能让后续几十个请求排队卡住,尤其在 Web 环境下容易触发超时或连接池耗尽。
用 LOCK_EX | LOCK_NB 切换到非阻塞模式,失败立刻返回 false,由你决定下一步动作:
- 最多尝试 3 次,每次间隔用
usleep(50000)(50ms)起步,指数退避到 200ms - 超过重试次数后,可记录告警、写入临时队列,或抛出异常让上游重试
- 千万别写
while (!flock(...)) { usleep(1000); }这种无限循环,CPU 和响应时间都会崩
显式解锁比依赖 fclose() 或脚本结束更安全
虽然 PHP 在 fclose() 或进程退出时会自动释放锁,但以下情况会让锁“悬空”:
- 业务逻辑中调用
exit或die,fclose()没机会执行 - 未捕获的
Fatal error(如内存溢出、类未定义),finally块都不运行 - PHP-FPM 下启用
fastcgi_finish_request()后继续异步操作,句柄可能已被回收
正确做法是把加锁、写入、解锁全包进 try...finally:
$fp = fopen($file, 'c+');
if (!$fp) die('open failed');
try {
if (!flock($fp, LOCK_EX)) throw new Exception('lock failed');
fwrite($fp, $data);
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}
锁本身不复杂,难的是它藏在调用链深处——比如一个封装好的日志函数里加了锁,但上层用 pcntl_fork() 派生子进程,子进程会继承句柄和锁状态,此时锁就失效了。



















