图片刷新卡死本质是并发写冲突:高并发下多个请求争抢同一文件句柄或磁盘锁,导致linux ext4小文件覆盖时rename()/unlink()+file_put_contents()触发内核级写阻塞,php-fpm进程卡在w状态。

图片刷新卡死本质是并发写冲突
PHP 高并发下图片刷新(比如用户上传头像、商品图覆盖同名文件)卡死,通常不是 PHP 执行慢,而是多个请求同时 fopen('avatar.jpg', 'w') 或 file_put_contents('thumb.png', $data, LOCK_EX) 争抢同一个文件句柄或磁盘锁。Linux 的 ext4 文件系统在小文件高频覆盖时,rename() 或 unlink() + file_put_contents() 组合极易触发内核级写阻塞,表现就是请求 hang 在 write() 系统调用上,Nginx 报 504 Gateway Timeout,而 PHP-FPM 进程状态卡在 W (writing)。
用 Redis 队列异步处理图片写入
把“生成/覆盖图片”从 Web 请求中剥离,只保留入队动作。关键点不是加不加锁,而是让写操作彻底串行化且不阻塞响应:
- 前端上传后,PHP 只做校验、存原始二进制到临时路径(如
/tmp/upload_abc123.bin),然后LPUSH img_queue json_encode(['type'=>'avatar','uid'=>123,'tmp_path'=>'/tmp/upload_abc123.bin']) - 单独起一个常驻进程(
php /path/to/worker.php),用BRPOP img_queue 0阻塞取任务,每次只处理一个任务 - worker 中执行真正的图片处理:GD/ImageMagick 缩放、加水印、
move_uploaded_file()覆盖目标路径 —— 此时没有并发,无需文件锁 - 处理完发消息通知(如 Redis Pub/Sub 或数据库更新状态),前端轮询或 WebSocket 获取结果
注意:BRPOP 的 0 表示无限等待,避免空轮询;worker 必须用 pcntl_fork() 或 supervisor 管理多进程,否则单 worker 成瓶颈。
必须关掉 PHP 的 session_write_close() 再操作文件
很多卡死其实和图片无关,是 PHP 默认 session 锁导致的 —— 同一用户的多个请求因共享 session 文件被串行阻塞。即使你没显式调用 session_start(),只要配置了 session.auto_start=1 或框架自动开启,就存在此问题:
立即学习“PHP免费学习笔记(深入)”;
- 在图片上传接口开头立即调用
session_write_close(),释放 session 文件锁 - 确认
session.save_handler不是files,改用redis或memcached(需扩展支持) - 检查是否误用
$_SESSION存大对象(如图片二进制),这会拖慢 session 写入本身
这个点最容易被忽略:你以为在优化图片逻辑,实际卡在 session 文件上。
文件覆盖用原子 rename(),别直接 write()
如果必须同步写(比如小项目无队列条件),file_put_contents($path, $data, LOCK_EX) 在高并发下仍可能卡住。更可靠的做法是:
- 生成唯一临时文件名:
$tmp = sys_get_temp_dir() . '/img_' . uniqid() . '.jpg'; -
file_put_contents($tmp, $data);(此时无锁,快速完成) -
rename($tmp, $path);(Linux 下rename()是原子操作,不会卡)
注意:rename() 跨文件系统会失败,确保 $tmp 和 $path 在同一磁盘分区;LOCK_EX 对 file_put_contents() 的临时写入无意义,真正要锁的是最终落盘动作,而 rename() 天然提供这个保证。
最麻烦的从来不是怎么写代码,而是搞清到底哪一层在卡 —— 是 PHP 的 session、文件系统锁、还是应用层逻辑锁。定位不到层,加再多队列和锁都是往错地方打补丁。











