拒絕義大利麵!Laravel 多租戶 (Multi-tenant) 系統設計實戰:從資料庫隔離到動態切換的 SaaS 煉金術

2026/01/7 | Laravel技術分享, 企業系統思維, 全端與程式開發

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) 來確保資料隔離。

 
立即諮詢,索取免費1年網站保固