將圖片二進位資料直接儲存於 MySQL BLOB 欄位,可實現多伺服器叢集環境下的檔案共享,免去分散式檔案系統或 NFS 掛載的複雜度。本文完整實現上傳、存儲、讀取、輸出流程,並分析優缺點與適用場景。
CREATE TABLE `images` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL COMMENT '原始檔名',
`mime_type` VARCHAR(100) NOT NULL COMMENT 'MIME 類型',
`size` INT UNSIGNED NOT NULL COMMENT '檔案大小(bytes)',
`data` MEDIUMBLOB NOT NULL COMMENT '二進位資料',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_mime` (`mime_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
| 類型 | 最大容量 | 適用場景 |
|---|---|---|
| TINYBLOB | 255 B | 極小圖示、圖標 |
| BLOB | 64 KB | 縮圖、小圖片 |
| MEDIUMBLOB | 16 MB | **通用網頁圖片(推薦)** |
| LONGBLOB | 4 GB | 高解析度原圖、文件 |
⚠️ max_allowed_packet (MySQL 設定) 必須 ≥ 最大檔案大小,預設 4MB/64MB 需調大。
<?php
// db.php
$dsn = 'mysql:host=127.0.0.1;dbname=demo;charset=utf8mb4';
$pdo = new PDO($dsn, 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 真實預處理
]);
return $pdo;
<?php
require 'db.php';
$pdo = $pdo ?? require 'db.php';
$action = $_GET['action'] ?? 'list';
if ($action === 'upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
// 驗證檔案
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
die(json_encode(['success' => false, 'msg' => '檔案上傳失敗']));
}
$file = $_FILES['image'];
// MIME 驗證 (防偽裝)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mime, $allowedMimes)) {
die(json_encode(['success' => false, 'msg' => '不支援的圖片格式']));
}
// 讀取二進位資料
$data = file_get_contents($file['tmp_name']);
$size = strlen($data);
// 寫入資料庫
$stmt = $pdo->prepare(
"INSERT INTO images (name, mime_type, size, data) VALUES (?, ?, ?, ?)"
);
$stmt->execute([
$file['name'],
$mime,
$size,
$data
]);
$id = $pdo->lastInsertId();
echo json_encode(['success' => true, 'id' => $id, 'msg' => '上傳成功']);
exit;
}
// 列表頁
$stmt = $pdo->query("SELECT id, name, mime_type, size, created_at FROM images ORDER BY id DESC");
$images = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>圖片上傳至資料庫</title>
<style>
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 12px; text-align: center; }
.card img { max-width: 100%; height: 150px; object-fit: contain; }
</style>
</head>
<body>
<h1>圖片上傳 (存入 MySQL BLOB)</h1>
<form method="POST" action="?action=upload" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*" required>
<button type="submit">上傳</button>
</form>
<div class="grid">
<?php foreach ($images as $img): ?>
<div class="card">
<img src="?action=view&id== $img['id'] ?>" alt="= htmlspecialchars($img['name']) ?>">
<p><strong>= htmlspecialchars($img['name']) ?></strong></p>
<p>= $img['mime_type'] ?> · = number_format($img['size'] / 1024, 1) ?> KB</p>
<p><small>= $img['created_at'] ?></small></p>
</div>
<?php endforeach; ?>
</div>
</body>
</html>
<?php
require 'db.php';
$pdo = $pdo ?? require 'db.php';
if (($action ?? '') === 'view') {
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) { http_response_code(400); exit; }
$stmt = $pdo->prepare("SELECT mime_type, data FROM images WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) { http_response_code(404); exit; }
// 輸出圖片
header('Content-Type: ' . $row['mime_type']);
header('Content-Length: ' . strlen($row['data']));
header('Cache-Control: public, max-age=31536000'); // 1 年快取
header('ETag: "img-' . $id . '"');
echo $row['data'];
exit;
}
PDO::PARAM_LOB 會自動處理,無需 mysql_escape_string (已棄用)。
// 分塊讀取寫入 (避免 file_get_contents 佔用大量記憶體)
$stmt = $pdo->prepare("INSERT INTO images (name, mime_type, size, data) VALUES (?, ?, ?, ?)");
$stmt->bindParam(1, $name);
$stmt->bindParam(2, $mime);
$stmt->bindParam(3, $size);
$stmt->bindParam(4, $data, PDO::PARAM_LOB); // 關鍵:LOB 模式
$fp = fopen($file['tmp_name'], 'rb');
$size = fstat($fp)['size'];
$data = $fp; // PDO 會從資源流讀取
$stmt->execute();
fclose($fp);
header('ETag: "img-' . $id . '-' . $row['updated_at'] . '"');
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
http_response_code(304);
exit;
}
| 優點 | 缺點 |
|---|---|
| 多伺服器自動共享,無需 NFS/分散式 FS | 資料庫體積快速膨脹,備份/還原耗時 |
| ACID 事務保證,圖片與業務資料原子性 | 查詢大 BLOB 影響緩衝池效能,建議分表 |
| 刪除/更新單一行即可,無孤兒檔案 | 無法直接利用 CDN/瀏覽器快取靜態檔案 URL |
| 權限控制集中在資料庫層 | 不適合大檔案 (>10MB) 或高頻讀寫 |
| 遷移簡單:單一 SQL dump | 複寫延遲大時從庫同步慢 |
images_meta (id, name, mime, size) + images_data (id, data),查詢列表不掃描 BLOBproxy_cache 或 Redis 快取熱門圖片,減少 DB 壓力Innodb_buffer_pool_reads、備份耗時將圖片存入 MySQL BLOB 是「以空間換簡單」的架構決策。在單體/中小型專案、多節點無共享存儲環境下,是快速實現檔案共享的務實選擇。隨著業務增長,建議平滑遷移至「資料庫存元資料 + 物件儲存存檔案」的混合架構。