Laravel 多租戶系統設計:從資料庫隔離到 SaaS 航母升級
正在打造你的 SaaS 帝國嗎?工程師最怕的就是資料混雜和維護多個版本。資深工程師 Eric 帶你優雅告別傳統「義大利麵」實作,深度剖析多租戶(Multi-tenant)系統的核心:資源共享與資料隔離。我們建議選擇安全穩固的「多資料庫」模式,並透過 Laravel Middleware 實現動態連線切換,徹底杜絕資料外洩風險。這不僅是技術實現,更是保護商業信譽的關鍵戰略。別再讓架構卡關,立即聯繫我們,將你的 Laravel 專案升級為穩健、可擴展的 SaaS 航母!
拒絕義大利麵!Laravel 多租戶 (Multi-tenant) 系統設計實戰:從資料庫隔離到動態切換的 SaaS 煉金術
嗨,我是 Eric,浪花科技的資深工程師。如果你正在打造下一個 SaaS 帝國,或者老闆突然跟你說:「欸,我們這個系統能不能賣給別家公司用,但資料要分開?」,那你現在肯定頭很痛。這就是所謂的「多租戶系統 (Multi-tenant System)」。
說實話,我看過太多「災難級」的實作。有些人為了省事,直接把專案 Copy 一份給新客戶,最後維護 50 個版本的程式碼,搞到自己懷疑人生;有些人則是在每一行 SQL 查詢後面手動加上 where('tenant_id', $id),結果某天實習生忘記加,A 客戶看光了 B 客戶的報表,接著就是公關危機和老闆的怒吼。
今天這篇文章,我們要用工程師的思維,優雅地在 Laravel 中實現多租戶架構。我們不談空泛的理論,直接切入核心:資料庫策略、租戶識別以及中介層 (Middleware) 的動態切換。
為什麼你需要多租戶架構?
在進入程式碼之前,我們先釐清觀念。多租戶架構的核心在於「資源共享,資料隔離」。這就像是一棟公寓大樓(你的 Laravel 應用程式),裡面住著很多戶人家(租戶 Tenant)。大家共用電梯、水管(程式碼、伺服器資源),但你絕對不會希望鄰居拿著鑰匙開你家的門(資料外洩)。
實作良好的 Laravel 多租戶系統能解決以下痛點:
- 維護成本降低: 更新一次程式碼,所有客戶同步升級。
- 硬體資源最大化: 不需要為每個客戶開一台 VPS,除非他是付了幾百萬的 VIP。
- 資料安全性: 透過架構層級來保證資料隔離,而不是靠開發者的「記憶力」。
核心戰略:資料庫隔離策略的選擇
這是最關鍵的一步。選錯了,後面重構會讓你哭出來。Laravel 多租戶設計通常有兩種主流流派:
1. 單一資料庫 (Single Database) – 共享公寓
所有租戶的資料都在同一個 Database 裡,透過一個 tenant_id 欄位來區分。例如 users 表裡面會有 id, name, email, tenant_id。
- 優點: 成本最低,遷移 (Migration) 跑一次就好,報表分析跨租戶數據很方便。
- 缺點: 開發時容易因為漏寫
where條件導致資料外洩(雖然可以用 Laravel 的 Global Scope 解決),資料量大時單表效能會變慢,備份單一客戶資料很麻煩。
2. 多資料庫 (Multi Database) – 獨棟別墅
每個租戶擁有自己獨立的 Database。有一個主資料庫 (Landlord DB) 存租戶列表,其他的 Application Data 則分散在 Tenant A DB, Tenant B DB…
- 優點: 資料絕對隔離,安全性最高。單一客戶資料備份、還原、搬移都非常容易(VIP 客戶最愛)。
- 缺點: 部署和維護較複雜。試想一下,當你要跑
php artisan migrate時,你需要對 100 個資料庫執行迴圈。
Eric 的建議: 如果你是做 B2B 且客戶付費意願高,或者對資安要求極高(如醫療、金融),請務必選擇多資料庫 (Multi Database) 模式。這是最穩健的長遠之計。
實戰:Laravel 動態資料庫切換
接下來我們以「多資料庫模式」為例。核心邏輯是:請求進來 -> 識別是哪個租戶 -> 切換 Laravel 的 DB Connection -> 處理請求。
步驟一:定義租戶識別 (Tenant Identification)
通常我們透過 Domain 或 Subdomain 來識別。例如 company-a.saas.com 代表租戶 A。我們需要一個 Middleware 來攔截請求。
步驟二:建立 Middleware
這個 Middleware 負責「換軌」。我們假設你有一個 Tenant Model 儲存了該租戶的資料庫名稱。
namespace App\Http\Middleware;
use Closure;
use App\Models\Tenant;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
class IdentifyTenant
{
public function handle($request, Closure $next)
{
// 1. 從 Host 抓取 Subdomain
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
// 2. 查找對應的租戶
$tenant = Tenant::where('subdomain', $subdomain)->first();
if (!$tenant) {
abort(404, 'Tenant not found');
}
// 3. 神奇時刻:動態設定資料庫連線
// 這裡我們不去改 .env,而是動態修改 Config
Config::set('database.connections.tenant', [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'database' => $tenant->database_name, // 關鍵:換成租戶的 DB
'username' => $tenant->database_user,
'password' => $tenant->database_password,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
]);
// 4. 清除現有的連線並重新連接
DB::purge('tenant');
DB::reconnect('tenant');
// 5. 將預設連線設為 tenant
DB::setDefaultConnection('tenant');
// 6. 將租戶實例綁定到 Service Container,方便後續取用
app()->instance('current.tenant', $tenant);
return $next($request);
}
}
這段程式碼看似簡單,卻是多租戶系統的心臟。它確保了在這個 Request 的生命週期內,所有的 Model 查詢(如 User::all())都會自動指向該租戶的資料庫,完全不需要在 Controller 裡寫噁心的切換邏輯。
步驟三:處理 Migrations
這是一個大坑。你的 migrations 資料夾需要分家。通常我們會建立 database/migrations/landlord (存租戶列表、訂閱資訊) 和 database/migrations/tenant (存實際應用資料)。
在部署時,我們需要一個自訂的 Artisan Command 來遍歷所有租戶並執行遷移:
// app/Console/Commands/MigrateTenants.php
public function handle()
{
$tenants = Tenant::all();
foreach ($tenants as $tenant) {
$this->info("Migrating tenant: {$tenant->name}");
// 動態設定 Config (邏輯同 Middleware)
$this->configureTenantConnection($tenant);
// 執行遷移,指定 path 為 tenant migrations
$this->call('migrate', [
'--database' => 'tenant',
'--path' => 'database/migrations/tenant',
'--force' => true,
]);
}
}
除了資料庫,還有什麼要隔離?
工程師容易只看著資料庫,卻忘了還有其他資源需要隔離:
1. Cache 與 Redis
如果你用 Redis 做 Cache,記得要隔離 Key。最簡單的方法是在 Middleware 裡動態修改 Cache Prefix:
Config::set('cache.prefix', 'tenant_' . $tenant->id);
2. 檔案儲存 (Storage)
如果租戶上傳圖片,絕對不能混在一起。如果你用 S3,可以在路徑上做區隔,例如 bucket-name/tenant-1/avatars/user.jpg。你可以在 Middleware 裡設定 Filesystem 的 root 路徑。
3. Queue (佇列)
這是進階題。當 Job 被推送到 Queue 時,Worker 怎麼知道這個 Job 屬於哪個租戶?你需要確保 Job 在 payload 裡攜帶 tenant_id,並且在 Job 被執行 (unserialize) 時,重新觸發上述的「資料庫切換」邏輯。Laravel 的 JobMiddleware 是一個處理這件事的好地方。
Eric 的良心建議:不要造輪子
雖然了解原理很重要(像我們上面寫的那樣),但在商業專案中,我強烈建議使用成熟的套件。你自己寫的 Middleware 可能會漏掉某些 Edge Case,例如 Console Command 的環境模擬、或是測試環境的隔離。
目前 Laravel 生態系最推薦的兩個方案:
- spatie/laravel-multitenancy: 輕量、靈活,適合喜歡掌控細節的人。
- stancl/tenancy: 功能超級強大,幾乎包山包海(自動建立 DB、自動切換 Domain、整合 Nova 等),是目前的黃金標準。
總結
設計多租戶系統 (Multi-tenant) 是一場對「架構潔癖」的考驗。選擇「多資料庫」模式雖然前期基礎建設比較累,但它帶來的資料隔離性與未來的擴充彈性(例如某個大客戶要搬去獨立伺服器),絕對值得你現在的投入。
別再寫義大利麵程式碼了,讓你的 Laravel 專案優雅地變身為 SaaS 航母吧!
相關閱讀
你的 SaaS 專案架構卡關了嗎?或者是正準備從單一系統轉型為多租戶平台?別讓技術債拖垮你的商業模式。
立即聯繫浪花科技,讓我們幫你打造穩健、可擴展的系統基石!
常見問題 (FAQ)
Q1: 單一資料庫 (Single Database) 和 多資料庫 (Multi Database) 哪種效能比較好?
這取決於資料量。在初期,單一資料庫因為連線池 (Connection Pool) 利用率高,效能可能略好。但隨著資料量爆炸,單一資料庫的大表查詢 (就算有 Index) 效能會下降,且備份困難。多資料庫在物理上隔離了資料,單一租戶的查詢速度不會受其他租戶影響,長期來看擴展性與效能調優空間更大。
Q2: 使用多資料庫模式,Migration 跑起來會不會很久?
會。如果你有 1000 個租戶,部署一次可能需要跑 1000 次 migrate。解決方法包括:1. 平行處理 (Parallel processing) 執行遷移。2. 僅對活躍租戶優先遷移。3. 優化 CI/CD 流程,不要在流量高峰期部署。
Q3: Laravel 內建的 Global Scope 足夠應付單一資料庫的多租戶隔離嗎?
Global Scope 是很好的工具,能自動在所有查詢加上 `where(‘tenant_id’, …)`。但在某些情況下(如 `DB::table()` 原生查詢、或是忘記在 Model 引用 Trait)可能會失效。建議除了 Global Scope,還要在測試階段加上嚴格的單元測試 (Unit Test) 來確保資料隔離。





