本文目录导读:

在PHP项目中实现订单超时处理,通常有几种主流方案,各有优劣,我会从简单到复杂、从被动到主动为你介绍。
核心思路
订单超时处理的核心是:在订单创建时记录一个“过期时间”,然后在该时间点触发“取消订单、释放库存”等操作。
常见实现方案
方案1:用户请求时被动检查(最简单,适合低并发)
在用户查看订单、支付、或访问任何相关页面时,检查订单是否超时。
public function checkOrder($orderId) {
$order = Order::find($orderId);
if ($order && $order->status == 'pending') {
if (time() > strtotime($order->created_at) + 1800) { // 30分钟
// 取消订单
$order->status = 'cancelled';
$order->cancel_reason = '超时自动取消';
$order->save();
// 释放库存
$this->releaseStock($order);
}
}
}
优点: 实现简单,无需额外组件 缺点: 依赖用户触发,如果用户不访问,订单永远不会被取消
方案2:定时任务轮询(适合中小型项目)
使用Linux Crontab或Windows计划任务,定期扫描超时订单。
// cancel_expired_orders.php
// 每1分钟执行一次
$expireTime = date('Y-m-d H:i:s', time() - 1800); // 30分钟前
$expiredOrders = Order::where('status', 'pending')
->where('created_at', '<=', $expireTime)
->get();
foreach ($expiredOrders as $order) {
DB::beginTransaction();
try {
$order->status = 'cancelled';
$order->save();
$this->releaseStock($order);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
Log::error('订单超时处理失败: ' . $order->id . ' - ' . $e->getMessage());
}
}
Crontab配置:
* * * * * php /path/to/cancel_expired_orders.php
优点: 简单可靠,易于理解和维护 缺点: 有1分钟左右的延迟,大量订单时轮询效率低
方案3:定时任务 + 批量优化(推荐中小项目)
改进轮询策略,按ID范围分批处理。
public function batchCancel() {
$lastId = 0;
$batchSize = 100;
while (true) {
$expiredOrders = Order::where('status', 'pending')
->where('id', '>', $lastId)
->where('created_at', '<=', date('Y-m-d H:i:s', time() - 1800))
->orderBy('id')
->limit($batchSize)
->get();
if ($expiredOrders->isEmpty()) {
break;
}
foreach ($expiredOrders as $order) {
// 处理逻辑...
$lastId = $order->id;
}
}
}
方案4:RabbitMQ/Redis延迟队列(推荐高并发项目)
利用消息队列的延迟投递功能。
使用Redis Zset实现延迟队列
class OrderDelayQueue {
private $redis;
private $queueKey = 'order:delay';
public function addOrder($orderId, $delaySeconds = 1800) {
$this->redis->zadd(
$this->queueKey,
time() + $delaySeconds,
$orderId
);
}
public function processExpiredOrders() {
// 获取当前时间之前的所有订单
$expiredOrders = $this->redis->zrangebyscore(
$this->queueKey,
0,
time()
);
foreach ($expiredOrders as $orderId) {
// 尝试移除(防止重复处理)
$removed = $this->redis->zrem($this->queueKey, $orderId);
if ($removed) {
$this->cancelOrder($orderId);
}
}
}
}
使用RabbitMQ延迟队列(推荐):
// 创建订单时,发送延迟消息
$message = new AMQPMessage(json_encode(['order_id' => $orderId]));
$channel->basic_publish($message, 'delayed_exchange', 'order.delay', [
'headers' => ['x-delay' => 1800000] // 30分钟,单位毫秒
]);
// 消费者处理
$callback = function($msg) {
$data = json_decode($msg->body, true);
$this->cancelOrder($data['order_id']);
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};
方案5:PHP脚本常驻内存(适合需要实时性的场景)
使用Workerman或Swoole实现常驻进程,推荐使用MeEdu课程项目中的超级好用方案:
// 使用Swoole的Timer
$server = new Swoole\Server('0.0.0.0', 9501);
// 每1秒检查一次
$server->tick(1000, function() {
// 检查Redis延迟队列
$redis = new Redis();
// ... 处理逻辑
});
$server->start();
实际项目推荐方案
中小型项目(日订单<1000):方案2(Crontab轮询)
// 优化版轮询
class OrderCancelService {
public function process() {
$page = 1;
$size = 50;
do {
$orders = Order::where('status', 'pending')
->where('expire_time', '<=', now())
->forPage($page, $size)
->get();
foreach ($orders as $order) {
// 使用乐观锁避免并发
$affected = Order::where('id', $order->id)
->where('status', 'pending')
->update(['status' => 'cancelled']);
if ($affected) {
// 释放库存、发送通知等
event(new OrderCancelled($order));
}
}
$page++;
} while ($orders->count() >= $size);
}
}
大型项目:方案4(RabbitMQ延迟队列)+ 方案2作为兜底
双重保障:
- 主流程使用消息队列实时处理
- 定时任务作为兜底,处理MQ可能漏掉的订单
性能优化技巧
-
索引优化
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at); -- 或使用expire_time字段 ALTER TABLE orders ADD INDEX idx_expire_time (expire_time);
-
分批处理,避免长事务
-
使用乐观锁避免重复处理
Order::where('id', $orderId) ->where('status', 'pending') ->update(['status' => 'cancelled']); // 检查affected_rows -
记录处理日志,方便追踪
注意事项
- 事务处理:取消订单和释放库存要放在一个事务中
- 幂等性:确保重复执行不会产生副作用
- 监控告警:对处理失败的订单进行记录和告警
- 补偿机制:对于特殊订单(如已支付但未发货),需要人工干预
- 新手/小项目:用户请求时检查 + Crontab轮询
- 中级项目:优化版Crontab轮询 + Redis Zset
- 高并发项目:RabbitMQ延迟队列 + Crontab兜底
选择哪种方案取决于你的项目规模和性能要求,对于大多数PHP项目,Crontab轮询 + 索引优化 已经能够很好地解决问题。