走上延伸段的第一课:不是所有工作都该在 HTTP 请求里干完。下单后发短信、整点跑报表、深夜清算积分——这些动作要么慢、要么不需要人等着,把它们搬进命令行世界的队列与定时调度,主干请求才能快去快回。本节承接 4.4 的纪律「慢动作出事务、进队列」,讲指令定义、队列投递消费与定时注册三件事。学完你应当能搭起「下单通知进队列、每日报表跑定时」的完整链路。
框架的命令行体系与 Web 侧同构:指令类对应控制器,input 参数对应请求对象。给书店建一个补单指令:
// app/command/OrderRepair.php namespace app\command; use think\console\Command; use think\console\Input; use think\console\input\Argument; use think\console\input\Option; use think\console\Output; class OrderRepair extends Command { protected function configure() { $this->setName('order:repair') ->addArgument('date', Argument::REQUIRED, '补算日期,格式 Y-m-d') ->addOption('dry', null, Option::VALUE_NONE, '试跑模式,只统计不改数据'); } protected function execute(Input $input, Output $output) { $date = $input->getArgument('date'); $dry = $input->getOption('dry'); $count = $dry ? 12 : $this->doRepair($date); $output->writeln("处理日期:{$date},命中订单:" . $count . ($dry ? '(试跑)' : '')); return 0; // 退出码:脚本与调度器靠它判断成败 } }
// config/console.php 注册后即可执行 return ['commands' => [\app\command\OrderRepair::class]]; // 运行:php think order:repair 2024-05-01 --dry
指令与 Web 控制器共享同一套容器与模型——业务逻辑写在服务类里,Web 与命令行两侧各自薄薄一层做适配,这是避免「补单逻辑写两遍」的关键结构。试跑选项是批处理指令的好习惯:先 --dry 看命中范围,确认无误再真跑,一行参数换来一次保险。
队列的两端各一个角色:生产端在请求路径上只做一件事——投递消息;消费端是常驻进程,逐条取出执行。以「下单成功发通知」为例:
// 生产端:控制器里只投递,不干活 use think\Queue; Queue::push(\app\job\OrderNotify::class, [ 'order_id' => $order->id, 'channel' => 'sms', ], 'order');
// 消费端:任务类 namespace app\job; class OrderNotify { public function fire($job, $data) { $order = \app\model\Order::find($data['order_id']); if ($order) { // 调用通知服务,失败抛异常触发重试 \app\service\Notify::send($order, $data['channel']); } $job->delete(); // 干完删任务;不删则等待重试 } }
两条纪律照应 4.4 的原则:任务要幂等——重试机制意味着同一条通知可能发两次,任务体内先查「是否已发」再做动作;驱动看场景——本机开发用 sync(同步执行便于调试),生产用 Redis 驱动,任务量极大或需要跨机房时再考虑专用消息中间件,起步阶段驱动越朴素越好排障。
定时调度沿用系统级方案:框架提供任务定义,触发交给 crontab。项目里定义任务表,生产机只挂一条每分钟触发的总闸:
// app/cron.php(示意):任务定义 use think\facade\Db; return [ // 每天凌晨两点的积分清算 ['cron' => '0 2 * * *', 'task' => 'order:clearing'], // 工作日每小时的畅销榜重算 ['cron' => '0 9-18 * * 1-5', 'task' => 'report:hotlist'], // 每五分钟扫一次超时未支付订单 ['cron' => '*/5 * * * *', 'task' => 'order:cancel-expired'], ];
# 服务器上只挂这一条 crontab,每分钟拉起调度器 * * * * * php /www/shop/think schedule:run >> /dev/null 2>&1
三个工程要点:任务必须可重复触发而不出错(上一轮还没跑完下一轮又启动是最常见的脏数据来源,给任务加运行锁或用队列串行化);表达式集中在一处定义便于审计,散落多台机器的 crontab 是排障噩梦;定时任务也走队列时,注意与 Web 侧共用驱动与失败重试策略,监控口径才能统一。
延伸段的生态位最后补一句扩展包的判断法:队列、定时这类核心设施优先用框架官方包(兼容性与文档最稳);第三方包引入前查三样——维护活跃度、框架版本兼容声明、卸载成本。原则一句话:核心设施宁缺勿杂,周边能力(支付、短信、验证码)选社区成熟方案。每个新 Composer 包都让下一次框架升级变贵一点,装之前把它对升级成本的贡献估进去。
背景:练习项目的下单接口同步发短信,压测时响应时间被通知拖长。操作:把通知改造成队列任务并幂等化;配置两条定时任务(超时订单取消、每日报表);试跑补单指令的 dry 模式;用队列监控命令观察任务从投递到出队的完整生命周期。结果示例:下单接口耗时下降到只剩业务写入的时间,短信在数秒后陆续发出;定时任务按表触发且日志可查。解读:注意失败路径——手动制造一次通知服务故障,观察任务重试与最终入死信(或标记失败)的过程,重试不是无限兜底,死信处理才是闭环。变式:给队列换 Redis 驱动再跑一遍全流程,记录两种驱动在排障手感上的差异。
⚠️ 常见坑:本地开发忘了把队列驱动从 sync 换回生产配置,或反过来——sync 驱动在请求内同步执行任务,生产高峰等于没有队列。驱动选择必须进环境变量,与环境隔离(8.3)绑定管理。
重活搬完了,主干提速了吗?下一节用数字回答:日志、慢查询与瓶颈定位见 8.2。