SaaS 創業的技術賭注:Laravel 多租戶 (Multi-tenant) 系統設計實戰——從單一資料庫到獨立隔離的終極抉擇

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

SaaS 生存戰:Laravel 多租戶架構的資料庫抉擇與實戰攻略

SaaS 創業家,您正面臨技術的關鍵抉擇!本指南深入拆解 Laravel 多租戶架構的兩大核心:快速但高風險的「單一資料庫」雅房模式,以及安全隔離但維護複雜的「獨立資料庫」套房模式。我們提供 Global Scopes 和動態連線切換的實戰程式碼,助您避開 Redis 衝突與 Queue 陷阱。資料外洩或 Migration 地獄,您選擇哪一個?立即停止盲目開發,做出最符合您業務需求的架構決策,並諮詢專業建議,確保您的技術藍圖萬無一失!

需要專業協助?

聯絡浪花專案團隊 →

SaaS 創業的技術賭注:Laravel 多租戶 (Multi-tenant) 系統設計實戰

嗨,我是浪花科技的資深工程師 Eric。最近又有新創團隊跑來問我:「Eric,我們想要做一個像 Shopify 那樣的 SaaS 平台,用 Laravel 開發,但資料庫該怎麼設計?是所有客戶塞在同一個資料庫,還是幫每個客戶開一個資料庫?」

老實說,這個問題在工程界就像「粽子該吃南部粽還是北部粽」一樣,永遠有兩派人馬在戰。但在 SaaS (Software as a Service) 的世界裡,這不只是口味問題,而是攸關你未來兩年會不會因為維護資料庫而過勞死的「生存問題」。

今天這篇文章,我不談太虛幻的理論,我們直接從 Laravel 的程式碼實作角度,來拆解 多租戶系統 (Multi-tenant System) 的架構選擇。無論你是剛起步的獨立開發者,還是準備擴張的技術長,這篇應該都能幫你省下不少走冤枉路的時間。

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

簡單來說,就是「一套程式碼,服務多個客戶」。

想像你開了一棟公寓(伺服器與程式碼),每個房客(租戶/Tenant)都有自己的房間(資料)。

  • 單一資料庫 (Single Database): 像是雅房,大家共用客廳衛浴(資料表),只是門上有貼名字(tenant_id)。
  • 獨立資料庫 (Database per Tenant): 像是獨立套房,每個人有自己的衛浴廚房(獨立 Database),完全隔離。

方案一:單一資料庫 (Shared Database) —— 快速卻危險的「雅房」模式

這是最常見,也是初期開發最快的方式。所有的租戶資料都存在同一個 Database 中,每一個 Table (如 orders, products) 都會有一個 tenant_id 欄位。

優點:

  • 成本極低: 不用管資料庫連線切換,主機資源利用率最高。
  • 維護方便:php artisan migrate 一次就搞定,不用對著 100 個資料庫跑迴圈。
  • 跨租戶分析容易: 老闆想看「全平台總營業額」,一行 SQL 就出來了。

缺點:

  • 資料外洩風險高: 這是工程師的惡夢。只要你的 WHERE tenant_id = ? 少寫一次,A 客戶就會看到 B 客戶的訂單。這在 GDPR 或醫療法規下是絕對死刑。
  • 備份困難: 如果大客戶 A 說他誤刪資料要回溯,你很難只還原他的資料而不影響其他人。

Laravel 實作關鍵:Global Scopes

在 Laravel 中,為了避免工程師「忘記」寫 where('tenant_id', $id),我們通常會利用 Eloquent 的 Global Scopes 來自動注入過濾條件。

首先,建立一個 Scope:


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);
        }
    }
}

接著,在你的 Model (或是建立一個 Base Model) 中引用它:


namespace App\Models;

use App\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new TenantScope);

        // 自動在建立資料時寫入 tenant_id
        static::creating(function ($model) {
             $model->tenant_id = app(TenantManager::class)->getTenantId();
        });
    }
}

這樣一來,當你呼叫 Product::all() 時,Laravel 會自動轉成 SELECT * FROM products WHERE tenant_id = 1。這就是所謂的「防呆機制」。

方案二:獨立資料庫 (Database per Tenant) —— 尊爵不凡的「套房」模式

這是企業級 SaaS (如 Salesforce) 偏好的模式。每當有一個新客戶註冊,系統會自動建立一個全新的 Database。

優點:

  • 安全性最高: 物理層級的隔離。就算程式碼有 SQL Injection 漏洞,駭客也只能撈到該租戶的資料,無法跨庫攻擊。
  • 備份與還原靈活: 客戶要付費還原昨天的資料?沒問題,直接 Dump 他的 DB 就好。
  • 效能擴展: 如果某個客戶流量爆衝,可以把他的 Database 搬到一台獨立的高規格 RDS 上,不影響其他小客戶。

