flock() 是唯一可靠、跨平台的文件锁定机制,因它通过内核级锁绑定文件句柄确保并发写入安全,而其他方法不跨进程或依赖外部服务;需在 fopen() 后立即加锁,锁随句柄关闭自动释放,仅适用于本地磁盘且须配合 fflush() 和正确文件模式。

flock() 是 PHP 中唯一可靠、跨平台的文件锁定机制,能防止并发写入导致的数据损坏。其他方法(如重命名、信号量)要么不跨进程,要么依赖外部服务,实际不可靠。
为什么 flock() 是唯一推荐方案
PHP 没有原子级文件写入,直接 fwrite() + fclose() 在多请求场景下极易丢数据。比如两个请求同时读取 JSON 文件、各自修改后写回,后写入者会覆盖前者的变更。flock() 通过操作系统内核级锁确保同一时间只有一个进程可操作该文件句柄。
- 必须在
fopen()之后、任何读写之前调用flock($fp, LOCK_EX) - 锁是与文件句柄绑定的,不是文件路径;关闭
$fp或脚本结束时自动释放 - 不支持 NFS 等网络文件系统(会静默失败),本地磁盘才可靠
- 阻塞式调用默认会挂起,可用
LOCK_EX | LOCK_NB改为非阻塞并手动处理失败
典型安全修改流程(含错误处理)
以下是最小可行的安全写入模式,覆盖打开、加锁、读取、修改、写入、刷新、解锁全流程:
$path = '/tmp/data.json';
$fp = fopen($path, 'c+'); // 'c+' 确保文件存在且可读写,不截断
if (!$fp) {
throw new RuntimeException("无法打开文件: $path");
}
if (!flock($fp, LOCK_EX)) {
fclose($fp);
throw new RuntimeException("无法获取排他锁: $path");
}
// 读取当前内容(注意:文件指针在开头)
rewind($fp);
$content = stream_get_contents($fp);
$data = json_decode($content, true) ?: [];
// 修改数据
$data['updated'] = time();
// 写入前清空并重置指针
fseek($fp, 0);
ftruncate($fp, 0);
fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE));
fflush($fp); // 强制刷到磁盘,避免缓冲区延迟
flock($fp, LOCK_UN);
fclose($fp);
常见失效场景和避坑点
很多“锁了但还是出错”的问题,根源不在 flock() 本身,而在使用方式:
立即学习“PHP免费学习笔记(深入)”;
- 在
fopen('w')模式下加锁无效:'w' 会立即清空文件,且某些系统下flock()对截断后的句柄行为未定义 - 多个脚本用不同路径打开同一文件(如软链接、相对路径),锁不生效——
flock()锁的是 inode,不是路径名 - 忘记
fflush()或fclose():数据可能滞留在 PHP 缓冲区或系统页缓存中,其他进程读到旧内容 - 在 CLI 和 Web SAPI 混用时,Web 服务器(如 Apache 的 mpm_event)可能复用进程,导致锁残留——务必显式
flock($fp, LOCK_UN)
什么情况下不该用 flock()
高并发写同一文件本身就是设计坏味道。如果单个文件每秒被修改数次以上,应考虑替代方案:
- 改用数据库(哪怕 SQLite)做事务性更新
- 用临时文件 +
rename()原子替换(仅限同一文件系统,且需注意rename()在 Linux 上是原子的,Windows 上不是) - 引入消息队列(如 Redis List +
BRPOP)串行化写操作
真正难的不是加锁,而是判断「这个文件是否真的需要被多个进程同时修改」——多数时候,问题出在架构,不在代码。











