終結通知風暴: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 中,我們可以利用 Cache 或 Database 先暫存通知,再透過 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 通知系統玩膩了?帶你手刻 Custom Channel,從簡訊到 LINE Notify 全搞定!
- Laravel Queue 不是跑起來就好!資深工程師的「彈性」與「容錯」背景任務設計聖經
- Laravel + Redis 不只會快取?資深工程師揭秘 3 大進階戰術:即時廣播、API 限流與分散式鎖!
常見問題 (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 使用。






