對應的步驟
看看代碼實現流程,支付成功後,首先是pay網關收到支付成功的消息,調用rpc接口,預發送消息,更新訂單狀態,存儲發送消息,發送消息到中間件,由中間件把消息傳遞給消費端的監聽隊列,最終增加用戶的積分,更新訂單狀態和增加積分雖然不是同時進行,但是最終的結果是一致的,就是分佈式事務的最終一致性
存儲預發送消息(主動方應用系統)
先看看pay網關的order函數,預發送消息,然後通過rpc調用messge微服務的prepareMsg方法預存儲消息,然後rpc調用order微服務的update方法更新訂單狀態,如果成功就rpc調用messge微服務的confirmTosend方法確認併發送消息到消息中間件
public function order()
{
//預發送消息(消息狀態子系統)
$msg_id=session_create_id(md5(uniqid()));
$data=[
'msg_id'=>uniqid(),
'version'=>1,
'create_time'=>time(),
'message_body'=>['order_id'=>12133,'shop_id'=>2],
'notify_url'=>'http://127.0.0.1:9804/notify/index',//通知地址
'notify_rule'=>[1=>5,2=>10,3=>15],//單位爲秒
'notify_retries_number'=>0, //重試次數,
'default_delay_time'=>1,//毫秒爲單位
'status'=>1, //消息狀態
];
// var_dump($this->notifyService->publish($data));
// return ['1'];
$prepareMsgData=[
'msg_id'=>$msg_id,
'version'=>1,
'create_time'=>time(),
'message_body'=>['order_id'=>12133,'shop_id'=>2],
'consumer_queue'=>'order', //消費隊列(消費者)
'message_retries_number'=>0, //重試次數,
'status'=>1, //消息狀態
];
//預存儲消息
$result = $this->messageService->prepareMsg($prepareMsgData);
$data=[
'order_id'=>1,
'msg_id'=>$msg_id
];
//調用訂單服務更新狀態
if ($result['status'] == 1) { //消息恢復子系統(查詢未確認消息) 確認並且投遞
$this->orderService->update($data)['status']==1 && $this->messageService->confirmMsgToSend($msg_id,1);//更新訂單
}
//確認並且投遞消息
return [$result];
}
查詢消費確認超時的消息(消息恢復子系統)
然後在message微服務設置一個監聽事件,用做恢復消息子系統,定義一個定時器,查詢超時未確認的任務,消息狀態爲2表示已經投遞給消息中間件,但是沒有被消費,判斷一下這條消息的投遞次數,如果超過兩次就不用讓它繼續投遞了,因爲如果消息一直失敗,會對服務器造成很大壓力,把這條消息從redis刪掉,存到某個隊列當中,可以人工手動進行恢復,如果消息沒有超過投遞次數,就繼續投遞消息;如果消息的狀態爲1,表示已經進入消息子系統但是未投遞的,這時候確認訂單狀態,如果訂單狀態更新了,就繼續投遞消息,如果訂單狀態沒有更新成功,直接刪除這條消息
public function handle(EventInterface $event): void
{
//多進程自己實現下
$time=1; //超時任務
swoole_timer_tick(10000,function ()use($time){
sgo(function()use($time){
//自動初始化一個Context上下文對象(協程環境下)
$context = ServiceContext::new();
Context::set($context);
try{
//查詢超時未確認的任務
$service=$this->connection->zRangeByScore('message_system_time', "-inf", (string)(time()-$time));
var_dump(conunt($service));
foreach ($service as $v){
$data=$this->connection->hget('message_system',(string)$v);
if(!empty($data)){
$data=json_decode($data,true);
if($data['status']==2){ //狀態爲2代表的是已投遞,超時沒有被正確消費(消息恢復系統)
//嘗試重新投
//如果投遞的次數超過最大值,刪除任務,並且存到單獨存到redis一個隊列當中
if($data['message_retries_number']>=2){
//var_dump($data['message_retries_number'],"投遞失敗,手動重試");
//可以封裝成服務
$this->connection->transaction(function (\Redis $redis) use ($v,$data) {
$redis->hdel("message_system", (string)$v);
$redis->zrem("message_system_time", (string)$v);
//放在某個隊列當中,在消息管理子系統當中可以手動恢復
$redis->lPush("message_system_dead",json_encode($data));
});
}
$this->messageService->confirmMsgToSend($v,2); //投遞業務
}elseif($data['status']==1){ //消息狀態子系統(已經進入消息子系統但是未投遞的)
$stateJob=$this->orderService->confirmStatus($v);
//1.查詢任務結果(主動方任務是成功的,第一次投遞到被動方的服務)
if($stateJob['status']==1){
$this->messageService->confirmMsgToSend($v,1); //投遞業務
}elseif($stateJob['status']==0){ //當前任務是失敗的任務,刪掉
//3.任務失敗(刪除任務)
$this->messageService->ackMsg($v);
}
}
}
}
//判斷任務的狀態是預發送,並且確認消息狀態,如果主動方任務成功,我們就投遞,否則刪除
//確認業務狀態,業務成功投遞,業務失敗刪除
}catch (\Exception $e){
var_dump($e->getMessage());
}
});
});
}
確認併發送消息(主動方應用系統)
消息投遞到消息中間件的監聽隊列,是通過confirmToSend這個函數,用的是rabbitmq這個中間件,創建隊列,創建交換機,綁定消息交換機和隊列,從redis獲取這條消息的相關數據,如果flag爲2,說明是被恢復過的消息,投遞次數+1,發佈消息到交換機當中,並綁定好路由關係,設置消息在redis中的狀態,並將消息投遞給MQ(實時消息隊列),成功則投遞成功,失敗或者消息數據爲空則投遞失敗
public function confirmMsgToSend($msg_id, $flag): array
{
try {
$connection = $this->rabbit->connect();
$connectionRabbit = $connection->connection;
$exchangeName = 'tradeExchange';
$routeKey = '/trade';
$queueName = 'trade';
$channel = $connectionRabbit->channel();
/**
* 創建隊列(Queue)
* name: hello // 隊列名稱
* passive: false // 如果設置true存在則返回OK,否則就報錯。設置false存在返回OK,不存在則自動創建
* durable: true // 是否持久化,設置false是存放到內存中的,RabbitMQ重啓後會丟失
* exclusive: false // 是否排他,指定該選項爲true則隊列只對當前連接有效,連接斷開後自動刪除
* auto_delete: false // 是否自動刪除,當最後一個消費者斷開連接之後隊列是否自動被刪除
*/
$channel->queue_declare($queueName, false, true, false, false);
/**
* 創建交換機(Exchange)
* name: vckai_exchange// 交換機名稱
* type: direct // 交換機類型,分別爲direct/fanout/topic,參考另外文章的Exchange Type說明。
* passive: false // 如果設置true存在則返回OK,否則就報錯。設置false存在返回OK,不存在則自動創建
* durable: false // 是否持久化,設置false是存放到內存中的,RabbitMQ重啓後會丟失
* auto_delete: false // 是否自動刪除,當最後一個消費者斷開連接之後隊列是否自動被刪除
*/
$channel->exchange_declare($exchangeName, \PhpAmqpLib\Exchange\AMQPExchangeType::DIRECT, false, true, false);
// 綁定消息交換機和隊列
$channel->queue_bind($queueName, $exchangeName);
$data = $this->connectionRedis->hget("message_system", (string)$msg_id);
if (!empty($data)) {
$data = json_decode($data, true);
$data['status'] = 2;
if ($flag == 2) {
//被消息恢復子系統投遞的任務
$data['message_retries_number'] = $data['message_retries_number'] + 1;
}
$data = json_encode($data);
/**
* 創建AMQP消息類型
* delivery_mode 消息是否持久化
* AMQPMessage::DELIVERY_MODE_NON_PERSISTENT 不持久化
* AMQPMessage::DELIVERY_MODE_PERSISTENT 持久化
*/
$msg = new \PhpAmqpLib\Message\AMQPMessage($data, ['delivery_mode' => \PhpAmqpLib\Message\AMQPMessage:: DELIVERY_MODE_NON_PERSISTENT]);
//發佈消息到交換機當中,並且綁定好路由關係
if ($this->connectionRedis->hset("message_system", (string)$msg_id, $data) == 0 && $channel->basic_publish($msg,$exchangeName, $routeKey) == null) {
//將消息投遞給MQ(實時消息服務)
$data = ['status' => 1, 'result' => '確認並且投遞成功'];
} else {
$data = ['status' => 0, 'result' => '確認投遞失敗'];
}
} else {
$data = ['status' => 0, 'result' => '確認投遞失敗'];
}
$channel->close();
$connection->release(true);
return $data;
} catch (\Exception $e) {
var_dump($e->getFile(), $e->getLine(), $e->getMessage());
}
}
確認消息已被成功消費(被動方應用系統)
消息投遞到實時消息隊列後,定義一個消費端的監聽事件,來消費這些消息,從而增加用戶的積分,先連接rabbitmq,隊列綁定交換機跟路由,調用basic_consume取到消息隊列中的數據,根據消息的id從redis中取到這條消息的積分消息狀態,如果狀態爲2,說明消費成功了,如果狀態爲1,說明正在進行中,如果沒有相關的狀態,先設置消息在redis中的積分消息狀態爲1(setex消費冪等:同一個任務執行10次跟執行一次的效果是一樣),這是模擬讓我在執行中,然後設置消息狀態爲2,調用message微服務消費成功的方法ackMsg從redis中刪除消息並返回消費成功,最後basic_ack確認這條消息,表示這條已經被消費,wait表示消息的消費是阻塞等待的
public function handle(EventInterface $event): void
{
//註冊服務
$config = bean('config')->get('provider.consul');
bean('consulProvider')->registerServer($config);
$callBack = function () {
go(function () {
$context = ServiceContext::new();
\Swoft\Context\Context::set($context);
$exchangeName = 'tradeExchange';
$routeKey = '/trade';
$queueName = 'trade';
$connection=$this->rabbit->connect();
$connectionRabbit=$connection->connection;
$channel = $connectionRabbit->channel();
$channel->queue_declare($queueName, false, true, false, false);
$channel->exchange_declare($exchangeName, \PhpAmqpLib\Exchange\AMQPExchangeType::DIRECT, false, true, false);
//隊列綁定交換機跟路由
$channel->queue_bind($queueName, $exchangeName, $routeKey);
$channel->basic_consume($queueName, '', false, false, false, false, function ($message) {
$data = json_decode($message->body, true);
//1.記錄消息任務是否完成(消費冪等:同一個任務執行10次跟執行一次的效果是一樣)
$statusJob = $this->connectionRedis->get("integrating_message_job", (string)$data['msg_id']);
if ($statusJob == 2) { //已經消費成功了
$this->messageService->ackMsg($data['msg_id']); //這個任務已經消費成功了
} elseif ($statusJob == 1) { //任務正在執行
var_dump("任務正在執行當中");
return;
}else{
//執行任務當中,並且設置釋放的時間
$this->connectionRedis->setex("integrating_message_job:".$data['msg_id'],10,1);
//sleep(5);//任務正在執行當中
//2.操作mysql更新積分(業務邏輯執行完畢)
$this->connectionRedis->set("integrating_message_job:".$data['msg_id'] , 2);//執行任務完畢
$this->messageService->ackMsg($data['msg_id']); //這個任務已經消費成功了
//迴應ack
$message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);
var_dump($data);
}
});
// Loop as long as the channel has callbacks registered
while ($channel->is_consuming()) {
$channel->wait(); //阻塞消費
}
/**
* @param \PhpAmqpLib\Channel\AMQPChannel $channel
* @param \PhpAmqpLib\Connection\AbstractConnection $connection
*/
function shutdown($channel, $connection)
{
$channel->close();
$connection->close();
}
register_shutdown_function('shutdown', $channel, $connection);
});
};
// //防止進程意外崩潰,回收子進程
\Swoole\Process::signal(SIGCHLD, function ($sig) use ($callBack) {
while ($ret = \Swoole\Process::wait(false)) {
$p = new \Swoole\Process($callBack);
$p->start();
}
});
$p = new \Swoole\Process($callBack);
$p->start();
}
看看ackMsg方法,從redis中刪除該條消息,並返回消費成功
/**
* 消息消費成功
* @return array
*/
public function ackMsg($msg_id): array
{
//刪除已確認消費的消息
$result = $this->connectionRedis->transaction(function (\Redis $redis) use ($msg_id) {
$redis->hdel("message_system", (string)$msg_id);
$redis->zrem("message_system_time", (string)$msg_id);
});
if ($result[0] !== false) {
$data = ['status' => 1, 'result' => '任務消費成功'];
} else {
$data = ['status' => 0, 'result' => '任務消費失敗'];
}
return $data;
}