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

消息子系統的構建

1.構建消息服務子系統(包含接口)

存儲預發送消息(主動方應用系統)
確認併發送消息(主動方應用系統)
確認消息已被成功消費(被動方應用系統)
查詢狀態確認超時的消息(消息狀態確認子系統)
查詢消費確認超時的消息(消息恢復子系統)

2.構建其他服務(骨架大搭建)

1.訂單服務
2.積分服務
3.消息子系統
4.網關服務

3.簡單的封裝rabbitMQ的連接池

1.解決連接池,無法取出連接的問題

4、消息發佈確認
5、消息存儲的設計(任務數據存儲)

version 版本號
create_time 創建時間
message_id 消息ID
message_body 消息內容
consumer_queue 消費隊列
message_retries_number 消息重發次數
dead 是否死亡
status 狀態(預發送、發送中、已消費、已死亡)

6、消息id的生成
7、服務提供任務執行結果查詢接口
整體流程:

1、用戶下單,主動方應用預發送消息給消息服務子系統。
2、消息服務子系統存儲預發送的消息。
3、返回存儲預發送消息的結果。
4、如果第3步返回的結果是成功的,則執行業務操作,否則不執行。
5、業務操作成功後,調用消息服務子系統進行確認發送消息。
6、將消息服務庫中存儲的預發送消息發送,並更新該消息的狀態爲已發送(但不是已被消費)。
7、消息中間件發送消息到消費端應用。
8、消費端應用調用被動方應用服務。
9、被動方應用返回結果給消費端應用。
10、消費端應用向消息中間件ack此條消息,並向消息服務子系統進行確認成功消費消息,11、讓消息服務子系統刪除該條消息或者將狀態置爲已成功消費。
12、消息狀態子系統定時去查一下消息數據,看看有沒有是已發送狀態的超時消息,就是一直沒有變成已成功消費的那種消息,主動方應用系統應該提供查詢接口,針對某條消息查詢該條消息對應的業務數據是否爲處理成功
13、如果業務數據是處理成功的狀態,那麼就再次調用確認併發送消息,即進入第6步。
14、如果業務數據是處理失敗的,那麼就調用消息服務子系統進行刪除該條消息數據。
在這裏插入圖片描述

現在在做紅色框框的部分,代碼體現
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];
    }

通過rpc調用message服務的預存儲消息函數,通過redis事務把數據存入message_system,和message_system_time中,他們是通過$msg_id來區分不同的消息

 public function prepareMsg($prepareMsgData): array
    {
        $msg_id = $prepareMsgData['msg_id'];
        //存數據,還要存時間,依據時間查找超時的任務
        $result = $this->connectionRedis->transaction(function (\Redis $redis) use ($msg_id, $prepareMsgData) {
            $redis->hset("message_system", (string)$msg_id, json_encode($prepareMsgData));
            $redis->zAdd("message_system_time", $prepareMsgData['create_time'], (string)$msg_id);
        });
        if ($result[0] == false) {
            return ['status' => 0, 'result' => '預發送消息失敗'];
        }
        return ['status' => 1, 'result' => '預發送消息成功'];

    }

預存儲消息成功後,rpc調用訂單服務的update函數更新訂單的狀態,如果訂單狀態更新成功,則調用消息服務確認發送消息的函數confirmMsgToSend

public function update($data): array
    {
        //業務執行成功
        //調用mysql更新信息(業務邏輯)
        //確認某個任務執行成功
        if($this->redis->hset("order_message_job",(string)$data['msg_id'],(string)1)){
            return ['status'=>1,'result' =>'訂單狀態更新成功'];
        }
        return ['status'=>1,'result' =>'訂單狀態更新失敗'];
    }

confirmMsgToSend函數,連接rabbitmq的連接池,從連接池裏面取出對應的實例,調用裏面的方法,從redis裏面取出消息的數據,如果消息狀態爲2就是之前沒有發送成功,現在恢復發送的消息,在嘗試次數上加1,創建消息隊列,發送消息到交換機中,如果redis中這條消息爲0並且發佈消息爲null,則投遞消息成功

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());
        }
    }

現在來看這兩行代碼發生了什麼

	$connection = $this->rabbit->connect();
	$connectionRabbit = $connection->connection;

這裏封裝了rabbitmq連接連接池的相關代碼

 public function connect()
    {
        try {
            /* @var ConnectionManager $conManager */
            $conManager = BeanFactory::getBean('connection.manager');
            $connection = $this->getConnection();
            $connection->setRelease(true); //設置重新使用
            $conManager->setConnection($connection); //設置連接
        } catch (Throwable $e) {
            throw new \Exception(
                sprintf('Pool error is %s file=%s line=%d', $e->getMessage(), $e->getFile(), $e->getLine())
            );
        }
        // Not instanceof Connection
        if (!$connection instanceof Connection) {
            throw new \Exception(
                sprintf('%s is not instanceof %s', get_class($connection), Connection::class)
            );
        }
        return $connection;
    }

getBean函數是從容器中調用獲取實例化方法獲取實例名字

public static function getBean(string $name)
    {
        return Container::getInstance()->get($name);
    }

getConnection函數,通過通道獲取連接

public function getConnection(): ConnectionInterface
    {
        return $this->getConnectionByChannel();
    }