缺點:

  • Migration 地獄: 當你發布新功能要新增欄位時,你需要對 1,000 個資料庫跑 Migration。如果第 500 個失敗了,你的部署流程會變得很複雜。
  • 連線數爆炸: 每個 Database 都要消耗連線資源,資料庫伺服器的配置要夠高。

Laravel 實作關鍵:動態切換 Connection

這通常需要依賴 Middleware 來識別進來的 Domain,然後即時切換 Config。


namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Config;
use App\Models\Tenant;

class SwitchTenantDatabase
{
    public function handle($request, Closure $next)
    {
        // 1. 從 Subdomain 辨識租戶
        $host = $request->getHost();
        $subdomain = explode('.', $host)[0];
        $tenant = Tenant::where('subdomain', $subdomain)->firstOrFail();

        // 2. 動態設定資料庫連線
        Config::set('database.connections.tenant', [
            'driver' => 'mysql',
            'host' => $tenant->db_host,
            'database' => $tenant->db_name,
            'username' => $tenant->db_user,
            'password' => $tenant->db_password,
            // ... 其他設定
        ]);

        // 3. 清除並重新連線
        DB::purge('tenant');
        DB::reconnect('tenant');
        
        // 設定預設連線
        DB::setDefaultConnection('tenant');

        return $next($request);
    }
}

除了資料庫,還有這些「坑」

Eric 必須提醒大家,多租戶系統最麻煩的往往不是資料庫,而是周邊設施:

1. Redis Cache 與 Session

如果你用 Redis 存 Session 或 Cache,記得要加上 Prefix。否則 A 客戶登入後,Session ID 剛好跟 B 客戶撞到(雖然機率低),或者 Cache key 叫做 homepage_data,結果大家看到同一份快取,那就尷尬了。

2. 檔案儲存 (Storage)

上傳圖片時,千萬不要只用 /uploads/avatar.jpg。一定要分資料夾:/tenant_1/uploads/avatar.jpg。若是用 S3,這更是基本功。

3. Queue (隊列)

這是最容易被忽略的!當你把 Job 丟到 Queue 裡時,Worker 處理那一刻,它怎麼知道要連線到哪個 Database?
如果你用 spatie/laravel-multitenancy 這種套件,它會幫你在 Job Payload 裡註記 tenant_id,Worker 啟動時會自動切換環境。如果是手刻,記得在 Job 的 handle() 方法裡重新執行一次 Tenant 的切換邏輯。

Eric 的選型建議

寫了這麼多 Code,到底該怎麼選?

  • 選單一資料庫 (Shared): 如果你是 B2C 平台、預算有限、預期會有海量免費帳戶(數萬個以上),且資料結構單純。例如:個人部落格平台、簡單的記帳軟體。
  • 選獨立資料庫 (Isolated): 如果你是 B2B SaaS、客戶付費意願高、極度重視資安、未來可能需要針對大客戶做客製化。例如:ERP 系統、醫療診所管理系統、企業 CRM。

總結

Laravel 在多租戶的生態系已經非常成熟。如果你不想自己造輪子,我強烈推薦研究 Stancl/TenancySpatie/Laravel-Multitenancy 這兩個套件。它們幫你解決了 Migration、Queue、Redis Prefix 等 90% 的髒活。

但在引入套件之前,身為工程師的我們,必須理解底層的運作邏輯,才能在系統出錯時(相信我,一定會出錯),知道是哪個環節的「門」沒關好。

常見問題 (FAQ)

Q1: 已經開發好的單一租戶系統,改成多租戶會很難嗎?

非常痛苦。尤其是如果你選擇「單一資料庫」模式,你需要檢查每一行 Eloquent 查詢,確保都有加上 tenant_id 的過濾。如果選擇「獨立資料庫」模式,雖然程式碼改動較少,但需要重構 Migration 和部署流程。建議在專案啟動初期就決定好。

Q2: 獨立資料庫模式下,如何執行 Migration?

通常會撰寫一個自訂的 Artisan Command,利用迴圈遍歷所有 Tenant,並動態切換連線來執行 migrate 指令。例如:php artisan tenants:migrate

Q3: 多租戶系統的圖片資源該如何處理?

建議在 Storage 層級(如 AWS S3 或 Local Disk)為每個租戶建立獨立的目錄(Directory),例如 /storage/{tenant_id}/...。這樣不僅管理方便,也能在需要時輕鬆計算個別租戶的硬碟使用量。

您的 SaaS 專案架構卡關了嗎?

資料庫設計一但走錯方向,未來的技術債將難以償還。浪花科技擁有豐富的 Laravel 企業級系統開發經驗,協助您做出最正確的架構決策。

立即諮詢 Eric,打造可擴展的系統架構 →

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