如何优化PHP代码快速删除服务器未被引用的闲置图片
性能问题根因
你现有代码速度慢且存在严重逻辑bug,核心问题出在这几个地方:
- 最大性能瓶颈是
in_array查找为线性遍历,时间复杂度O(n)。你现在需要遍历15万个文件,每次查找都要扫完12万条数据库记录,总操作次数达180亿次,跑30分钟属于正常情况。 - 存在致命逻辑bug:
mysqli_fetch_all(MYSQLI_ASSOC)返回的是二维数组(每个元素是包含image、media两个键的数组),直接拿字符串做in_array的搜索值永远无法匹配成功,代码实际运行会删除目录下所有图片。 glob会一次性把15万个文件路径全加载到内存,内存占用高,大目录场景下容易触发PHP内存限制。- 用
str_replace截取文件名鲁棒性极差,只要路径中出现重复字符串就会出现替换错误。
优化方案
核心优化思路是把数据库内的在用文件名存为哈希表(PHP关联数组),将文件名查找的时间复杂度从O(n)降到O(1),整体耗时可以压到2秒以内。
优化点说明:
- 遍历数据库结果时,把
image和media两个字段的文件名都作为关联数组的键存储,查找时直接用isset()判断,性能是in_array的上千倍。 - 用
DirectoryIterator逐文件遍历目录,不需要一次性加载全量文件列表,内存占用稳定。 - 直接通过迭代器内置方法获取文件名和扩展名,避免字符串处理带来的错误。
- 增加后缀校验逻辑,避免误删非图片文件。
优化后可直接使用的代码:
<?php // 数据库查询逻辑保持原有逻辑,本身仅耗时1秒无优化必要 $sql = "SELECT image, media FROM products p, product_media pm WHERE p.id = pm.id"; $result = mysqli_query($con, $sql) or die(mysqli_error($con)); $rows = mysqli_fetch_all($result, MYSQLI_ASSOC); // 将所有在用文件名存入哈希表,键为文件名,值固定为true即可 $usedImages = []; foreach ($rows as $row) { if (!empty($row['image'])) { $usedImages[$row['image']] = true; } if (!empty($row['media'])) { $usedImages[$row['media']] = true; } } // 释放查询结果占用的冗余内存 mysqli_free_result($result); unset($rows); $directory = "images/products/full"; // 用目录迭代器逐文件读取,不一次性加载全量文件列表 $dirIterator = new DirectoryIterator($directory); $allowExt = ['jpg', 'jpeg', 'png', 'bmp']; foreach ($dirIterator as $fileInfo) { // 跳过系统点文件、子目录 if ($fileInfo->isDot() || $fileInfo->isDir()) { continue; } // 仅处理指定后缀的图片文件 $ext = strtolower($fileInfo->getExtension()); if (!in_array($ext, $allowExt)) { continue; } // 直接获取文件名,无需字符串替换处理 $filename = $fileInfo->getFilename(); // 哈希表查找,时间复杂度O(1) if (!isset($usedImages[$filename])) { // 首次运行请先注释unlink,替换为echo $filename . PHP_EOL; 打印待删除列表,核对无误后再执行删除,避免误删 unlink($fileInfo->getPathname()); } } ?>
注意事项
- 第一次运行务必先做删除校验,不要直接执行删除操作,确认待删除文件列表完全符合预期后再放开
unlink逻辑。 - 如果你的
media字段存储的是带路径的文件名,需要提前做路径裁剪,保证和目录内读取到的文件名格式完全一致,否则会出现匹配失败误删的问题。 - 如果目录下存在需要递归处理的子目录,可以把
DirectoryIterator换成RecursiveDirectoryIterator,核心匹配逻辑不需要改动。
内容的提问来源于stack exchange,提问作者bobi
相关产品推荐
相关产品推荐

