flock()不是万能解药,高并发下直接锁图片文件易致PHP-FPM卡死在W状态;应改用原子rename()替代file_put_contents()直接写入,并关闭session写锁。

flock() 不是万能解药,高并发下直接用它锁图片文件反而容易把 PHP-FPM 进程卡死在 W 状态——这不是代码写错了,是锁没用对场景。
file_put_contents() + LOCK_EX 在图片覆盖时为何卡死
你以为加了 LOCK_EX 就安全了?错。Linux ext4 对小文件做 unlink() + file_put_contents() 时,内核会触发 write 阻塞,flock() 根本拦不住这个层级的冲突。表现就是 Nginx 返回 504,ps aux | grep php-fpm 看到一堆 W(writing)状态进程,CPU 很低但请求堆着不返回。
常见错误模式:
- 前端上传头像,后端直接
file_put_contents('avatar.jpg', $imgData, LOCK_EX) - 多个用户同时更新同一商品图,路径固定为
uploads/product_123.jpg - 没关 session 写锁,
session_start()先卡住 2 秒,再进文件操作
必须用原子 rename() 替代直接写入
真正安全的覆盖方式不是“写”,而是“换”。先写到临时路径,再用 rename() 原子替换——这个系统调用在同文件系统下是不可中断的,不会触发内核级阻塞。
正确流程:
立即学习“PHP免费学习笔记(深入)”;
- 生成唯一临时名:
$tmp = sys_get_temp_dir() . '/img_' . uniqid() . '.jpg'; - 写入临时文件:
file_put_contents($tmp, $imgData);(无需锁) - 原子覆盖目标:
rename($tmp, $targetPath);(失败则unlink($tmp)) - 注意:
$tmp和$targetPath必须在同一挂载点,否则rename()会退化为 copy+unlink,失去原子性
跨机器部署时,flock() 失效怎么办
NFS、Ceph、GlusterFS 等共享存储不保证 flock() 跨节点一致性,此时锁形同虚设。更危险的是——你本地测通了,上线多台机器就出竞态。
替代方案只有两个:
- 强制走单点写入:所有图片生成/覆盖请求打到同一台机器(加负载均衡权重或路由规则),再配合
rename()流程 - 切 Redis 分布式锁:
redlock-php获取lock:product_123,成功后再执行临时写 + rename,锁过期时间设为 3–5 秒(别太长,防误杀) - 绝对不要用
setnx + expire手写锁——没有原子性,Redis 主从切换时大概率丢锁
session_write_close() 是隐藏开关
90% 的“图片接口卡死”其实和图片无关。PHP 默认用文件存 session,session_start() 会锁住 sess_xxx 文件,同一用户的多个请求(比如头像上传 + 个人资料保存)被串行阻塞。
解决方法极其简单但常被跳过:
- 在上传接口开头立刻加:
if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } - 确认
session.save_handler不是files,改成redis或memcached - 检查有没有把大对象(如原始图片二进制)塞进
$_SESSION,这会让 session 写入本身变慢
rename() 能解决的,就别碰 flock();单机能扛住的,就别上 Redis 锁;session 没关,其他优化全白搭。



















