Scaling Laravel: Mastering CQRS Architecture 🏗️ 封面圖片

Prajapati Paresh

傳統 CRUD 的瓶頸

當您建置標準的 Laravel 應用程式時,您通常會遵循 CRUD(Create、Read、Update、Delete)典範。您會有單一的 User 模型和單一的 UserController。此控制器處理寫入資料(建立使用者)和讀取資料(取得使用者清單)。在底層,讀取和寫入操作都與完全相同的 MySQL 或 PostgreSQL 資料庫表格互動。

對於大多數應用程式來說,這完全沒問題。然而,在高流量的企業系統中,工作負載很少是對稱的。在報表儀表板或電子商務平台中,您的應用程式可能每執行 1 次「寫入」查詢,就會執行 1,000 次「讀取」查詢。如果您對讀取和寫入操作都使用完全相同的資料庫和完全相同的 Eloquent 模型,那麼繁重的寫入操作(會鎖定資料列並需要交易完整性)將開始阻擋您閃電般的讀取操作,讓您的應用程式陷入停滯。

Smart Tech Devs,當我們架構需要大量可擴展性的平台時,我們會實作 CQRS(Command Query Responsibility Segregation,指揮查詢職責分離)。CQRS 是一種架構模式,嚴格將變更資料的操作(Command,指揮)與讀取資料的操作(Query,查詢)分開。

CQRS 的理念

透過分離 Command 和 Query,我們可以獨立最佳化它們。

  • Command(寫入):這些處理複雜的業務驗證、領域邏輯和交易完整性。它們不回傳資料(除了成功確認之外)。
  • Query(讀取):這些完全繞過複雜的領域邏輯。它們只是盡快取得資料,通常來自高度最佳化、非正規化的讀取複本或 Redis 等快取層。

階段 1:建構 Command 堆疊

讓我們架構一個訂單處理系統。當使用者下訂單時,這是一個複雜的寫入操作,涉及庫存檢查、付款處理和狀態變更。我們將其完全封裝在 Command 物件和 Command Handler 中。


namespace App\CQRS\Commands;

// 1. The Command: A simple Data Transfer Object (DTO)
class PlaceOrderCommand
{
    public function __construct(
        public readonly string $userId,
        public readonly array $items,
        public readonly string $paymentToken
    ) {}
}

接下來,我們建置 Handler。此類別執行業務邏輯。請注意,它不回傳建立的 Order 物件;它只執行變更。


namespace App\CQRS\Handlers;

use App\CQRS\Commands\PlaceOrderCommand;
use App\Models\Order;
use Illuminate\Support\Facades\DB;

class PlaceOrderHandler
{
    public function handle(PlaceOrderCommand $command): void
    {
        DB::transaction(function () use ($command) {
            // 1. Validate inventory (Domain Logic)
            // 2. Process Payment via Stripe
            
            // 3. Persist to the "Write" Database
            $order = Order::create([
                'user_id' => $command->userId,
                'status' => 'processing',
                'total' => calculateTotal($command->items)
            ]);

            // 4. Fire an event to sync the Read Database later
            OrderPlaced::dispatch($order);
        });
    }
}

階段 2:建構 Query 堆疊

現在,讓我們看看讀取端。如果使用者想要檢視其訂單歷史記錄,我們不需要繁重的領域邏輯,甚至不需要 Eloquent 的物件水化(object hydration)開銷。我們只需要原始速度。我們建立一個 Query 物件來定義請求,以及一個 Query Handler 來取得資料。


namespace App\CQRS\Queries;

class GetUserOrdersQuery
{
    public function __construct(public readonly string $userId) {}
}

namespace App\CQRS\Handlers;

use App\CQRS\Queries\GetUserOrdersQuery;
use Illuminate\Support\Facades\DB;

class GetUserOrdersHandler
{
    public function handle(GetUserOrdersQuery $query): array
    {
        // 1. We bypass Eloquent entirely for maximum read speed.
        // 2. We query from the optimized "Read" database connection.
        return DB::connection('mysql_read')
            ->table('user_order_views')
            ->where('user_id', $query->userId)
            ->orderBy('created_at', 'desc')
            ->get()
            ->toArray();
    }
}

階段 3:實體資料庫分離

CQRS 的真正威力在於實體分離資料庫時才能發揮。在 Laravel 中,您可以在 config/database.php 檔案中設定主要/複本連線。您將資料寫入主要資料庫,並從複本讀取資料。


'mysql' => [
    'read' => [
        'host' => ['192.168.1.2', '192.168.1.3'], // Read Replicas
    ],
    'write' => [
        'host' => ['192.168.1.1'], // Primary Master
    ],
    'driver' => 'mysql',
    'database' => env('DB_DATABASE'),
    'username' => env('DB_USERNAME'),
    'password' => env('DB_PASSWORD'),
    // ...
],

當訂單建立(Command)時,它會進入寫入資料庫。我們先前派發的 OrderPlaced 事件會觸發非同步背景工作程式。此工作程式會更新讀取複本上高度非正規化的 user_order_views 表格。這稱為最終一致性(Eventual Consistency)。讀取資料庫可能比寫入資料庫慢幾毫秒,但作為交換,我們可以在不鎖定主要表格的情況下處理數萬個並行讀取。

工程投資報酬率

在 Laravel 中採用 CQRS 不適合簡單的部落格或內部工具;它會引入無可否認的架構複雜性。然而,對於處理高吞吐量報表、大量使用者並行或複雜領域規則的企業系統來說,這是一項改變遊戲規則的技術。透過將讀取與寫入解耦,您可以不對稱地擴展資料庫基礎架構——啟動十台廉價的讀取複本伺服器,同時只維護一台功能強大的主要寫入伺服器。此外,您的程式碼庫會變得高度組織化;最佳化複雜報表查詢的開發人員永遠不會意外破壞核心付款處理邏輯,因為這兩個關注點在實體和邏輯上都是分開的。