事件驱动非高并发银弹,需配合数据库锁防超卖;ShouldBroadcast须配独立Redis队列并监听;高频事件应裁剪数据、避免同步执行;CQRS下推荐事件表+轮询保障最终一致;事件类必须显式指定Redis连接。

事件驱动不是高并发的“银弹”,它本身不解决并发竞争,而是把耗时、可异步、非核心路径的逻辑从请求生命周期里摘出来——真正防超卖、防重复扣款、防脏写,还得靠数据库锁或原子 SQL。
ShouldBroadcast 用错就拖垮 Redis
实现 ShouldBroadcast 接口后,Laravel 默认会把事件塞进广播队列(比如 Redis 的 laravel_database_broadcasting 队列),但很多人忽略两点:一是没配 BROADCAST_QUEUE 导致和普通任务混用一个队列,二是没开队列监听导致事件积压阻塞主线程。
- 必须在
.env显式指定独立广播队列:BROADCAST_QUEUE=redis-broadcast - 启动单独的队列监听进程:
php artisan queue:work --queue=redis-broadcast --max-jobs=100,避免和邮件、通知等任务争抢资源 - 私有频道(
PrivateChannel)触发授权请求,若BroadcastController@auth走了 Eloquent 查询且没加索引,单次授权可能耗时 200ms+,直接压垮 PHP-FPM - 高频事件(如聊天消息)别广播原始模型对象,用
toArray()或 DTO 显式裁剪字段,避免序列化整个User模型带出关系数据
event() 同步调用是隐藏的性能陷阱
默认 event(new OrderShipped($order)) 是同步执行所有监听器,哪怕监听器里只有一行 Mail::to(...)->send(...),也会卡住当前请求。这不是 Laravel 的 bug,是设计使然——它不自动帮你投队列。
- 监听器类必须显式实现
ShouldQueue接口,否则永远同步执行 - 不要在监听器
handle()里手动调用dispatch(new SendEmailJob(...)),这是冗余操作;直接让监听器自己上队列更轻量 - 若监听器依赖数据库连接,记得在
__construct()中不加载模型实例,改用 ID 延迟到handle()再查,避免序列化失败或连接泄漏 - 高频事件(如用户点击埋点)建议先写入 Kafka 或本地日志文件,再由后台服务统一消费,绕过 Laravel 队列瓶颈
事件 + CQRS 协同时最容易漏掉投影更新一致性
CQRS 场景下,命令处理器发完 UserRegistered 事件,监听器去更新 Elasticsearch 或物化视图,但没人保证“写库已提交”和“投影已落地”是原子的。MySQL 事务提交了,ES 还没收到,查询端就看到脏数据。
- 别在监听器里直接调用
ProductSearch::update($product),ES 更新失败会导致数据永久不一致 - 推荐用「事件表 + 投影服务轮询」模式:监听器只往
event_store表插入一条记录,后台服务定时拉取未处理事件并重试,失败则记录死信 - 若必须强一致,把投影更新放进同一个 MySQL 事务(仅限同库),用
DB::transaction()包裹写库操作 + 插入事件记录,再由监听器异步触发最终推送 - Laravel 11 的轻量级事件调度器不支持事务绑定,所以
DB::transaction()内event()调用仍可能在事务外触发监听器——这点极易被忽略
最常被跳过的一步:没给事件类加 public $connection = 'redis',结果所有事件都走默认 database 驱动,在高并发下直接打满 SQLite 或 MySQL 连接池。Redis 驱动不是可选项,是高并发事件系统的基础设施底线。



















