[TOC] #### 1. 前言 ---- 你做了一个电商系统,用户下单之后如果 30 分钟内没付款,订单自动取消 这个需求很常见,淘宝、京东、拼多多都有,但怎么实现 ? 笨办法:定时任务扫表 最直观的思路:写一个定时任务,每隔 1 分钟扫一次订单表 ```php // 查询所有未支付且创建时间超过 30 分钟的订单 $orders = DB::table('orders') ->where('status', 'unpaid') ->where('created_at', '<', now()->subMinutes(30)) ->get(); foreach ($orders as $order) { // 取消订单,恢复库存 cancelOrder($order->id); } ``` 能用是能用,但有问题: + 延迟:最多延迟 1 分钟,用户体验不好 + 浪费:大部分时候扫出来是空的,白跑 + 数据库压力:每分钟查一次表,订单量大了扛不住 那么,有没有更好的办法 ? 用消息队列的思路,换个角度想: 用户下单的时候,往队列里丢一条消息,设好 30 分钟过期 30 分钟后,如果这条消息还没被消费(说明用户没付款),消息就过期了,过期之后,触发取消订单的逻辑 不需要定时任务,不需要扫表,RabbitMQ 会负责管理消息的过期时间 #### 2. 认识 TTL ---- 什么是 TTL ? TTL 是 Time To Live 的缩写,意思是「存活时间」 消息的 TTL 就是:这条消息在队列里最多能活多久,超过这个时间没被消费,消息就过期了 设置方式有两种: ```php // 方式一:给单条消息设置(创建消息时传入参数) $message = new AMQPMessage(json_encode($data), [ 'expiration' => '1800000', // 30分钟,单位毫秒 ]); // 方式二:给整个队列设置(声明队列时传入参数) $args = new AMQPTable([ 'x-message-ttl' => 1800000, // 队列里所有消息30分钟过期 ]); $channel->queue_declare('order_queue', false, true, false, false, false, $args); ``` 两种方式的区别: | | 单条消息设置 | 整个队列设置 | | ------------ | ------------ | ------------ | | 灵活性 | 每条消息可以不同 | 所有消息统一 | | 适用场景 | 不同订单有不同时长 | 所有订单都是 30 分钟 | 订单付款超时的实现: ```php // 用户下单逻辑 public function createOrder($userId, $goodsId) { // 创建订单(默认是待付款) $order = $this->insertOrder($userId, $goodsId); // 往队列里丢一条消息,30分钟过期 $message = new AMQPMessage(json_encode([ 'order_id' => $order->id, 'action' => 'check_payment', ]), ['expiration' => '1800000']); $channel->basic_publish($message, '', 'order_timeout_queue'); return $order; } ``` 消费者从 `order_timeout_queue` 里取消息: ```php $callback = function ($msg) { $data = json_decode($msg->body, true); $order = DB::table('orders')->find($data['order_id']); if ($order && $order->status === 'unpaid') { // 还没付款,取消订单 cancelOrder($order->id); } $msg->ack(); }; ``` 如果用户在 30 分钟内付款了,消费者收到消息后查一下订单状态,这里只是为了说明业务判断逻辑 真正的超时取消方案,还需要配合下一讲的死信队列一起实现。发现已经付了,直接 `ack` 跳过就行 但这里有个问题:消息过期之后,它去哪了 ? 答案是:直接消失了。这意味着:如果用户没付款,消息过期了,消费者根本收不到这条消息 消费者收不到消息,就没法触发取消订单的逻辑,那 TTL 有什么用 ? 别急,消息过期之后不会凭空消失,它可以被「接住」,接住它的东西,叫 “死信队列(DLX)”,下一讲我们来聊 #### 3. 本文小结 ---- TTL 就是消息的「保质期」,超过 TTL 没被消费的消息会过期 设置方式:单条消息设置 `expiration`,或者整个队列设置 `x-message-ttl` 但光有 `TTL` 还不够,消息过期之后需要一个去处,下一讲我们来聊死信队列,它是 `TTL` 的最佳搭档