Laravel(PHP/MySQL)存储500MB大文件遇内存不足,求流/分块存储方案
解决大文件(500MB)存入MySQL的内存耗尽问题(Laravel/通用PHP场景)
一、PHP到MySQL的数据流存储方法
核心思路是不一次性把文件加载到内存,用PHP流资源配合MySQL参数绑定实现流式写入,避开file_get_contents的内存瓶颈。
Laravel实现
利用Laravel数据库连接获取底层PDO实例,绑定文件流执行插入:
$filePath = '/path/to/your/large.pdf'; $fileHandle = fopen($filePath, 'rb'); // 获取PDO连接 $pdo = DB::connection()->getPdo(); // 假设表名为`large_files`,`file_data`为LONGBLOB类型 $stmt = $pdo->prepare("INSERT INTO large_files (file_data) VALUES (:file)"); // 绑定流资源,指定LOB类型 $stmt->bindParam(':file', $fileHandle, PDO::PARAM_LOB); $stmt->execute(); fclose($fileHandle);
这种方式让PHP与MySQL以流形式传输数据,内存仅占用流缓冲区大小(默认几KB),不会触发内存耗尽。
通用PHP实现
不用Laravel时直接用PDO逻辑:
$dsn = 'mysql:host=localhost;dbname=your_db;charset=utf8'; $pdo = new PDO($dsn, 'user', 'pass'); $fileHandle = fopen('/path/to/your/large.pdf', 'rb'); $stmt = $pdo->prepare("INSERT INTO large_files (file_data) VALUES (:file)"); $stmt->bindParam(':file', $fileHandle, PDO::PARAM_LOB); $stmt->execute(); fclose($fileHandle);
二、分块存储方法
受max_allowed_packet(15MB)限制时,可将文件拆分为多个不超过14MB的块,分表存储:
1. 数据库表设计
-- 文件元数据表:存基本信息 CREATE TABLE file_metadata ( id INT AUTO_INCREMENT PRIMARY KEY, filename VARCHAR(255) NOT NULL, total_chunks INT NOT NULL, file_size BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 文件分块表:存拆分后的块数据 CREATE TABLE file_chunks ( id INT AUTO_INCREMENT PRIMARY KEY, file_id INT NOT NULL, chunk_index INT NOT NULL, chunk_data LONGBLOB NOT NULL, FOREIGN KEY (file_id) REFERENCES file_metadata(id) );
2. PHP分块写入实现
$filePath = '/path/to/your/large.pdf'; $chunkSize = 14 * 1024 * 1024; // 14MB $fileSize = filesize($filePath); $totalChunks = ceil($fileSize / $chunkSize); // 插入元数据并获取文件ID $fileId = DB::table('file_metadata')->insertGetId([ 'filename' => basename($filePath), 'total_chunks' => $totalChunks, 'file_size' => $fileSize ]); $fileHandle = fopen($filePath, 'rb'); $chunkIndex = 0; while (!feof($fileHandle)) { $chunkData = fread($fileHandle, $chunkSize); DB::table('file_chunks')->insert([ 'file_id' => $fileId, 'chunk_index' => $chunkIndex, 'chunk_data' => $chunkData ]); $chunkIndex++; } fclose($fileHandle);
三、从MySQL向用户流输出数据方案
通过分块读取+逐块输出实现流式下载,避免一次性加载大文件到内存。
1. 单字段LONGBLOB流式输出
利用SUBSTRING分块读取:
$fileId = 1; // 目标文件ID $chunkSize = 1 * 1024 * 1024; // 每次读1MB // 获取文件元信息 $fileInfo = DB::table('large_files')->where('id', $fileId)->first(); // 设置下载响应头 header('Content-Type: application/pdf'); header('Content-Disposition: attachment; filename="' . $fileInfo->filename . '"'); header('Content-Length: ' . $fileInfo->file_size); header('Cache-Control: no-cache'); $offset = 0; while ($offset < $fileInfo->file_size) { // MySQL的SUBSTRING从1开始计数 $chunk = DB::table('large_files') ->where('id', $fileId) ->value(DB::raw("SUBSTRING(file_data, {$offset}+1, {$chunkSize})")); echo $chunk; ob_flush(); flush(); $offset += $chunkSize; } exit;
2. 分块存储流式输出
按块顺序读取输出:
$fileId = 1; $metadata = DB::table('file_metadata')->where('id', $fileId)->first(); // 设置响应头 header('Content-Type: application/pdf'); header('Content-Disposition: attachment; filename="' . $metadata->filename . '"'); header('Content-Length: ' . $metadata->file_size); header('Cache-Control: no-cache'); // 按块顺序读取并输出 $chunks = DB::table('file_chunks') ->where('file_id', $fileId) ->orderBy('chunk_index') ->get(); foreach ($chunks as $chunk) { echo $chunk->chunk_data; ob_flush(); flush(); } exit;
注意事项
- 确保字段类型为
LONGBLOB(最大支持4GB),不要用BLOB/MEDIUMBLOB(容量不足)。 - 输出时主动调用
ob_flush()和flush(),避免缓冲区累积占用内存。 - 分块存储时,写入和读取必须保证块顺序正确,否则文件损坏。
内容的提问来源于stack exchange,提问作者mankowitz
相关产品推荐
相关产品推荐

