目录导读
- 为什么需要优先级?从“通知风暴”说起
- Laravel 通知系统的核心机制回顾(Queue 与 Channel)
- 基础篇:利用
via()方法实现静态频道过滤 - 进阶篇:动态决策与
shouldSend()的妙用(基于优先级) - 实战篇:结合 Redis 队列与延迟参数实现“分级降级”策略
- 性能陷阱与避坑指南(高频发送、失败重试)
- 常见问题解答(FAQ)
为什么需要优先级?从“通知风暴”说起
在现代 PHP 项目中,尤其是基于 Laravel 框架构建的 SaaS 系统或电商平台,通知系统往往是业务触达的核心,假设你的应用拥有 10 万活跃用户,当某件商品降价或订单状态变更时,若同时通过邮件、短信、数据库推送(Database)和实时推送(Pusher)发送通知,立刻会形成“通知风暴”,这不仅导致服务器负载飙升、第三方 API 费用剧增,更严重的是,低优先级通知(如营销邮件)会淹没高优先级通知(如密码重置、安全警报)。设置通知频道的优先级并非“锦上添花”,而是保障系统稳定与用户体验的“生死线”。

Laravel 通知系统的核心机制回顾(Queue 与 Channel)
Laravel 的通知系统由 Notification 类与 Channel 构成,默认情况下,通知通过 notify() 方法同步发送,这会阻塞请求,为了提升性能,我们通常将通知实现 ShouldQueue 接口,让通知进入队列异步处理,关于频道优先级设置,核心在于两点:发送渠道的选择(邮件/短信/数据库)与发送顺位的控制,许多开发者误以为优先级是“某个频道先发、另一个后发”,实际上更重要的是 “在何种条件下发送哪个频道”。
基础篇:利用 via() 方法实现静态频道过滤
最简单的方式是在通知类中定义 via($notifiable) 方法,返回一个频道数组。
public function via($notifiable)
{
// 如果是 VIP 用户,发送短信+邮件;否则只发邮件
if ($notifiable->isVip()) {
return ['mail', 'sms'];
}
return ['mail'];
}
这是“用户级别优先级”的雏形,但这种方式是静态的,无法应对“同一用户在不同时间、不同业务场景下的复杂要求”。
进阶篇:动态决策与 shouldSend() 的妙用(基于优先级)
真正的优先级设置,需要结合业务逻辑优先级与成本优先级,Laravel 允许 Notification 类实现 shouldSend($notifiable, $channel) 方法,它会针对每个频道单独调用,这是设置优先级的关键战场。
案例实操:假设我们要发送“订单发货提醒”,我们认为:数据库站内信(成本最低)> 邮件(成本中等)> 短信(成本最高),我们要实现的目标是:如果用户最近 24 小时内已在我们网站活跃过,则只发站内信(低优先级,省钱);如果用户 48 小时未活跃,则优先发邮件;如果订单金额超过 1000 元且用户未读站内信,则追加发送短信(高优先级,保体验)。
public function shouldSend($notifiable, $channel)
{
// 站内信(Database)优先级最低
if ($channel === 'database') {
return true; // 总是写入数据库
}
// 邮件优先级中等
if ($channel === 'mail') {
// 如果用户 24 小时内活跃过,不发送邮件(依靠数据库通知即可)
return $notifiable->last_active_at < now()->subHours(24);
}
// 短信优先级最高(成本高)
if ($channel === 'sms') {
// 仅当订单金额大且数据库通知未被读取时,才触发短信
$dbNotification = $notifiable->notifications()
->where('type', static::class)
->first();
$orderAmount = $this->order->amount;
return ($orderAmount > 1000 && $dbNotification && is_null($dbNotification->read_at));
}
return false;
}
shouldSend 的顺序决定了逻辑优先级——数据库先执行,短信最后执行,通过这种方式,你不仅控制了频道优先级,还实现了“降级策略”:高成本渠道只在低成本渠道无法触达时启用。
实战篇:结合 Redis 队列与延迟参数实现“分级降级”策略
在大型 PHP 项目中,我们很少只依赖 shouldSend,真正的“优先级”往往体现在时间先后上,使用 Laravel 队列的自定义延迟可以做到这一点。
策略设计:
- 第一步:立即推送数据库通知与实时推送(Pusher),保证用户在当前会话中能看到。
- 第二步:延迟 5 分钟后发送邮件(低优先级)。
- 第三步:30 分钟后,检测到用户仍未读数据库通知,则触发高优先级短信。
在这个方案中,我们不能仅靠一个 Notification 类,而需要拆分为多个通知或使用通知调度器。
代码实现思路:
// 在 OrderShipped 事件中 $user->notify((new OrderShippedDatabase($order))->delay(now()->addSeconds(0))); // 高优先级 数据库 $user->notify((new OrderShippedMail($order))->delay(now()->addMinutes(5))); // 中优先级 邮件 // 对于短信,使用调度任务 (Example: Laravel Task Scheduling) // 每 10 分钟检查一次未读的 Database 通知,对未读用户触发 Sms 通知
这种方法将“发送顺序”转换为“时间优先级”。 短信作为最后一道防线,只在其他低优先级频道失效时补发,这是最符合商业逻辑的“优先级”定义。
性能陷阱与避坑指南(高频发送、失败重试)
阻塞队列,如果使用 sync 驱动,shouldSend 里的数据库查询会阻塞请求,导致 TPS 急剧下降。解决方案:务必配置 redis 或 beanstalkd 作为队列驱动,并开启 --queue=high,low 处理多队列。
权限校验冗余,在 via() 或 shouldSend() 中,避免执行复杂的业务逻辑(如查询报表),因为这会在队列消费时执行,同样消耗 CPU。建议:将决策所需的变量(如金额、用户是否活跃)提前在调用 notify() 的控制器中计算好,并通过 public 属性传递到通知类中。
失败重试的死循环,当高优先级短信发送失败,由于 Laravel 默认对 ShouldQueue 会重试 3 次,如果短信服务商持续宕机,则重试会阻塞队列。优化:为通知设置 $tries 和 backoff(回退时间),若短信失败,建议在 failed() 方法中记录日志并调用邮件作为备用兜底,而不是无限重试。
常见问题解答(FAQ)
Q1:能否在运行时全局切换所有通知的优先级?
A:可以,推荐使用 Laravel 的管道(Pipeline)或事件监听器,监听 NotificationSending 事件,如果满足条件(例如系统维护状态),则修改 $event->channel 或返回 false 阻止发送。
Q2:shouldSend 和 via 哪个优先执行?如何处理冲突?
A:Laravel 先执行 via() 确定数组列表,然后遍历该列表,并对每个频道调用 shouldSend()。shouldSend 返回 false,则该频道被跳过。shouldSend 的优先级高于 via,逻辑上,via 负责“广撒网”,shouldSend 负责“精准拦截”。
Q3:如何监控各个频道的通知成功率?
A:在通知类中实现 failed($notifiable, $e, $channel) 方法,将失败信息记录下来,推荐使用 Laravel Pulse 或 Sentry 结合自定义事件来追踪。
Q4:数据库通知(站内信)有自动清理机制吗?
A:Laravel 没有内置清理,建议编写调度命令定期清理 notifications 表中已读且超过 30 天的记录,避免表膨胀导致 shouldSend 中的查询变慢。
在复杂的 PHP 项目中,Laravel 通知频道的优先级设置并非指“固定数组顺序”,而是一套基于用户状态、业务成本与时间窗口的动态决策模型,通过巧妙地组合 shouldSend() 与队列延迟,你可以构建一套弹性伸缩的通知系统:既保证了高优先级触达(安全/交易),又控制了低优先级成本(营销/活动)。优先级的终极目标不是“不让用户收到广告”,而是“让用户适时的收到需要的信息”,开发时,请务必做好监控,确保每一个决策都在掌控之中。