基於Yii2對RabbitMQ的基本用法封裝及RPC佇列(二)

so_easy發表於2020-08-28

前言

RPC即跨專案呼叫。可以將一個大的專案分成多個子專案,然後子專案之間透過對外提供API從而實現了專案之間的解耦,再者便於各個子專案的單獨部署和管理。而子專案之間的呼叫除了提供API之外,還可以透過比如Yar將專案的類對外提供服務,其他專案對類的呼叫就猶如呼叫自己的類一樣。
再此透過RabbitMQ實現的RPC佇列也可以完成跨專案呼叫,但是實現的方式和Yar不同。實現的原理圖如下:

基於Yii2對RabbitMQ的基本用法封裝及RPC佇列(二)

1、client將request(包含corrid和reply_to引數)傳送到了事先定義好的佇列rpc_queue
2、啟動rpc_queue佇列的消費者去處理佇列中的訊息,並且將響應結果投遞到reply_to指定的臨時佇列
3、等待臨時佇列的消費者返回rpc_queue的結果並校驗,最終返回client。
4、client根據返回訊息的corrid和request的corrid對比(批次請求時用到)。

RPC佇列的配置和使用

配置和普通佇列的配置一樣簡單,在Yii的配置檔案中配置RPC佇列的元件屬性:

/** RPC佇列 */
    'rpcQueue' => [
        'class' =>  \pzr\amqp\queue\RpcQueue::class,
        'host' => '127.0.0.1',
        'port' => 5672,
        'user' => 'guest',
        'password' => 'guest',
        'queueName' => 'rpc',
        'exchangeName' => 'rpc',
        'routingKey' => 'rpc',
        'duplicate' => 2,
    ],

然後在控制器中呼叫元件物件:

public function actionRpc() {
        Yii::$app->rpcQueue->on(AmqpBase::EVENT_BEFORE_PUSH, function(PushEvent $event) {
            Yii::$app->rpcQueue->bind();
        });

        // 批次請求
        for ($i=1; $i<=10; $i++) {
            $jobs[] = new RequestJob([
                'request' => 'request_' . $i,
            ]);
        }
        $response = Yii::$app->rpcQueue->publish($jobs);
        return $response;
    }

這裡RequestJob是消費體物件必須繼承AmqpJob,程式碼如:

class RequestJob extends AmqpJob
{    
    public $request;
    public function execute()
    {
        $response = $this->request . ', corrid:' . $this->getUuid();
        return $response;
    }
}

不出意外得到:

request_1,corrid:xxxx
request_2,corrid:xxxx
... //省略
request_9,corrid:xxxx

這裡的corrid是請求的唯一標誌。因為支援批次請求,如果對結果的返回需要對號入座那麼就需要用到corrid。

RPC佇列的實現

竟然講到了RPC,那麼就以跨專案呼叫舉例(很細緻):
場景:A專案期望呼叫B專案的某個方法
step1:在A和B專案中都執行引入pzr/amqp包操作

composer require pzr/amqp

step2:引入包之後,可以看到在vendor目錄下有pzr/amqp目錄,目錄大致情況是:

pzr/amqp
    forntend
    src

如果只是為了測試功能大可以在frontend目錄下修改Yii2的配置和控制器,但是一旦執行composer update操作可能就會被覆蓋。因此建議copy frontend目錄。
frontend目錄是AMQP消費者程式web管理,所以很多和web相關的程式碼對於Rpc呼叫來說其實並無作用。frontend目錄大致情況是:

assets/
commands/
config/
controllers/
models/
vagrant/
views/
web/
widgets/

如果只是為了實現RPC功能只有以下目錄留著就行:

commands/
config/
models/

這裡假設copy的frontend重新命名為yii
step3:A呼叫B,所以可以理解為A為客戶端,B為服務端
A的使命就是發出請求並且等待響應,B的使命就是響應請求。因為A和B是兩個專案,所以專案環境的不同也將導致一個問題:B為了響應A的請求,佇列的消費者“必須”在B工作環境下啟動,否則可能導致A接收不到預期的結果。使用過Yar的同學肯定知道Yar的一個方便之處在於RPC呼叫的API也提供視覺化,對於Client呼叫方來說非常友好!但是這裡似乎做不到API的視覺化,呼叫的基礎是A已經瞭解B專案的呼叫方法。基於此去展開的RPC呼叫。

