10.微服務-分佈式事務最終一致性(三)


對應的步驟
在這裏插入圖片描述
看看代碼實現流程,支付成功後,首先是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;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章