getConnectionByChannel函數,如果通道爲空創建一個,如果沒有到達最小連接數則創建連接,如果通道不爲空則從裏面拋出一個連接,如果連接不爲空,則更新連接的最新數據並返回,如果小於最大連接數,則創建連接,如果最大等待大於0,並且消費數量大於最大等待拋異常,以上都沒進入返回就從通道拋出最大等待時間,調用更新返回連接函數並返回連接

private function getConnectionByChannel(): ConnectionInterface
    {
        // Create channel
        if ($this->channel === null) {
            $this->channel = new Channel($this->maxActive);
        }

        // To reach `minActive` number
        if ($this->count < $this->minActive) {
            return $this->create();
        }

        // Pop connection
        $connection = null;
        if (!$this->channel->isEmpty()) {
            $connection = $this->popByChannel();
        }

        // Pop connection is not null
        if ($connection !== null) {
            // Update last time
            $connection->updateLastTime();
            return $connection;
        }

        // Channel is empty or  not reach `maxActive` number
        if ($this->count < $this->maxActive) {

            return $this->create();
        }

        // Out of `maxWait` number
        $stats = $this->channel->stats();
        if ($this->maxWait > 0 && $stats['consumer_num'] >= $this->maxWait) {
            throw new ConnectionPoolException(
                sprintf('Channel consumer is full, maxActive=%d, maxWait=%d, currentCount=%d',
                    $this->maxActive, $this->maxWaitTime, $this->count)
            );
        }

        /* @var ConnectionInterface $connection*/
        // Sleep coroutine and resume coroutine after `maxWaitTime`, Return false is waiting timeout
        $connection = $this->channel->pop($this->maxWaitTime);
        if ($connection === false) {
            throw new ConnectionPoolException(
                sprintf('Channel pop timeout by %fs', $this->maxWaitTime)
            );
        }

        // Update last time
        $connection->updateLastTime();

        return $connection;
    }

看看通道里面的create函數

/**
     * @return ConnectionInterface
     *
     * @throws ConnectionPoolException
     */
    private function create(): ConnectionInterface
    {
        // Count before to fix more connection bug
        $this->count++;
        try {
            $connection = $this->createConnection();
        } catch (Throwable $e) {
            // Create error to reset count
            $this->count--;

            throw new ConnectionPoolException(
                sprintf('Create connection error(%s) file(%s) line (%d)',
                    $e->getMessage(),
                    $e->getFile(),
                    $e->getLine())
            );
        }

        return $connection;
    }

create裏面調用了createConnection()

 public function createConnection(): ConnectionInterface
    {
        if (empty($this->client)) {
            throw new \Exception(
                sprintf('Pool(%s) client can not be null!', __CLASS__)
            );
        }
        return   $this->client->createConnection($this);
    }

然後是client調用連接池的createConnection()

public function createConnection($pool): Connection
    {
        $connection = Connection::new($this, $pool);
        $connection->create();
        return $connection;
    }

然後連接池createConnection調用的create方法,是一個tcp連接

 public function create(): void
    {
        $connection = new \Co\Client(SWOOLE_SOCK_TCP);
        [$host, $port] = $this->getHostPort();
        $setting = $this->client->getSetting();
        //賦值屬性用於區分服務
        $this->host=$host;
        $this->port=$port;

        if (!empty($setting)) {
            $connection->set($setting);
        }
        if (!$connection->connect($host, (int)$port)) {
            throw new RpcClientException(
                sprintf('Connect failed host=%s port=%d', $host, $port)
            );

        }
        $this->connection = $connection;
    }

再看看這裏面的connect是調用Swoole\Coroutine的client類,是一個協程連接

 public function connect(string $host, int $port=null, float $timeout=null, $sock_flag=null){}

打印$connection = $this->rabbit->connect();這個數據,數據太多刪掉了一些


object(Six\Rabbit\Connection)#6651 (9) {
  ["connection"]=>
  object(PhpAmqpLib\Connection\AMQPStreamConnection)#6652 (44) {
    ["channels"]=>
    array(1) {
      [0]=>
      *RECURSION*
    }
 
    }
    ["methodMap":protected]=>
    object(PhpAmqpLib\Helper\Protocol\MethodMap091)#6661 (1) {
      ["method_map":protected]=>
      array(64) {
        ["10,10"]=>
        string(16) "connection_start"
        ["10,11"]=>
      
      }
    }
    ["channel_id":protected]=>
    int(0)
    ["msg_property_reader":protected]=>
  
  ["pool":protected]=>
  object(Six\Rabbit\Pool)#6437 (11) {
    ["client":protected]=>
    object(Six\Rabbit\Rabbit)#6417 (3) {
      ["host":protected]=>
      string(9) "127.0.0.1"
      ["port":protected]=>
      int(5672)
  
    ["channel":protected]=>
    object(Swoole\Coroutine\Channel)#6649 (2) {
      ["capacity"]=>
      int(1000)
      ["errCode"]=>
      int(0)
    }
  
}

設置連接函數setConnection就是把connection打包成數組

 public function setConnection(ConnectionInterface $connection): void
    {
        $key = sprintf('%d.%d.%d', Co::tid(), Co::id(), $connection->getId());
        $this->set($key, $connection);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章