从低库存通知到事件驱动架构:一次真实业务中的系统设计思考 前言一个“简单”功能的开始前段时间我接手第一个电商项目时产品经理丢来一个需求“商品库存低于阈值时给商家发个微信提醒最好再来封邮件。”听起来再简单不过。我花半小时写了个decreaseStock()方法里面顺手调了微信接口 —— 测试通过上线。但接下来的三个月我至少在这个“简单功能”上改了七八次代码每次改都心惊胆战。直到后来我接触到事件驱动架构才恍然大悟原来一个通知功能竟然是检验系统设计的绝佳试金石。这篇文章不会教你怎么“用 Laravel 实现低库存通知”因为这样的代码一搜一大把。我想带你走一遍真实的演进之路从一个粗糙的实现开始一步步暴露问题再逐步引入解耦、幂等、频控、队列等工程思想最终让它成长为一个可靠的事件驱动通知系统。希望读完后你再看“发通知”这类需求时眼里不再是“几行 if 和 API 调用”而是一幅可以持续演进的系统蓝图。一、业务背景一个朴素的库存提醒需求1.1 需求描述在商城后台商家维护商品库存。当某个商品的库存数量跌到预设的预警阈值以下时系统需要主动通知商家。具体场景商品iPhone 15当前库存3 件预警阈值5 件触发动作通过微信、邮件发送“库存不足”提醒通知内容通常包括商品名称、当前库存、阈值可能还有补货链接。1.2 初期实现思路我曾经的写法我当时的直觉是库存扣减完顺手判断一下如果低于阈值就直接调用通知接口。流程可以画成一条直线text 用户下单 → 扣减库存 → 判断库存 阈值 ? → 调用微信API → 发送邮件对应的 Laravel 代码大致长这样php public function decreaseStock(Product $product, int $quantity) { $product-stock - $quantity; $product-save(); if ($product-stock $product-threshold) { // 直接发送微信 WechatService::sendTemplateMessage( $product-merchant-wechat_openid, 商品 {$product-name} 库存不足当前仅剩 {$product-stock} 件 ); // 直接发送邮件 Mail::to($product-merchant-email)-send( new LowStockMail($product) ); } }上线之后确实能发通知。但很快噩梦开始了。二、直接调用通知服务埋下的四个深坑2.1 业务代码与通知实现“焊”在一起库存模块的decreaseStock()里既要知道微信怎么发需要 openid、模板 ID又要知道邮件怎么发要构造LowStockMail对象。将来产品说“再加个短信通知”或者“接入企业微信机器人”我就得回来改decreaseStock()。违反的正是开闭原则对扩展开放对修改关闭。库存模块本不该关心“用什么渠道发通知”它只需要说一句“库存低了”就够了。2.2 通知失败会拖垮核心交易有一次微信接口突然超时整整 3 秒才返回错误而decreaseStock()是同步调用的导致用户下单接口响应时间从 200ms 暴涨到 3.2 秒。更可怕的是因为异常未捕获整个事务回滚了 —— 库存没扣成订单却创建了因为订单在另一个事务里。最终库存超卖。通知不该影响核心业务这是血的教训。2.3 无法应对复杂的业务规则几个月后产品又提了新需求同一商家每天最多通知 10 次防止骚扰同一商品30 分钟内不重复提醒避免反复发送商家可以在后台“关闭通知”。如果继续往decreaseStock()里塞这些逻辑代码很快就变成一锅粥。2.4 扩展性为零后来公司引入了新的通知渠道如钉钉、APP 推送每次都要修改库存模块。通知渠道的变更竟然要牵动核心业务代码这显然不合理。这些问题其实都可以归结为两个字耦合。三、引入事件驱动第一次优雅转身3.1 事件驱动是什么事件驱动的核心思想很简单一个业务动作发生后只发布一个“事件”至于这个事件被谁处理、怎么处理发布者一概不管。用库存通知为例库存扣减完成后如果低于阈值就发布一个LowStockEvent至于谁负责发微信、发邮件、记日志那是事件监听器的事。库存模块从此“两耳不闻窗外事”只负责自己的核心职责。3.2 新架构的示意图text ┌─────────────┐ 发布事件 ┌─────────────────┐ │ 库存服务 │ ───────────────▶ │ LowStockEvent │ │ (扣减库存) │ └────────┬────────┘ └─────────────┘ │ ┌───────┴────────┐ │ 事件监听器 │ └───────┬────────┘ │ ┌─────────────────┼─────────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │微信通知 │ │邮件通知 │ │日志记录 │ └──────────┘ └──────────┘ └──────────┘3.3 Laravel 中的落地代码定义事件它只是一个携带数据的 DTOphp namespace App\Events; use App\Models\Product; class LowStockEvent { public Product $product; public function __construct(Product $product) { $this-product $product; } }在库存扣减方法中发布事件php public function decreaseStock(Product $product, int $quantity) { $product-stock - $quantity; $product-save(); if ($product-stock $product-threshold) { event(new LowStockEvent($product)); // 仅发布事件 } }创建监听器独立于库存服务php namespace App\Listeners; use App\Events\LowStockEvent; use App\Services\NotificationService; class LowStockListener { public function handle(LowStockEvent $event) { NotificationService::sendLowStockAlert($event-product); } }然后在EventServiceProvider中注册监听关系。从此库存服务不再知道“通知”二字。将来增加短信、钉钉只需要添加新的监听器库存代码纹丝不动。效果耦合解除了但问题也才刚刚开始。四、通知模块职责划分别造“万能类”解耦之后我把所有通知逻辑塞进了NotificationService它既判断重复、又统计次数、还负责选渠道、发消息。两个月后这个类膨胀到 800 行改一个渠道可能影响另一个测试都不敢动。4.1 错误的设计text ┌──────────────────────────────────────┐ │ NotificationService │ │ - 判断库存是否低于阈值重复业务 │ │ - 查询今日已发次数 │ │ - 检查是否重复发送 │ │ - 选择发送渠道 │ │ - 调用微信/邮件/短信 API │ │ - 记录发送日志 │ └──────────────────────────────────────┘这个类既管“该不该发”又管“怎么发”违反单一职责。4.2 正确的分层我把它拆成两层业务决策层监听器 / 服务负责判断“是否应该发送”库存真的低于阈值了吗今天发送次数超限了吗距离上次发送是否超过 30 分钟商家关闭通知了吗通知执行层渠道服务只负责“如何发送”WechatChannel::send($message)EmailChannel::send($message)SmsChannel::send($message)这样决策逻辑与渠道实现彻底分离。以后调整频控策略只改业务层更换短信供应商只改渠道层。php // 监听器业务决策 class LowStockListener { public function handle(LowStockEvent $event) { if (! $this-shouldSend($event-product)) { return; } $message $this-buildMessage($event-product); app(NotificationDispatcher::class)-send($message); } private function shouldSend(Product $product): bool { // 检查商家是否开启通知、今日次数、重复间隔…… return true; } } // 分发器选择渠道 class NotificationDispatcher { public function send(Message $message) { foreach ($this-channels as $channel) { $channel-send($message); } } }五、消息幂等如何避免商家被“轰炸”5.1 重复通知的源头商品库存不是一次性从 100 降到 0而是一点点减少。比如10:00 库存从 10 变为 4低于阈值 5 → 发送一次10:05 库存从 4 变为 3还是低于阈值 → 又发送一次10:10 库存从 3 变为 2 → 第三次……商家在 10 分钟内收到三条几乎一样的“库存不足”提醒必然投诉。5.2 幂等方案通过通知记录去重引入一张notification_records表记录每一次已发送的通知关键信息merchant_idproduct_idtypesent_at100188001low_stock2026-08-08 10:00:00在发送前查询最近 30 分钟内是否已有相同商品、相同类型的通知记录。若有则跳过本次发送。php private function shouldSend(Product $product): bool { $recent NotificationRecord::where(merchant_id, $product-merchant_id) -where(product_id, $product-id) -where(type, low_stock) -where(sent_at, , now()-subMinutes(30)) -exists(); return ! $recent /* 其他条件 */; }5.3 幂等思想的价值幂等性Idempotence是分布式系统里的核心概念同一个操作执行一次和执行多次结果保持不变。在支付回调、消息队列消费、订单处理中幂等设计是防重复的关键。在这里我们只不过把思想用到了一个“小通知”上但它背后的工程价值是相通的。简单的去重背后是严谨的幂等哲学。六、频控设计别把第三方服务打垮6.1 为什么要限流假设某天由于系统异常库存服务一次性生成了 10000 个LowStockEvent监听器会疯狂调用微信和邮件接口。微信接口限流直接封禁我们的 IP邮件服务商也报警了。更严重的是商家手机被 10000 条微信炸得死机。6.2 频控应该放在哪错误做法把限流逻辑放到WechatChannel里让渠道服务自己计数。但渠道服务不该理解“今天能发几条”这种业务规则。正确做法放在业务决策层监听器或专门的中介服务在调用渠道之前进行频控检查。php private function shouldSend(Product $product): bool { // 1. 检查重复 // 2. 检查今日总次数 $todayCount NotificationRecord::where(merchant_id, $product-merchant_id) -where(type, low_stock) -whereDate(sent_at, today()) -count(); if ($todayCount 10) { return false; } return true; }频控策略可以根据商户等级动态配置未来甚至可以做到滑动窗口等更复杂的算法但无论如何决策层要清晰。七、系统进一步演进消息队列与异步化当业务量再上一个台阶事件监听器同步执行可能成为瓶颈。比如一次库存变化触发了 5 个监听器微信、邮件、日志、数据分析、风控总执行时间可能超过 1 秒影响用户体验。这时候我们可以将事件发布到消息队列由独立的消费者异步处理。7.1 异步架构图text ┌─────────────┐ 发布事件 ┌──────────────┐ 投递 ┌─────────────┐ │ 库存服务 │────────────▶│ 消息队列 │───────────▶│ 通知消费者 │ └─────────────┘ │ (Redis/Kafka)│ └──────┬──────┘ └──────────────┘ │ ┌───────┴───────┐ │ 微信 / 邮件 │ └───────────────┘7.2 Laravel 中的队列化Laravel 的事件系统天然支持队列只需让监听器实现ShouldQueue接口php use Illuminate\Contracts\Queue\ShouldQueue; class LowStockListener implements ShouldQueue { public function handle(LowStockEvent $event) { // 该监听器将被推送到队列异步执行 } }优势异步库存扣减后立即返回不等待通知完成响应更快。失败重试如果微信接口临时不可用队列可以自动重试可配置次数和间隔。削峰填谷大量通知堆积时消费者以可控速度消费保护下游依赖。可观测性队列监控可以直观看到积压情况方便扩容。当然异步也带来新挑战如消息丢失、重复消费、顺序性问题但这些都有成熟的解决方案是工程师成长路上的必修课。八、从一个通知功能看后端工程能力成长地图回顾整个过程我把它整理成一条成长曲线阶段做法问题解决方案阶段1在业务方法里直接调 API耦合严重、影响核心业务事件驱动解耦阶段2把所有逻辑塞进一个通知类职责混乱、难以维护分层设计决策层/执行层阶段3重复发送骚扰商家体验差、浪费资源幂等 通知记录阶段4瞬时流量打垮第三方系统不可用频控 限流阶段5同步执行拖慢响应性能瓶颈消息队列异步化每一个阶段都是对“工程思维”的锤炼。从“能跑就行”到“可靠、可维护、可扩展”这个转变不是靠背诵设计模式而是靠一次次踩坑后的反思。真正优秀的后端工程师不是能最快实现需求的人而是能在设计阶段就预见到变化、给系统留出呼吸空间的人。九、总结从“堆代码”到“做设计”写这篇文章时我一直在想如果当初接手那个“简单库存提醒”时我能有现在的认知会少走多少弯路一个看似不起眼的通知功能串联起了事件驱动、解耦、幂等、频控、异步队列等一整套思想。这些思想并不仅限于“库存通知”它们是构建任何可靠分布式系统的基石。下次当你接到一个“发消息”的需求时不妨问自己几个问题如果通知服务挂了会影响核心业务吗重复发消息怎么办流量洪峰来了下游扛得住吗今天加短信明天加钉钉我的代码需要改多少把这些问题的答案写进你的设计文档而不是堆进代码里。你的系统会感谢你。希望这篇文章能给你带来一些启发。如果你也在实践中遇到过类似的困惑欢迎在评论区交流你的故事。