flock() 是 PHP 中唯一被广泛支持的跨进程文件锁方案,基于操作系统建议性锁,需配合 fopen() 使用并确保读-改-写原子性,但在 NFS 和部分容器环境中可能失效。

PHP 中 flock() 是唯一靠谱的文件锁方案
PHP 没有跨进程的全局锁机制,flock() 是唯一被广泛支持、无需额外扩展、且能真正起作用的文件锁定方式。它基于操作系统级的 advisory lock(建议性锁),依赖内核维护,不是 PHP 自己模拟的。这意味着:如果所有进程都主动调用 flock() 检查,锁就有效;但如果有程序直接读写文件而不调用它,锁就形同虚设。
常见错误是以为 flock() 能阻止其他语言或 shell 命令访问文件——它不能。它的作用只是协调 PHP 进程之间的读写顺序。
-
flock()必须在已打开的文件资源上使用,不能对路径字符串直接加锁 - 加锁前必须用
fopen()打开文件,且推荐使用'c'或'a+'模式(避免因文件不存在导致fopen()失败而跳过锁) - 锁是与文件描述符绑定的,
fclose()或脚本结束会自动释放;不要依赖LOCK_UN显式解锁(除非你提前需要) - 非阻塞模式(
LOCK_NB)必须手动检查返回值,否则可能误以为加锁成功
如何安全地写入并避免并发覆盖
典型场景是多个请求同时更新一个计数器文件或日志配置。关键不是“加锁”,而是“加锁 + 原子读-改-写”流程是否完整。只锁不重读,或读完就释放锁再写,都会导致竞态。
正确做法是:打开 → 加锁 → 读取当前内容 → 修改 → 写入(通常用 fseek(0) + ftruncate() + fwrite() 保证覆盖)→ 解锁(或靠 fclose)。
立即学习“PHP免费学习笔记(深入)”;
$fp = fopen('/tmp/counter.txt', 'c+');
if ($fp && flock($fp, LOCK_EX)) {
$count = (int)trim(fgets($fp));
fseek($fp, 0);
ftruncate($fp, 0);
fwrite($fp, ($count + 1) . "\n");
fflush($fp); // 确保写入磁盘
flock($fp, LOCK_UN);
}
fclose($fp);
- 用
'c+'模式确保文件存在且可读写,不会因文件不存在而失败 -
ftruncate($fp, 0)清空内容比直接fwrite()更安全,避免残留旧数据 -
fflush()强制刷新缓冲区,防止系统缓存导致写入延迟 - 务必检查
flock()返回值,失败时应退避或报错,不能继续执行
flock() 在 NFS 和容器环境中的失效风险
NFS(尤其是老版本)和某些容器存储驱动(如 overlay2 配合 rootless podman)不支持可靠的 flock()。此时调用始终返回 true,但实际无锁效果,极易引发数据错乱。
判断是否失效最简单的方法:启两个终端,分别运行带 flock($fp, LOCK_EX | LOCK_NB) 的脚本,如果都能立即成功返回,说明锁未生效。
- 生产环境部署前,必须在目标文件系统上实测
flock()的互斥行为 - 若确认不支持,需改用外部协调服务(如 Redis 的
SET key value NX EX 30)替代文件锁 - Web 服务器(如 PHP-FPM)多进程 + 共享存储时,NFS 锁失效是高频故障点,别心存侥幸
为什么不要用 file_put_contents(..., LOCK_EX)
这个参数看似方便,但它只对本次写入做独占锁,且锁的生命周期仅限于函数内部。它无法支撑「读-改-写」这类多步操作,也不支持共享锁(LOCK_SH),更不能用于长时持有(比如处理上传临时文件期间要一直锁着)。
它适合的场景只有一个:单次原子写入,且你完全不需要读取原内容。
- 它底层仍是调用
flock(),但封装后丢失了控制粒度 - 如果写入中途出错(如磁盘满),锁会释放,但你无法感知或回滚
- 无法与其他
flock()操作协同,比如一个进程用file_put_contents(),另一个用fopen()+flock(),它们之间可能不兼容 - 遇到大文件或慢存储时,长时间持有锁会导致其他请求阻塞,而你没法做超时控制



















