你的 Laravel 通知系統是「騷擾狂」嗎?資深工程師教你打造高併發、防打擾的 Notifications 進階架構

2026/01/28 | Laravel技術分享, 全端與程式開發, 架構與效能優化

終結通知風暴:Laravel 高階 Notifications 實戰

您的 Laravel 通知系統是否在高峰期變成了轟炸用戶的「騷擾狂」?資深工程師深入剖析企業級 Notifications 進階架構,教您實作摘要聚合(Digest)、動態路由控制與高併發控頻(Throttling)機制,徹底擺脫系統過載與用戶疲勞。立即升級您的通知系統,將珍貴的用戶注意力轉化為高黏著度,讓您的 App 從系統殺手變成貼心的私人秘書!

需要專業協助?

聯絡浪花專案團隊 →

你的 Laravel 通知系統是「騷擾狂」嗎?資深工程師教你打造高併發、防打擾的 Notifications 進階架構

嗨,我是 Eric,浪花科技的資深工程師。今天我們來聊聊一個讓很多開發者「愛恨交織」的功能:Laravel 通知系統(Notifications)

為什麼說愛恨交織?因為 Laravel 的 Notification 系統設計得太優雅了,優雅到你寫一行 $user->notify(new OrderShipped($order)); 就能發送郵件、簡訊、甚至 Slack 通知。但這也是災難的開始。

想像一下,你的電商平台正在舉辦「雙11」活動,當用戶在短時間內觸發了 50 次狀態更新,你的系統是不是就真的乖乖寄了 50 封 Email 給他?如果是一萬個用戶同時在操作,你的 SMTP Server 是不是已經開始冒煙,或者被 Gmail 判定為垃圾郵件發送者了?

這不是危言聳聽,這是我在重構無數個「義大利麵專案」時最常看到的慘況。很多開發者只學會了「怎麼發送」,卻忽略了「怎麼管理發送」。

今天這篇文章,不教你基礎的怎麼寄信(官方文件都有),我們要深入探討企業級的通知架構:如何實作通知摘要(Digest)頻率限制(Throttling)以及使用者偏好管理,讓你的 App 從「騷擾狂」變成貼心的「私人秘書」。

為什麼標準的 Laravel Notification 還不夠?

Laravel 預設的 Notification 其實是一個「觸發即發送(Fire and Forget)」的機制。雖然我們可以透過 ShouldQueue 介面將發送工作丟到隊列(Queue)中,解決了網頁卡頓的問題,但並沒有解決「過度通知(Notification Fatigue)」的問題。

在使用者體驗(UX)中,過多的通知是導致用戶關閉權限、甚至刪除 App 的元兇之一。作為工程師,我們需要思考以下三個層面的架構優化:

  • 聚合(Aggregation):將短時間內的多條通知合併為一條(例如:「Eric 和其他 5 人按了讚」)。
  • 控頻(Throttling):限制特定時間內的發送上限。
  • 路由控制(Routing Control):讓用戶自己決定「訂單通知要寄信,但行銷訊息只要站內通知」。

實戰一:實作「通知摘要(Digest)」模式

這是最常見的需求。假設你是一個專案管理工具,當有人在任務中留言時,你希望通知相關人員。如果一分鐘內有 10 則留言,你應該寄送一封包含 10 則留言的「摘要郵件」,而不是 10 封信。

在 Laravel 中,我們可以利用 CacheDatabase 先暫存通知,再透過 Scheduled Job 統一發送。

步驟 1: 建立一個暫存機制

我們不直接發送 Notification,而是先將事件「推入」一個暫存區。這裡我喜歡用 Redis List,因為速度快且支援原子操作。


// 在你的 Service 或 Event Listener 中
use Illuminate\Support\Facades\Redis;

public function handleNewComment($comment)
{
    $userId = $comment->post->author_id;
    $key = "notifications:digest:{$userId}";
    
    // 將留言 ID 或內容推入 Redis List
    Redis::rpush($key, json_encode([
        'id' => $comment->id,
        'content' => substr($comment->content, 0, 50),
        'time' => now()->toIso8601String(),
    ]));
    
    // 設定過期時間,避免累積太多垃圾資料
    Redis::expire($key, 3600 * 24);
}

步驟 2: 排程發送摘要

接下來,我們寫一個 Command 或 Job,每 15 分鐘或 30 分鐘執行一次,檢查有哪些用戶有未發送的摘要。


// Console/Commands/SendNotificationDigests.php

public function handle()
{
    // 這裡為了簡化,假設我們遍歷活躍用戶,實際場景可用 Redis SCAN 或 Set 來記錄有哪些用戶需要通知
    $users = User::where('is_active', true)->get();

    foreach ($users as $user) {
        $key = "notifications:digest:{$user->id}";
        $pendingNotifications = Redis::lrange($key, 0, -1);

        if (empty($pendingNotifications)) {
            continue;
        }

        // 解碼資料
        $comments = array_map(fn($item) => json_decode($item, true), $pendingNotifications);

        // 發送真正的 Laravel Notification
        $user->notify(new CommentDigestNotification($comments));

        // 發送成功後,清除 Redis
        Redis::del($key);
    }
}

這樣一來,無論對方留言多快,用戶每段時間最多只會收到一封整理好的郵件。這才是有「儀式感」的通知系統。

實戰二:精細的「使用者偏好」路由控制

Laravel 的 Notification 類別中有一個 via() 方法,這就是我們控制「發送管道」的靈魂所在。很多新手只會寫死 return ['mail', 'database'];,這是不及格的。

