ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LikeShop 定时任务与队列:秒杀、订单超时关闭等异步场景实现

LikeShop 定时任务与队列:秒杀、订单超时关闭等异步场景实现 一、前言在之前的系列文章中我写了 LikeShop 的分层架构、支付模块、数据库设计以及营销模块的优惠券、积分、分销。这一篇进入异步场景——定时任务与消息队列。为什么单独讲这个因为电商系统里最耗性能的操作往往不是用户能看到的那些。秒杀抢购、订单超时关闭、退款状态扫描、佣金自动结算——这些操作如果全部同步执行系统根本扛不住。LikeShop 用定时任务和消息队列两套机制把这些“不该让用户等待”的操作从主链路中剥离出来。这篇文章就从源码出发把这两套机制的实现思路拆开讲清楚。二、LikeShop 定时任务的整体设计入口一行命令跑起所有定时逻辑LikeShop 的定时任务入口非常简洁——项目根目录下的think文件通过命令行调用crontab命令php thinkcrontab这行命令是 LikeShop 所有定时任务的统一入口。它对应的是server/app/common/command/Crontab.php这个命令行类。服务器端的配置方式在服务器上需要把这条命令加入 Crontab 计划任务设置为每分钟执行一次*/1 * * * * php /www/wwwroot/likeshop/server/thinkcrontab其中/www/wwwroot/likeshop/server/think是项目的实际绝对路径。宝塔面板提供了更友好的配置方式。在「计划任务」中新建任务有两种方式可选方式一访问 URL推荐 2022 年 5 月后的版本使用任务类型选择「访问 URL」URL 填写https://你的域名/crontab执行周期设为「N 分钟 1 分钟」这个 URL 对应的路由定义在server/route/route.php中// 定时任务Route::rule(crontab,function(){think\Console::call(crontab);});这行代码的意思是当访问/crontab时通过think\Console::call()调用命令行中的crontab命令实现和命令行方式完全相同的效果。方式二Shell 脚本任务类型选择「Shell 脚本」命令填写php72 /www/wwwroot/likeshop/server/think crontab定时任务管理后台可配置LikeShop 的一个特色是定时任务可以在管理后台配置和开关。设置路径是「系统设置 → 系统维护 → 定时任务」。管理后台提供了完整的定时任务 CRUD 接口包括添加任务/crontab.crontab/add、操作任务/crontab.crontab/operate等。每个任务可以单独设置开启或停止状态必须为开启时才会真正执行。系统还支持记录定时任务运行日志方便排查任务执行情况。Crontab 命令内部的调度逻辑Crontab.php的核心逻辑是每分钟被触发一次然后检查哪些子任务到了执行时间。每个子任务在数据库中有自己的 cron 表达式和执行状态。Crontab 命令每次执行时会遍历所有已开启的任务判断当前时间是否匹配任务的 cron 表达式。如果匹配就执行对应的任务逻辑并更新最后执行时间。这种设计的好处是你不需要为每个子任务单独配置一条服务器 Crontab。所有任务共用一条php think crontab命令由 PHP 层面的调度器统一管理。新增任务时只需要在数据库中添加记录不需要登录服务器改配置。LikeShop 的定时任务主要处理以下几类业务订单退款扫描定时检查退款状态的订单推进退款流程关闭超时未支付订单扫描超过支付时限仍未付款的订单自动关闭佣金自动结算检查满足结算条件的订单执行佣金发放活动自动开启/结束检查营销活动的开始和结束时间三、消息队列异步削峰的核心机制ThinkPHP Queue 的引入LikeShop 基于ThinkPHP Queue组件实现异步任务。这是 ThinkPHP 官方提供的消息队列服务支持 sync同步和 redis异步两种驱动模式。当驱动类型为redis时Queue::later()和Queue::push()方法会将任务推入 Redis 队列由独立的消费者进程异步执行。任务的定义与发布一个队列任务由两部分组成任务类和发布调用。任务类需要继承BaseJob并实现具体的消费逻辑classOrderTimeoutJobextendsBaseJob{publicfunctionfire($data){// 处理订单超时逻辑// 返回 true 表示消费完成// 返回 false 表示消费失败默认3次失败后删除任务returntrue;}}发布任务时有两种模式——立即执行和延时执行// 立即执行Queue::instance()-do(fire)-job(OrderTimeoutJob::class)-data($data)-push();// 延时执行N 秒后执行Queue::instance()-do(fire)-job(OrderTimeoutJob::class)-data($data)-secs(30)-push();消费者进程的启动队列任务需要有消费者进程来执行。LikeShop 通过以下命令启动消费者php think queue:listen# 或者php think queue:work在生产环境中建议配合 Supervisor 使用保证消费者进程常驻。在宝塔面板中可以安装 Supervisor添加守护进程启动命令为php think queue:listen进程数量建议设置为 1。队列解决的问题队列在 LikeShop 中主要解决三类问题第一异步化非核心操作。下单成功后发送短信通知、记录用户行为日志、更新统计数据等操作不需要用户等待全部推入队列异步执行。第二削峰填谷。秒杀场景下瞬时涌入的订单创建请求如果全部同步写数据库数据库会直接崩溃。队列把这些请求暂时“缓冲”起来由消费者按自己的节奏处理。第三延时任务。订单超时关闭、优惠券到期提醒、自动收货确认等场景都需要“N 分钟后执行某个操作”这正是延时队列的用武之地。四、秒杀场景的异步实现秒杀的技术挑战秒杀是电商系统中最典型的高并发场景。核心挑战有三个库存不能超卖、系统不能被瞬时流量打穿、用户体验不能太差。LikeShop 的解决方案是Redis 预减库存 异步队列的组合。核心流程秒杀下单的流程大致如下用户点击秒杀 ↓ ① Redis 判断库存是否充足原子 DECR 操作 ↓ 库存不足 → 直接返回“已售罄” ↓ 库存充足 → 继续 ② 生成订单推入异步队列 ↓ ③ 立即返回“排队中”给用户 ↓ 异步消费者从队列取出任务 ↓ ④ 写入数据库扣减真实库存 ↓ ⑤ 更新订单状态第一步Redis 预减库存是整个流程的关键。利用 Redis 的DECR原子操作在内存中快速判断库存是否充足。如果DECR后返回的值小于 0说明库存已耗尽直接回滚 Redis 操作并返回失败。第二步推入队列将写操作异步化。真正的订单落库、库存扣减等数据库操作交给消费者异步执行前端用户不需要等待数据库写入完成。为什么这样做传统方案中秒杀请求直接在数据库层面扣减库存。当并发量达到数千 QPS 时数据库的行锁竞争会变得极其严重——多个请求同时竞争同一行库存记录后面的请求全部排队等待响应时间急剧上升。Redis 预减库存把竞争转移到了内存层面Redis 的单线程模型天然保证了操作的原子性。数据库只负责最终的数据持久化不再是性能瓶颈。代价是引入了延迟和数据一致性的挑战。Redis 中的库存值和数据库中的库存值可能存在短暂的不一致。LikeShop 通过异步消费者最终将 Redis 的库存变更同步到数据库保证最终一致性。五、订单超时关闭的实现业务需求用户下单后如果没有在规定时间内完成支付订单需要自动关闭释放占用的库存。这个时间通常由后台的“交易设置”配置决定——如果 C 端没有取消订单按钮需要检查“允许取消订单时长”的设置。实现方式定时任务扫描订单超时关闭是 LikeShop 定时任务的经典应用场景。Crontab 命令每分钟执行一次每次执行时扫描ls_order表找出满足以下条件的订单order_status为待付款状态0create_time距离当前时间已超过配置的支付时限订单未被取消或关闭找到超时订单后执行以下操作将订单状态更新为“已关闭”释放占用的库存如果库存是预占模式如果使用了优惠券返还优惠券如果使用了积分返还积分写入订单日志ls_order_log与状态机的配合订单超时关闭涉及的是一次状态迁移CREATED已创建→ TIMEOUT已超时。在之前写支付模块时我提到过LikeShop 把订单建模为有限状态机任何状态变更都必须经过合法性校验。CREATED 状态可以迁移到 TIMEOUT这是状态机中明确定义的合法路径。定时任务在关闭订单时调用的应该是OrderLogic中定义的状态迁移方法而不是直接update数据库。一个需要注意的坑订单超时关闭和支付回调之间存在竞争条件。假设订单在超时关闭的瞬间用户刚好完成了支付——如果两个操作同时执行可能出现“订单已关闭但支付成功”的状态冲突。LikeShop 的应对策略是在状态迁移时做乐观锁校验——更新订单状态时带上当前状态的判断条件只有状态仍然符合预期时才执行更新。这样即使出现并发也只有一个操作能成功。六、二开实战新增一个定时任务理解了定时任务的机制后新增一个任务就很简单了。假设业务需要在每天凌晨 3 点清理过期优惠券。第一步在server/app/common/command/Crontab.php中新增任务逻辑或者在管理后台通过定时任务配置界面添加新任务。第二步在server/app/common/logic/下新增处理逻辑classCouponLogic{publicstaticfunctionclearExpired(){// 找出所有已过期的未使用优惠券$nowtime();$expiredListCouponListModel::where(status,0)-where(expire_time,,$now)-select();foreach($expiredListas$item){// 批量更新状态为已过期CouponListModel::where(id,$item[id])-update([status2,update_time$now]);}}}第三步在管理后台的定时任务配置中设置 cron 表达式为0 3 * * *每天凌晨 3 点并开启任务。需要注意的是批量操作要控制每次处理的数据量。如果过期优惠券数量很大一次全量扫描会拖慢数据库。建议加limit分页处理。七、二开实战新增一个队列任务假设业务需要在下单成功后异步发送短信通知。第一步创建任务类server/app/job/OrderNotifyJob.phpclassOrderNotifyJobextendsBaseJob{publicfunctionfire($data){$orderId$data[order_id];// 查询订单和用户信息发送短信// 返回 true 表示消费成功returntrue;}}第二步在下单成功的逻辑中发布任务// OrderLogic 中下单成功后Queue::instance()-do(fire)-job(OrderNotifyJob::class)-data([order_id$orderId])-push();第三步确保消费者进程正在运行php think queue:listen。八、二开避坑指南坑一定时任务路径写错。Crontab 配置中的think文件路径必须是绝对路径很多人用相对路径导致任务无法执行。坑二队列消费者进程没有常驻。用php think queue:listen手动启动的消费者在终端关闭后就会停止。生产环境必须用 Supervisor 守护进程。坑三定时任务改了但没生效。如果通过宝塔配置了定时任务修改后需要重新保存并确认执行周期设置正确。2022 年 5 月前的版本建议用 Shell 脚本方式而不是 URL 方式。坑四秒杀场景没有做限流。即使有 Redis 预减库存如果请求量远超系统处理能力Redis 本身也可能成为瓶颈。建议在 Nginx 层做 IP 级别的连接数限制。坑五队列任务失败没有告警。默认情况下队列任务失败 3 次后会被删除但没有任何通知。建议在 BaseJob 中增加失败回调或者在消费者进程中监听失败事件。九、总结LikeShop 的异步机制由两条线组成定时任务解决的是“周期性执行”的问题。通过一条php think crontab命令统一调度每分钟检查一次执行到期的子任务。订单超时关闭、退款扫描、佣金结算都靠它。消息队列解决的是“异步削峰”的问题。秒杀下单、短信通知等场景将非核心操作从主链路剥离由消费者进程异步处理。Redis 作为队列存储Supervisor 保证消费者常驻。两条线配合构成了 LikeShop 在高并发场景下的异步处理底座。本文基于 LikeShop 单商户版 v3.5.1 源码及官方开发文档整理不同版本的命令路径和配置方式可能略有差异请以实际源码为准。
返回列表