SaaS 開發者必讀:Laravel 多租戶 (Multi-tenant) 資料庫隔離策略與實戰設計——從單一資料庫到獨立實體的抉擇

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

打造 SaaS 帝國:Laravel 多租戶架構核心抉擇


您的 Laravel 專案即將升級成 SaaS 服務嗎?從單一用戶邁向多租戶(Multi-tenant)是架構上的生死抉擇。本文深入剖析最關鍵的資料庫隔離策略:是採用簡便但高風險的「單一資料庫+Global Scope」,還是選擇資安極致的「獨立資料庫」?我們不僅提供實戰 Trait 範例,更揭露快取污染和佇列身份識別的隱藏地雷。別讓一個疏忽造成數據洩漏的災難!立即掌握 Laravel SaaS 最佳實踐,確保您的系統穩固且可擴展,並探索如何利用專業套件避免重複造輪子!

需要專業協助?

聯絡浪花專案團隊 →

SaaS 開發者必讀:Laravel 多租戶 (Multi-tenant) 資料庫隔離策略與實戰設計

嗨,我是 Eric,浪花科技的資深工程師。如果你正在看這篇文章,八成是你手上的 Laravel 專案被老闆要求:「欸,我們這個系統很棒,下個月能不能改成 SaaS 賣給其他一百家公司用?」

這時候你的內心可能正在崩潰。從「單一用戶」變成「多租戶(Multi-tenant)」,絕對不是複製貼上程式碼那麼簡單。這不僅僅是加一個 tenant_id 的問題,更牽涉到資料庫架構、快取隔離、佇列(Queue)處理,甚至是最敏感的資安邊界。

別擔心,這條路我也走過不少冤枉路。今天我們就來深入探討 Laravel 多租戶系統(Multi-tenant)設計 的核心邏輯,我會直接攤開最血淋淋的架構選擇題,告訴你該選「單一資料庫」還是「多資料庫」,以及如何優雅地用 Laravel 實作。

什麼是多租戶架構(Multi-tenancy)?

簡單來說,多租戶架構就是讓「一套程式碼(Codebase)」同時服務「多個客戶(Tenants)」。每個客戶都覺得自己擁有獨立的系統,數據互不干擾。

在 Laravel 開發中,實現多租戶通常有三種主流策略,這裡我們必須先做個生死抉擇,因為選錯了,後面的技術債會還到你哭出來:

  • 共享資料庫,共享 Schema (Shared Database, Shared Schema): 所有租戶的資料都在同一個 Table 裡,靠 tenant_id 欄位區分。
  • 共享資料庫,獨立 Schema (Shared Database, Separate Schemas): 使用 Postgres 的 Schema 功能,但在 MySQL 中較少見(通常直接對應到多資料庫)。
  • 獨立資料庫 (Database per Tenant): 最極致的隔離,每個租戶擁有自己的資料庫檔案。

策略對決:單一資料庫 vs. 獨立資料庫

這是在設計 Laravel SaaS 時最常被問到的問題。Eric 在這裡幫大家整理個比較表,別再憑感覺選了。

1. 單一資料庫 (Single Database)

這是最簡單的實作方式。你在每個 Table (例如 users, orders) 都加上一個 tenant_id 外鍵。

  • 優點:
    • 成本最低(只需要維護一個 DB 實體)。
    • 部署容易,Migration 跑一次就好。
    • 跨租戶的數據分析(例如:計算全平台總營業額)非常容易。
  • 缺點:
    • 開發風險高: 工程師只要忘記在 where 條件加 tenant_id,A 客戶就會看到 B 客戶的訂單,這是絕對的災難(Data Leak)。
    • 備份困難: 如果某個大客戶要求「我要回朔到昨天下午兩點的狀態」,你很難只還原他的資料而不影響其他人。

2. 獨立資料庫 (Database per Tenant)

每個租戶註冊時,系統自動建立一個新的 Database。

  • 優點:
    • 安全性最高: 物理層面的隔離,程式碼寫錯也不容易跨庫撈到資料。
    • 備份靈活: 可以針對付費VIP客戶提供獨立備份還原服務。
    • 擴展性: 可以將大客戶的 DB 搬到獨立的伺服器上。
  • 缺點:
    • 維運成本高: 你有 1000 個租戶,就要跑 1000 次 Migration。
    • 連線數限制: 資料庫伺服器的 Connection 消耗較快。

Eric 的建議: 如果你是做 B2C 或小型 B2B,且租戶數量預期會破萬,選「單一資料庫」。如果你是做中大型 B2B,客戶很在意資安且付得起錢,請務必選「獨立資料庫」。

實戰:在 Laravel 中實作「單一資料庫」隔離

如果你選擇單一資料庫,為了避免工程師「忘記加 where」導致的資安慘劇,我們必須利用 Laravel 的 Global Scopes 機制來自動過濾。

Step 1: 建立 TenantScope

這個 Scope 會自動將所有的查詢加上 where('tenant_id', $currentTenantId)

