在 Web 應用開發中,圖片等二進位大物件(BLOB)的存儲方式向來是架構設計的關鍵決策點。單純存資料庫易導致 I/O 瓶頸與備份膨脹;單純存檔案系統則面臨資料一致性、備份同步、權限管理等挑戰。本文提出「資料庫存元數據、檔案系統存實體」的混合架構,並在 ASP.NET + SQL Server 環境下給出完整落地方案。
| 維度 | 資料庫存儲 (varbinary/FILESTREAM) | 檔案系統存儲 + 資料庫存路徑 |
|---|---|---|
| 管理維護 | 備份/還原原子性強,ACID 保障完整 | 需額外同步機制,易產生孤兒檔案 |
| 讀取效能 | 大檔案佔用 Buffer Pool,擠佔熱資料記憶體 | 可直接由 IIS 靜態檔案處理,支援 CDN/範圍請求 |
| 修改編輯 | 需經應用層讀寫,不支援直接編輯 | 圖片處理工具可直接操作檔案 |
| 資料庫體積 | 急劇膨脹,影響備份窗口與還原 RTO | 資料庫維持輕量,僅存結構化元數據 |
| 事務一致性 | 天然支援分散式交易 | 需應用層補償或延遲刪除機制 |
CREATE TABLE dbo.MyImages (
ImageID INT IDENTITY(1,1) PRIMARY KEY,
RelativePath NVARCHAR(500) NOT NULL, -- 相對路徑:/uploads/2024/01/abc.jpg
FileName NVARCHAR(255) NOT NULL,
ContentType NVARCHAR(100) NOT NULL, -- image/jpeg, image/png
FileSize BIGINT NOT NULL,
Description NVARCHAR(MAX) NULL,
UploadUserID INT NOT NULL,
UploadTime DATETIME2(3) NOT NULL DEFAULT SYSUTCDATETIME(),
IsActive BIT NOT NULL DEFAULT 1,
CONSTRAINT UQ_Path UNIQUE (RelativePath)
);
CREATE INDEX IX_MyImages_UserTime ON dbo.MyImages (UploadUserID, UploadTime DESC);
/uploads/{yyyy}/{MM}/{dd}/{Guid}.{ext} 避免單目錄檔案過多[HttpPost]
public async Task<ActionResult> Upload(HttpPostedFileBase file, string description)
{
if (file == null || file.ContentLength == 0) return Json(new { success = false, msg = "空檔案" });
// 1. 驗證副檔名與 Content-Type
var allowed = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowed.Contains(ext)) return Json(new { success = false, msg = "不支援的格式" });
// 2. 產生儲存路徑
var dateDir = DateTime.UtcNow.ToString("yyyy/MM/dd");
var saveDir = Server.MapPath($"~/uploads/{dateDir}");
Directory.CreateDirectory(saveDir);
var newName = $"{Guid.NewGuid():N}{ext}";
var fullPath = Path.Combine(saveDir, newName);
var relPath = $"/uploads/{dateDir}/{newName}";
// 3. 串流存檔(避免大檔案佔用記憶體)
using (var fs = new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 81920, true))
{
await file.InputStream.CopyToAsync(fs);
}
// 4. 寫入資料庫(交易包裹)
using (var conn = new SqlConnection(connStr))
using (var cmd = new SqlCommand(@"
INSERT INTO dbo.MyImages (RelativePath, FileName, ContentType, FileSize, Description, UploadUserID)
VALUES (@path, @name, @type, @size, @desc, @uid);", conn))
{
cmd.Parameters.AddWithValue("@path", relPath);
cmd.Parameters.AddWithValue("@name", file.FileName);
cmd.Parameters.AddWithValue("@type", file.ContentType);
cmd.Parameters.AddWithValue("@size", file.ContentLength);
cmd.Parameters.AddWithValue("@desc", description ?? "");
cmd.Parameters.AddWithValue("@uid", CurrentUser.Id);
await conn.OpenAsync();
await cmd.ExecuteNonQueryAsync();
}
return Json(new { success = true, path = relPath });
}
隨著系統擴展,本地磁碟面臨單點、擴容、跨機房同步難題。建議引入介面抽象:
public interface IBlobStore {
Task<string> SaveAsync(Stream content, string contentType, CancellationToken ct);
Task DeleteAsync(string key, CancellationToken ct);
string GetPublicUrl(string key);
}
// 實作:LocalBlobStore / AzureBlobStore / S3BlobStore / MinIOBlobStore
資料表 `RelativePath` 改為 `BlobKey`,上層業務邏輯完全不變,無縫遷移至 Azure Blob Storage、AWS S3 或自建 MinIO。
| 坑點 | 後果 | 對策 |
|---|---|---|
| 直接用使用者原始檔名 | 路徑遍歷攻擊、特殊字元導致 404、重名覆蓋 | Guid 命名 + 白名單副檔名驗證 |
| 上傳目錄開放執行權限 | 上傳 webshell 導致伺服器淪陷 | IIS 設定「處理對應」移除 .aspx/.php 等腳本映射 |
| 大檔案同步讀寫阻塞執行緒 | 併發上傳導致 ThreadPool 耗盡 | 全链路 async/await + 緩衝區 80KB+ |
| 資料庫與檔案不同步 | 孤兒檔案佔用空間、資料庫指向不存在檔案 | 軟刪除 + 定期對帳作業 + 事件溯源審計日誌 |
「資料庫存元數據、檔案系統存實體」混合架構兼顧了效能、擴展性與維運便利性。關鍵在於:
採用此模式,可支撐從中小型專案到千萬級用戶的圖片業務平滑演進。