我們應該讓資料庫中的使用者設定來決定回傳什麼陣列。

資料庫設計思路

建議建立一張 notification_settings 表,或在 users 表中有一個 JSON 欄位 preferences


// user->preferences 範例
{
    "new_comment": {
        "mail": true,
        "database": true,
        "slack": false
    },
    "order_update": {
        "mail": true,
        "database": true,
        "sms": true
    }
}

動態的 via() 方法

在 Notification 類別中,我們可以這樣寫:


namespace App\Notifications;

use Illuminate\Notifications\Notification;

class OrderUpdated extends Notification
{
    public function via(object $notifiable): array
    {
        $channels = [];
        $preferences = $notifiable->preferences['order_update'] ?? [];

        // 預設開啟資料庫通知
        if ($preferences['database'] ?? true) {
            $channels[] = 'database';
        }

        // 只有用戶明確開啟 Email 才發送
        if ($preferences['mail'] ?? false) {
            $channels[] = 'mail';
        }

        // 簡訊成本高,預設關閉
        if ($preferences['sms'] ?? false) {
            $channels[] = TwilioChannel::class;
        }

        return $channels;
    }
}

這段程式碼的價值在於,它將控制權交還給了用戶,同時也幫公司節省了不必要的 SMS 或 Email 費用(這些 API 可都是要錢的啊!)。

實戰三:防止意外的無限迴圈(Circuit Breaker)

工程師的噩夢之一:系統發生錯誤 -> 發送錯誤通知 -> 通知發送失敗 -> 觸發錯誤處理 -> 再次發送錯誤通知。這就是傳說中的「通知風暴」。

在 Laravel 11 中,我們可以利用 Redis 的 Rate Limiter 來做最後一道防線。即使前面的邏輯都失效了,我們也要確保不會在一分鐘內對同一個用戶轟炸 100 封信。


use Illuminate\Support\Facades\RateLimiter;

public function via(object $notifiable): array
{
    $key = "notification:throttle:{$notifiable->id}";

    // 限制每分鐘最多 5 則通知
    if (RateLimiter::tooManyAttempts($key, 5)) {
        // 可以在這裡記錄 Log,或者只回傳 database channel (不干擾用戶)
        return ['database'];
    }

    RateLimiter::hit($key, 60);

    return ['mail', 'database'];
}

這個簡單的 RateLimiter 就像是家裡的保險絲,雖然平時沒感覺,但在關鍵時刻能救你的 SMTP Server 一命。

2025 新趨勢:Laravel Reverb 與即時通知

隨著 Laravel 11 推出的 Laravel Reverb,WebSocket 變得前所未有的好用。現在的通知系統不僅僅是「發送」,更強調「即時互動」。

當你使用 BroadcastMessage 時,配合前端的 Laravel Echo,可以做到像 Facebook 一樣,不用重新整理頁面,鈴鐺圖示就會自動亮起紅點。這對於提升使用者黏著度非常有幫助。

要在 Notification 中使用廣播,只需要實作 toBroadcast 方法:


use Illuminate\Notifications\Messages\BroadcastMessage;

public function toBroadcast(object $notifiable): BroadcastMessage
{
    return new BroadcastMessage([
        'title' => '訂單已出貨',
        'body' => '您的訂單 #1234 已經在運送途中,點擊查看詳情。',
        'action_url' => url("/orders/{$this->order->id}"),
    ]);
}

Eric 的碎碎念總結

寫了這麼多 Code,其實核心觀念只有一個:「把通知當成一種珍貴的資源」。每一次的 $user->notify() 都在消耗用戶的注意力(和你的伺服器資源)。

一個優秀的後端工程師,不會只滿足於「功能做出來了」,而是會去思考系統在極端狀況下的表現,以及對使用者的干擾程度。從今天開始,試著把 Middleware、Queue 和 Redis 整合進你的通知系統,你會發現,原來寫 Notification 也可以這麼有架構感。

延伸閱讀:讓你的 Laravel 架構更上一層樓

如果你對今天提到的技術感興趣,這裡有幾篇我精選的實戰文章,強烈建議搭配服用:

想為您的企業打造高併發、可擴展的系統架構嗎?

浪花科技擁有豐富的 Laravel 開發與效能優化經驗,協助企業解決技術債與效能瓶頸。

立即聯繫我們,預約技術諮詢

常見問題 (FAQ)

Q1: 資料庫通知 (Database Channel) 累積太多資料會影響效能嗎?

絕對會。notifications 表格如果不定期清理,會變得非常巨大,導致查詢變慢。建議實作一個排程 (Prunable),定期刪除 30 天或 90 天以前的已讀通知。Laravel Model 的 Prunable trait 非常適合用在這裡。

Q2: 使用 Queue 發送通知時,如果失敗了怎麼辦?

Laravel 的 Queue 機制有自動重試 (Retry) 功能。你可以在 Notification 類別中定義 $tries$backoff 屬性,設定重試次數和等待時間。對於像 Email 這種可能被 Rate Limit 的服務,使用指數退避 (Exponential Backoff) 是最佳實踐。

Q3: 為什麼我的通知 Email 樣式在 Outlook 或 Gmail 手機版跑版了?

這是 Email 開發的千古難題。Laravel 預設使用 Markdown Mailables,已經盡量相容各大客戶端。如果需要高度客製化,建議不要手刻 HTML,而是使用像 MJML 這樣的框架來生成響應式 Email 模板,再匯入 Laravel 使用。

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