![]()
去年有个订单接口让我盯着监控发了两天呆。数据库显示订单创建只用了80毫秒,但用户端到端耗时1.2秒。中间那1秒多去哪儿了?
后来翻代码发现,确认邮件、埋点上报、库存同步、缓存清理——四件事排着队等响应发完。用户其实只想知道"订单成功了",却被迫看完后台的整场马戏。
队列是正确答案,但小题大做了
常规解法是上队列(Queue)。建Job类、配Worker、搭监控、处理失败重试。这套组合拳打下来,14个Job类里有8个都是单方法裸奔,各自占一个文件、一套测试、一张失败记录表。
就像用集装箱船运一杯咖啡。能运,但船员比货重。
Laravel 11的defer()(延迟执行)给了第三种选择:响应发完再跑,不建Job、不启Worker、不碰Redis。代码长这样:
Route::post('/order', function () {
$order = Order::create($data);
defer(fn() => Mail::send(new OrderConfirmation($order)));
defer(fn() => Analytics::track('order_placed', $order));
defer(fn() => InventorySync::push($order));
defer(fn() => Cache::forget("user:{$order->user_id}:cart"));
return response()->json($order);
});
![]()
用户收到订单JSON的同时,四个闭包才刚开始执行。延迟从秒级压到毫秒级,代码量从8个类缩到4行。
但别急着把队列全拆了
我见过最离谱的踩坑,是把不可靠的Webhook塞进了defer()。目标服务10%的丢包率,失败静默,用户无感知,运营后台却永远对不上账。
这里有个粗糙但好用的分界:
用defer()的场景:发通知邮件、埋点、清缓存、写日志——失败就失败了,用户不会再来问。
必须上队列的场景:支付处理、大文件生成、批量外部API同步——失败要告警、要重试、要有人半夜爬起来看。
有个细节很多人没提:defer()的回调是在当前进程里跑的。如果四个闭包总共要跑3秒,PHP-FPM进程就被占3秒。并发高了会挤爆池子。
![]()
所以Laravel文档补了一刀:建议配合octane(长驻内存服务器)使用,或者确保回调足够轻。这不是银弹,是特定场景的手术刀。
我们最后怎么落地的
14个Job类砍到6个。8个"单方法裸奔户"进了defer(),剩下6个涉及第三方对账和资金流转的,老老实实留队列。
监控数据很有意思:P99响应时间从1.2秒掉到180毫秒,但defer()回调的平均执行时间其实没变——只是用户不用等了。服务器CPU涨了约5%,内存几乎没动,因为省了序列化Job和Redis往返的开销。
最意外的收获是代码可读性。以前看订单接口要跳去8个文件才能拼完全流程,现在4行闭包堆在路由旁边,新人10秒看懂业务全貌。
有个老同事review时吐槽:"这不就是当年register_shutdown_function(注册关闭时执行函数)的包装版?"技术上没错,但框架层的语义封装让决策成本从"查文档+写兼容+测边缘case"降到了"加个关键字"。
Laravel作者Taylor Otwell在发布说明里写得很克制:「不是所有后台任务都适合defer,但它让简单任务回归简单。」
我们的实践验证了后半句。至于前半句——那个静默失败的Webhook,现在躺在队列里,带着重试计数器和钉钉告警,每晚安稳地跑。
你的项目里有多少"单方法裸奔户"?数完可能发现,有些队列Worker根本不用养。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.