場景
在業務中有時會碰到延遲操作,如下單後半小時未支付則取消訂單、下單後十五分鐘未支付則發簡訊提醒等等。那這樣的需求如何去實現呢。
實現方式
- 第一個簡單的方式就是用一個後臺程式死迴圈去查訂單,根據下單時間去做不同的操作
- 第二種就是使用訊息佇列的定時訊息,下單之後傳送定時訊息,不同的定時佇列去處理不同的邏輯
- 第三種可以使用框架提供的一些既有功能去做
實現程式碼
我們以訂單建立15分鐘後未支付,給使用者傳送郵件為場景進行學習
準備工作:
- 簡單的訂單表:order
- 各種需要的composer包
- rabbitMq本地服務
- 開通阿里雲RocketMq服務
第一種
- 程式碼邏輯很簡單就直接死迴圈就行了
- 啟動這個指令碼程式,可以用supervisor配置
- 部分程式碼
//建立訂單的邏輯
/**
* 隨機建立訂單
*/
$order = [
'order_number' => mt_rand(100,10000).date("YmdHis"),
'user_id' => mt_rand(1, 100),
'order_amount' => mt_rand(100, 1000),
];
/**@var $manager Illuminate\Database\Capsule\Manager **/
$conn = $manager;
$insertResult = $conn::table("order")
->insert($order);
print_r($insertResult);
延遲處理邏輯
while(true) {
// 未支付訂單列表
$orderList = $conn::table("order")
->where("created_time", '<=', date("Y-m-d H:i:s", strtotime("-15 minutes")))
->where('sended_need_pay_notify', '=', 2)
->where('status', '=', 1)
->select(['user_id', 'id'])
->orderBy("id", 'asc')
->get();
$orderList = json_decode(json_encode($orderList), true);
foreach ($orderList as $orderInfo) {
sendEmail($orderInfo['user_id']);
$conn::table('order')
->where('id', '=', $orderInfo['id'])
->update(['sended_need_pay_notify' => 1]);
logs("update-success-orderId-". $orderInfo['id']."-userId-".$orderInfo['user_id']);
}
sleep(10);
}
執行處理指令碼
gaoz@nobodyMBP delay_mq_demo % php first_while_handler.php
send email to 73 success ...
2020-06-24 11:37:36:update-success-orderId-3-userId-73
這種方式吧實現簡單,但是不優雅,同時大批量訂單產生也會遇到問題。
第二種
- 比如使用阿里雲的MQ服務,目前rocketMq與rabbitMq版本支援延遲訊息,但是rabbit的延時訊息收費太高了
- 這裡先使用rocketMq的延遲訊息去實現
- 需要開通阿里雲的服務
// 建立訂單的邏輯
try
{
/**
* 隨機建立訂單
*/
$order = [
'order_number' => mt_rand(100,10000).date("YmdHis"),
'user_id' => mt_rand(1, 100),
'order_amount' => mt_rand(100, 1000),
];
/**@var $manager Illuminate\Database\Capsule\Manager **/
$conn = $manager;
$insertId = $conn::table("order")
->insertGetId($order);
$body = json_encode(['order_id' => $insertId, 'created_time' => date("Y-m-d H:i:s")]);
$publishMessage = new TopicMessage(
$body
);
// 設定訊息KEY
$publishMessage->setMessageKey("MessageKey");
// 定時訊息, 定時時間為3分鐘後
$publishMessage->setStartDeliverTime(time() * 1000 + 3 * 60 * 1000);
$result = $this->producer->publishMessage($publishMessage);
print "Send mq message success. msgId is:" . $result->getMessageId() . ", bodyMD5 is:" . $result->getMessageBodyMD5() . "\n";
} catch (\Exception $e) {
print_r($e->getMessage() . "\n");
}
消費邏輯 同樣是在消費者中處理
foreach ($messages as $message) {
$receiptHandles[] = $message->getReceiptHandle();
$messageBody = $message->getMessageBody();
$orderInfo = json_decode($messageBody, true);
if (!empty($orderInfo['order_id'])) {
$orderId = $orderInfo['order_id'];
/**@var $manager Illuminate\Database\Capsule\Manager * */
$conn = $manager;
$orderInfo = $conn::table("order")
->select(['id', 'user_id'])
->where('id', '=', $orderId)
->where('status', '=', 1)
->first();
if (!empty($orderInfo)) {
$orderInfo = json_decode(json_encode($orderInfo), true);
sendEmail($orderInfo['user_id']);
$conn::table('order')
->where('id', '=', $orderInfo['id'])
->update(['sended_need_pay_notify' => 1]);
logs("update-success-orderId-" . $orderInfo['id'] . "-userId-" . $orderInfo['user_id']);
}
}
}
啟動生產一條訊息
gaoz@nobodyMBP delay_mq_demo % php rocket_mq_handler_producer.php
Send mq message success. msgId is:76CF2135696C3D4EAC698A9FA1E1879D, bodyMD5 is:63448B50AA7B8AF47B07AA7CE807E3D3
gaoz@nobodyMBP delay_mq_demo %
啟動消費者慢慢等待
gaoz@nobodyMBP delay_mq_demo % php rocket_mq_handler_consumer.php
No message, contine long polling!RequestId:5EF752583441411C74869BA9
No message, contine long polling!RequestId:5EF7525B3441411C74869FE2
No message, contine long polling!RequestId:5EF7525E3441411C7486A42C
No message, contine long polling!RequestId:5EF752613441411C7486A7D9
consume finish, messages:
send email to 95 success ...
2020-06-27 12:08:05:update-success-orderId-8-userId-95
Array
(
[0] => 76CF2135696C3D4EAC698A9FA1E1879D-MCAxNTkzMjY2NzkxNDM5IDMwMDAwMCAzIDAgYmpzaGFyZTUtMDggNSAw
)
ack
這種方式有現有的服務可以使用,減少開發時間
第三種 使用rabbitMq去實現
- 查閱文件沒有找到rabbitMq支援延遲佇列的原生功能,但是可以通過訊息的ttl+死信佇列實現
- 私信佇列就是用來存放沒有被消費或者消費失敗等訊息的佇列
- 當設定訊息的有效期內沒有被消費訊息就會被轉發到死信佇列
- 通過設定訊息的有效期實現延時功能
// 生產者
$exchange = 'order15min_notify_exchange';
$queue = 'order15minx_notify_queue';
$dlxExchange = "dlx_order15min_exchange";
$dlxQueue = "dlx_order15min_queue";
$connection = new AMQPStreamConnection(getenv('RABBIT_HOST'), getenv('RABBIT_PORT'), getenv("RABBIT_USER"), getenv("RABBIT_PASS"), getenv("RABBIT_VHOST"));
$channel = $connection->channel();
$channel->exchange_declare($exchange, AMQPExchangeType::DIRECT, false, true, false);
$channel->exchange_declare($dlxExchange, AMQPExchangeType::DIRECT, false, true, false);
// 設定佇列的過期時間
// 正常佇列
$table = new \PhpAmqpLib\Wire\AMQPTable();
// 訊息有效期
$table->set('x-message-ttl', 3*60*1000);
$table->set("x-dead-letter-exchange", $dlxExchange);
$channel->queue_declare($queue, false, true, false, false, false, $table);
$channel->queue_bind($queue, $exchange);
// 死信佇列
$channel->queue_declare($dlxQueue, false, true, false, false, false);
$channel->queue_bind($dlxQueue, $dlxExchange);
/**
* 隨機建立訂單
*/
$order = [
'order_number' => mt_rand(100,10000).date("YmdHis"),
'user_id' => mt_rand(1, 100),
'order_amount' => mt_rand(100, 1000),
];
/**@var $manager Illuminate\Database\Capsule\Manager **/
$conn = $manager;
$insertId = $conn::table("order")
->insertGetId($order);
$messageBody = json_encode(['order_id' => $insertId, 'created_time' => date("Y-m-d H:i:s")]);
$message = new AMQPMessage($messageBody, array('content_type' => 'text/plain', 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT));
$channel->basic_publish($message, $exchange);
消費者
$dlxExchange = "dlx_order15min_exchange";
$dlxQueue = "dlx_order15min_queue";
$connection = new AMQPStreamConnection(getenv('RABBIT_HOST'), getenv('RABBIT_PORT'), getenv("RABBIT_USER"), getenv("RABBIT_PASS"), getenv("RABBIT_VHOST"));
$channel = $connection->channel();
$channel->queue_declare($dlxQueue, false, true, false, false);
$channel->exchange_declare($dlxExchange, AMQPExchangeType::DIRECT, false, true, false);
$channel->queue_bind($dlxQueue, $dlxExchange);
/**
* @param \PhpAmqpLib\Message\AMQPMessage $message
*/
function process_message($message)
{
echo "\n--------\n";
echo $message->body;
echo "\n--------\n";
$orderInfo = json_decode($message->body, true);
if (!empty($orderInfo['order_id'])) {
$orderId = $orderInfo['order_id'];
/**@var $conn Illuminate\Database\Capsule\Manager * */
$conn = getdb();
$orderInfo = $conn::table("order")
->select(['id', 'user_id'])
->where('id', '=', $orderId)
->where('status', '=', 1)
->first();
if (!empty($orderInfo)) {
$orderInfo = json_decode(json_encode($orderInfo), true);
sendEmail($orderInfo['user_id']);
$conn::table('order')
->where('id', '=', $orderInfo['id'])
->update(['sended_need_pay_notify' => 1]);
logs("update-success-orderId-" . $orderInfo['id'] . "-userId-" . $orderInfo['user_id']);
}
}
$message->delivery_info['channel']->basic_ack(
$message->delivery_info['delivery_tag']);
}
$channel->basic_consume($dlxQueue, $consumerTag, false, false, false, false, 'process_message');
啟動消費者
gaoz@nobodyMBP delay_mq_demo % php rabbit_mq_handler_consumer.php
--------
{"order_id":7,"created_time":"2020-06-27 11:50:08"}
--------
send email to 2 success ...
2020-06-27 11:56:55:update-success-orderId-7-userId-2
分別啟動消費者、生產者就可以了,這裡面訊息的流轉可以看到
訊息先進入到正常佇列,過期後進入了死信佇列而被消費
第四種
- 使用laravel自帶的Queue去實現
- 這裡沒有整理詳細程式碼,後面更新出來
- 可以檢視官方文件 佇列《Laravel 5.7 中文文件》
程式碼示例:github.com/nobody05/delay_mq_demo
參考資料
- RocketMQ:help.aliyun.com/product/29530.html...
- RabbitMq實戰
本作品採用《CC 協議》,轉載必須註明作者和本文連結