flock() 必须配合 fopen() 使用且仅进程内有效;file_put_contents(..., LOCK_EX) 仅瞬时加锁,无法保护“读-改-写”全流程;安全并发需手动 fopen/flock/fwrite/fflush/flock_unlock。

flock() 不是“加个锁就完事”,它必须配合 fopen() 打开的文件句柄使用,且锁只在进程内有效、不跨机器——用错地方等于没锁。
为什么直接 file_put_contents(..., LOCK_EX) 有时不管用
很多人以为加个 LOCK_EX 标志就能防并发,但实际中仍出现数据错乱。这是因为:file_put_contents() 的 LOCK_EX 只在写入那“一瞬间”加锁,而像 JSON 文件“读-改-写”这种三步操作,锁根本覆盖不到整个流程。
- 它只对单次写入原子生效,不保护前置的
file_get_contents() - 如果多个请求同时执行
file_get_contents()+file_put_contents(..., LOCK_EX),前一步已读到旧内容,后一步覆盖写入,照样丢数据 -
LOCK_EX在file_put_contents()内部调用flock(),但锁随函数返回立刻释放,无法延续到你自己的逻辑里
flock() 必须和 fopen/fwrite 配合手动控制生命周期
真正安全的文件并发控制,得自己打开文件、显式加锁、完成全部读/写/截断等操作、再显式解锁。关键点:
- 文件必须用
fopen()打开,模式推荐"c+"(可读写,不截断,不存在则创建)或"r+"(需确保文件存在) -
flock($fp, LOCK_EX)成功才继续,失败要处理(比如重试或拒绝请求) - 写入前建议用
ftruncate($fp, 0)清空(仅限覆盖场景),或用rewind($fp)定位,避免残留 - 写完必须调
fflush($fp)强制刷盘,再flock($fp, LOCK_UN),不能依赖fclose()自动释放——PHP 5.3.2+ 已不保证 fclose 一定触发解锁
示例片段:
立即学习“PHP免费学习笔记(深入)”;
$fp = fopen('/tmp/data.json', 'c+');
if (flock($fp, LOCK_EX)) {
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($newData));
fflush($fp); // 这行不能少
flock($fp, LOCK_UN);
}
fclose($fp);
非阻塞锁(LOCK_NB)怎么避免卡死
默认 flock() 会一直等锁,Web 请求可能超时。加 LOCK_NB 是正确做法,但它不是“自动重试”,而是立即返回 false —— 你需要自己决定策略:
- 简单拒绝:直接
exit或返回 503,适合后台任务不敏感场景 - 有限重试:循环
usleep(50000)+ 尝试,最多 3–5 次,避免雪崩 - 记录日志:把
$would_block参数设为引用变量,能知道是真冲突还是其他错误(如权限不足)
用法示例:
$would_block = 0;
if (!flock($fp, LOCK_EX | LOCK_NB, $would_block)) {
if ($would_block) {
error_log("Lock busy for /tmp/data.json");
return false;
}
throw new RuntimeException('flock failed: ' . implode(' ', error_get_last()));
}
共享锁(LOCK_SH)和独占锁(LOCK_EX)别混用场景
共享锁不是“读的时候随便用”,它和独占锁是互斥关系,但两个共享锁之间不互斥。常见误用:
- 用
LOCK_SH去“保护写操作”——无效,LOCK_EX才能阻止别人读或写 - 多个进程同时用
LOCK_SH读同一个配置文件没问题,但若其中某个进程顺手fwrite(),其他进程读到的就是脏数据 - 混合读写场景(如计数器),必须全程用
LOCK_EX,哪怕只是“读取当前值再+1”
真正需要 LOCK_SH 的典型场景极少,比如:一个长期运行的监控脚本只读日志,又不想阻塞其他写日志的进程。
最易被忽略的一点:flock 是咨询锁,不是强制锁。只要有一个进程不调用 flock(),它就能绕过所有锁定。所以它只适用于你完全掌控所有文件访问路径的场景——比如定时任务、单机 API 后端。别指望它能拦住 shell 脚本或另一个语言写的程序直接 echo >> file。



















