8.1 命令行、队列与定时任务


8.1 命令行、队列与定时任务

走上延伸段的第一课:不是所有工作都该在 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)绑定管理。

本节要点回顾

  • 指令即命令行的控制器:业务进服务类,Web 与命令行共用,试跑选项先行。
  • 队列两端各司其职:请求路径只投递,常驻进程做消费,任务幂等是重试的前提。
  • 定时一条总闸:调度入口全局唯一,任务定义集中审计,防重入加锁。
  • 扩展包算升级账:核心设施用官方包,周边能力选社区方案,每个包都让升级变贵一点。
  • 死信是闭环:重试之外必须设计最终失败的去向,监控口径才完整。

重活搬完了,主干提速了吗?下一节用数字回答:日志、慢查询与瓶颈定位见 8.2。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U