队列消费者可能未真正运行,需检查进程、Redis队列长度及日志;广播事件中避免DB查询、N+1、序列化大对象等重操作,应预加载数据并隔离Redis连接。

队列消费者是否真的在跑?
广播延迟高,第一反应不是代码慢,而是queue:work根本没在运行。Laravel广播事件必须走队列(除非用ShouldBroadcastNow),一旦消费者挂掉或被误杀,所有广播就卡在Redis里不动。
检查方式很简单:
- 执行
ps aux | grep "queue:work",确认至少有一个进程在运行; - 查Redis中待处理任务数:
redis-cli llen queues:default(把default换成你配置的BROADCAST_QUEUE值); - 如果长度持续上涨且>100,基本可断定消费者没跟上或已僵死。
别信supervisorctl status显示“RUNNING”就万事大吉——它只管进程存在,不管内部是否卡在某个死锁、超时或异常重试中。
消费者卡在哪?看日志比猜更准
默认情况下,queue:work不会输出详细错误,尤其当任务抛出未捕获异常或超时退出时,只会静默失败。广播事件因此堆积,但你完全看不到原因。
必须强制暴露真实问题:
- 启动时加
--verbose --tries=1:例如php artisan queue:work --queue=broadcasts --verbose --tries=1,避免重试掩盖首次失败; - 确保
config/queue.php中redis.retry_after设为合理值(如90),太短会导致任务被重复取走,太长则积压难察觉; - 在
App\Exceptions\Handler::report()里加判断,对Illuminate\Queue\MaxAttemptsExceededException单独记录完整上下文,包括事件类名和$exception->getPrevious()。
常见真凶:广播事件里调用了DB::transaction()但没设超时,或broadcastWith()中触发了N+1查询,导致单次广播耗时从20ms飙到8秒。
Redis连接池不够用,消费者全在等IO
当多个queue:work进程共用一个Redis连接(默认redis.connection=default),它们会排队抢同一个连接。尤其在广播高频小事件(如点赞、在线状态)时,连接争用比业务逻辑还拖后腿。
解法不是加更多worker,而是隔离资源:
- 在
config/database.php里新增一个专用Redis连接,比如叫broadcast-redis,设置'read_write_timeout' => 5; - 在
config/broadcasting.php中指定该连接:'connection' => 'broadcast-redis'; - 对应地,在
config/queue.php的redis驱动里,把connection也指向同一连接,确保广播任务和驱动使用同源Redis连接池。
注意:别在.env里复用REDIS_URL——它默认不带连接池参数,必须显式配置options => ['prefix' => 'queue:broadcast:']避免键冲突。
广播事件本身是否在做重活?
很多开发者以为“只要进了队列就安全了”,却忘了broadcastWith()和broadcastOn()是在消费者进程里同步执行的。这里任何DB查询、HTTP请求、循环拼数组,都会直接拖慢整个worker吞吐。
典型反模式:
-
broadcastWith()里写User::find($this->user_id)->load('profile'); -
broadcastOn()里动态查租户配置:Cache::get("tenant:{$this->tenant_id}:channel")但缓存未命中; - 事件构造函数没预加载,导致每次序列化都重新查一遍关联模型。
正确做法是:事件实例化时就完成所有数据准备,broadcastWith()只返回已计算好的数组。如果字段来自数据库,用with()预加载并select()精简字段;如果来自API,提前在触发事件前缓存结果,别留到队列里再拉。
最易被忽略的一点:广播事件对象被序列化时,PHP会递归遍历所有属性。若你不小心把Eloquent模型实例、Request对象甚至$this->app塞进事件属性,序列化开销会指数级上升——这比慢查询更隐蔽,也更致命。


