之前希望B可以返回A呼叫的序列化物件,但是發現物件能被序列化的只有非靜態化變數,方法和靜態屬性等都無法序列化。因此只能採取Client告知Server我需要呼叫哪個class、哪個method,傳入什麼引數。Server得到Client的請求之後就會按請求封裝物件,然後響應方法。如:(new $class())->method($args)。為了解決名稱空間的問題,往往Client在請求的時候需要帶上如:\namespace\class 去請求Server,於是Server才能夠正確找到需要例項化的物件。對於本身使用Composer管理的專案,那麼很輕鬆的就能載入並找到\namespace\class,而對於不是使用Composer管理的專案,那麼需要自己去引入名稱空間的autoload方法。

step4:準備工作完了之後,開始編寫程式碼
在A專案中yii/commands下實現ClientController類發出請求(無關程式碼不體現):

class ClientController extends Controller
{
    // qos消費者預處理數
    // timeout 請求超時時間,如果超過時間還沒接收到完整資料則返回結果 
    public  function  actionRequest($jobs, $qos=1, $timeout=3) {
        if (empty($jobs)) {
            return  null;
        }
        // 傳送訊息之前先繫結訊息,因為對於請求方來說如果傳送的請求丟失可以自己重新請求,所以關閉傳送方訊息確認機制
        Yii::$app->on(AmqpBase::EVENT_BEFORE_PUSH, function(PushEvent  $event) {
            $event->noWait = true;
            Yii::$app->rpcQueue->bind();
        });
        $response = Yii::$app->rpcQueue->setQos($qos)
            ->setTimeout($timeout)
            ->publish($jobs);
        return  $response;
    }
}

A專案中的Client算是完成了,接下來需要在models編寫Job類。Job類可以寫成通用型,即A專案中所有的RPC呼叫都可以複用的那種。

class RpcJob extends AmqpJob
{
    public $object; //請求的物件
    public $action;    //請求的方法
    public $params = []; //方法可能需要的引數

    // 這是執行請求響應的方法,只有在Server專案中啟動的消費者才能夠有效。如果在其他環境啟動消費者,那麼可能導致請求接收到不到響應資料(因為其他工作環境無法例項化並且響應請求)。
    public function execute()
    {
        try {
            $object = $this->object;
            $obj = new $object();
            if (!is_object($obj)) {
                return false;
            }
            if (!method_exists($obj, $this->action)) {
                return false;
            }
            return call_user_func_array([$obj, $this->action], $this->params);
        } catch (Exception $e) {
        } catch (Throwable $e) {
        }

        return false;
    }
}

最後只需要在A專案中實際需要RPC呼叫的地方去呼叫Client/request即可,如在A專案中的IndexController(個人專案的控制器,和Yii2無關)中進行RPC呼叫。

class IndexController
{
    public function init() 
    {
        $yii = new  MyYii();
        $chargeStatusUuid = uniqid(true);
        $jobs[] = new RpcJob([
            'object' => 'PayConfig',
            'action' => 'getChargeStatus',
            'params' => [],
            'uuid' => $chargeStatusUuid, //唯一標誌請求ID,當然如果你喜歡也可以自己隨便寫,只要能夠區分請求即可。
            ]);
        $response = $yii->request([
            'client/request', // 採用的Yii2的請求規則,類似:php yii client/request ,但是這裡我對yii指令碼改造了下,實現了MyYii
            $jobs
        ]);
        $chargeStatus = $response[$chargeStatusUuid];
    }
}

上面的例項程式碼實現的是:A在init方法中想要呼叫B專案中的某個配置方法。然後B和A的實現步驟完全一致即可。
step5:在B專案中的實現和A完全一模一樣,不同的是在B專案中需要啟動消費者去響應
最後在A得到結果截圖如:
基於Yii2對RabbitMQ的基本用法封裝及消費程式管理控制(二)

B中關於chargeStatus 的配置截圖如:

基於Yii2對RabbitMQ的基本用法封裝及消費程式管理控制(二)
如果是多個請求同時傳送,那麼按corrid對應獲取。

後言

在後來對這個專案也升級過很多次,當然存在很多缺陷也一直最佳化。也在使用的過程當中經常碰到各種問題。

1、找不到對應的檔案vendor/bower-asset/jquery/dist
方案1:composer require “fxp/composer-asset-plugin” -vvv (這個成功了)
方案2:在composer.json新增如下(未嘗試):

"config": {
        "fxp-asset": {
            "installer-paths": {
                "npm-asset-library": "vendor/npm",
                "bower-asset-library": "vendor/bower"
            }
        }
    }
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章