<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use App\Services\TenantManager;

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        // 假設你有一個 Service 負責管理當前租戶
        $tenantId = app(TenantManager::class)->getTenantId();

        if ($tenantId) {
            $builder->where('tenant_id', $tenantId);
        }
    }
}

Step 2: 建立 Tenantable Trait

為了方便在多個 Model 中重複使用,我們寫一個 Trait。這樣你建立新資料時,也會自動填入 tenant_id

<?php

namespace App\Traits;

use App\Scopes\TenantScope;
use App\Services\TenantManager;

trait Tenantable
{
    protected static function booted()
    {
        static::addGlobalScope(new TenantScope);

        static::creating(function ($model) {
            $tenantId = app(TenantManager::class)->getTenantId();
            if ($tenantId) {
                $model->tenant_id = $tenantId;
            }
        });
    }

    public function tenant()
    {
        return $this->belongsTo(Tenant::class);
    }
}

Step 3: 使用 Trait

現在,你只需要在 ProductOrder 模型中引入這個 Trait,Laravel 就會自動幫你處理隔離了。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Traits\Tenantable;

class Product extends Model
{
    use Tenantable;
    
    // ... 其他程式碼
}

這樣一來,當你寫 Product::all() 時,SQL 其實會執行 select * from products where tenant_id = 1。這就是 Laravel 優雅的地方。

多租戶系統的隱形地雷:佇列與快取

很多人搞定了資料庫,卻死在 Redis 上。請記住,資料庫隔離了,但 Redis 預設是共享的!

1. Cache 污染

假設 A 租戶快取了一個 key 叫 dashboard_stats,B 租戶登入後讀取同一個 key,就會看到 A 租戶的營業額。這會讓你被告到脫褲子。

解法: 修改 Cache Key,或是使用 Tag。例如:Cache::tags(['tenant_1'])->put('key', 'value');,或者在底層覆寫 Cache Prefix。

2. Queue Job 的身份識別

當你把一個 Job 丟到 Queue 裡執行時,背景的 Worker 並不知道現在是哪個租戶。你必須在 Job 的 Payload 裡帶上 tenant_id,並在 Job 開始執行時「切換環境」。

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Tenant;
use App\Services\TenantManager;

class ProcessOrder implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $tenant;

    public function __construct(Tenant $tenant)
    {
        $this->tenant = $tenant;
    }

    public function handle(TenantManager $manager)
    {
        // 在 Job 執行前,手動設定環境
        $manager->setTenant($this->tenant);

        // 執行你的邏輯...
    }
}

總結:不要重複造輪子

雖然上面我示範了手刻的邏輯,主要是為了讓你理解原理。但在實際的大型專案中,我強烈建議使用成熟的套件,例如 stancl/tenancyspatie/laravel-multitenancy

這些套件已經幫你處理了最頭痛的問題:

  • 自動切換資料庫連線 (Database Switching)。
  • Tenant 專屬的 Filesystem (S3 資料夾隔離)。
  • 域名識別 (Subdomain Identification)。
  • Redis Prefix 自動注入。

SaaS 開發是一場馬拉松,架構設計得好,後期維護是享受;架構沒弄好,每天都在修 Data Leak 的 Bug。希望這篇文章能成為你打造 SaaS 帝國的第一塊穩固基石。

延伸閱讀

如果你對 SaaS 架構與 Laravel 進階開發有興趣,這幾篇也是我不藏私的推薦:

你的企業 SaaS 系統正面臨擴展瓶頸,或是資料庫效能卡關嗎?浪花科技擁有豐富的 Laravel 企業級架構經驗。

立即填寫表單聯繫我們

常見問題 (FAQ)

Q1: 使用獨立資料庫模式 (Database per Tenant) 會不會很難維護 Migration?

確實會增加維護成本。當你有 500 個租戶時,部署新功能需要對 500 個資料庫執行 Migration。這通常需要搭配自動化腳本或使用專門的套件(如 stancl/tenancy)提供的指令來並行處理,絕對不能依賴手動操作。

Q2: 多租戶系統的檔案上傳 (如 S3) 該如何隔離?

如果所有租戶共用同一個 S3 Bucket,建議在路徑上做區隔。例如 `bucket-name/tenant-1/avatar.jpg` 和 `bucket-name/tenant-2/avatar.jpg`。這可以透過 Laravel 的 Filesystem 設定,動態修改 Root Path 來達成,確保 A 租戶永遠無法存取到 B 租戶的資料夾。

Q3: 單一資料庫模式下,如何確保 tenant_id 的唯一性與安全性?

除了在程式碼層面使用 Global Scopes 之外,建議在資料庫層面設定 Foreign Key Constraint 雖然較難(因為 tenant 也是資料),但至少要設定複合索引(Composite Index),例如 `(tenant_id, user_email)` 設為 Unique,防止資料錯亂。同時,務必撰寫自動化測試(Feature Tests)來模擬跨租戶存取,確保系統會回傳 403 或 404。

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