不能直接用sleep()模拟订单超时,因为PHP同步阻塞会导致请求卡死、浪费资源;应将超时检查剥离为独立后台任务,下单时存绝对截止时间pay_deadline,查询时用WHERE pay_deadline

为什么不能直接用 sleep() 模拟订单超时
因为 PHP 是同步阻塞执行的,sleep(300) 会让整个请求卡住 5 分钟,既浪费 FPM 进程,又无法响应用户其他操作。生产环境里,一个未支付订单可能要等 15–30 分钟,绝不能靠单次请求扛着等。
真正可行的路径只有一条:把「检查是否超时」这件事从下单请求中剥离,交给独立、可伸缩的后台任务来处理。
- 下单成功后,只往数据库写入订单,并记录
pay_deadline(比如time() + 1800) - 不启动任何定时器或线程,也不依赖前端轮询
- 后续由另一个进程(如 Laravel 的
queue:work或自建守护脚本)持续扫描待处理订单
用数据库时间戳字段做延迟判断最稳妥
别信 created_at 加固定秒数去比对——如果服务器时间被校准、NTP 同步抖动,或者 MySQL 时区配置不一致,NOW() - INTERVAL 30 MINUTE 可能漏掉或误杀订单。
正确做法是:下单时就存好绝对截止时间,查询时用 WHERE pay_deadline (注意是 <code>UNIX_TIMESTAMP(),不是 NOW()),确保 PHP 和 MySQL 都基于同一套时间基线。
- 字段类型用
INT(10)存时间戳,比DATETIME更轻量、无时区歧义 - 加联合索引:
INDEX idx_status_deadline (status, pay_deadline),加速「未支付且已超时」的筛选 - 避免在 WHERE 中对字段做函数运算,例如
WHERE UNIX_TIMESTAMP(pay_deadline) 会失效索引
Laravel 队列 + 数据库驱动实现零外部依赖
如果你用 Laravel,不用装 Redis 或 Beanstalkd 也能跑延迟队列——数据库驱动本身支持 available_at 字段控制投递时间。
关键不在「怎么延时」,而在「谁来触发」:你得有一个常驻进程定期拉取到期任务,而不是等用户下单那一刻就调度。
- 下单后不 dispatch,而是先插入一条待处理记录到
delayed_jobs表(或复用jobs表),设置available_at = time() + 1800 - 运行
php artisan queue:work --sleep=5 --max-jobs=100,它会每 5 秒查一次available_at 的 job - job 执行体里先
SELECT ... FOR UPDATE锁住订单,再查一遍状态和时间戳,双重校验防止重复取消
示例核心逻辑:
public function handle()
{
$order = Order::where('id', $this->orderId)
->where('status', Order::STATUS_UNPAID)
->where('pay_deadline', 'lockForUpdate()
->first();
<pre class="brush:php;toolbar:false;">if ($order) {
$order->status = Order::STATUS_CANCELLED;
$order->cancel_reason = 'timeout';
$order->save();
}}
手动轮询的性能临界点与降级方案
当订单量上万/天,每 5 秒全表扫 pay_deadline 会拖慢主库。这不是设计问题,是规模到了必须拆解。
两个低成本优化方向:
- 把超时检查从「所有未支付订单」缩小到「最近 2 小时创建的订单」,加
created_at > DATE_SUB(NOW(), INTERVAL 2 HOUR)条件,配合created_at索引 - 用 MySQL 的
EVENT定时触发存储过程更新状态(仅限超时动作简单、不要求强事务的场景),但注意EVENT不保证精确到秒,且无法回调业务逻辑
真正难的是并发取消:多个 worker 同时捞到同一批订单怎么办?唯一可靠解法是加 SELECT ... FOR UPDATE 或用 UPDATE ... WHERE status = 'unpaid' AND pay_deadline 返回影响行数做判断——没影响到行,说明已被别人处理过。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











