系統拆分解耦利器之消息隊列---RabbitMQ-RPC遠程調用

[一曲廣陵不如晨鐘暮鼓]

本文,我們來介紹RabbitMQ中的RPC遠程調用。在正式開始之前,我們假設RabbitMQ服務已經啓動,運行端口爲5672,如果各位看官有更改過默認配置,那麼就需要修改爲對應端口,保持一致即可。

準備工作:

操作系統:window 7 x64 

其他軟件:eclipse mars,jdk7,maven 3

--------------------------------------------------------------------------------------------------------------------------------------------------------


Remote procedure call (RPC)


在前文介紹的關於“工作隊列”的教程中,我們演示瞭如何在多個接受這之間分發資源密集型的任務。接下來,我們將繼續深入的討論這個問題。

如果我們想要在一臺遠程的機器上運行一個資源密集型的任務,那麼是不是就意味着需要等待返回結果呢?本文,我們就來介紹RabbitMQ中的RPC遠程調用模式的概念及使用。

在下文中,我們將會演示構建一個遠程調用系統:一個客戶端,一個可伸縮的RPC服務器。由於我們並沒有實際的資源密集型的任務,所以我們打算假裝執行一個RPC服務,實際上卻是返回一個斐波那契數列。


客戶端接口(Client interface)


爲了說明RPC服務是如何被使用的,我們將創建一個簡單的客戶端,其將負責暴露一個call方法,並且其會堵塞進程,直到接收到返回值。示例如下:

FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();   
String result = fibonacciRpc.call("4");
System.out.println( "fib(4) is " + result);

特別提醒:

儘管在系統中,RPC調用時非常普遍存在的模式,但其卻常被人們所詬病。問題發生在:當程序開發人員沒有及時的注意到一個服務是被本地調用的,或者,遠程調用過程非常的緩慢。類似於這樣的情況發生在不可預測的系統環境中,並且,爲了測試系統,增加了不必要的測試複雜度。本應該簡化的軟件,由於濫用RPC導致了在系統中增加了大量不可維護的代碼。

銘記上面的問題,我們給出以下的幾點建議:

  • 確保可以明確看出:那些服務是本地調用,那些服務是遠程調用。
  • 將系統結構文檔化,使系統結構之間的關係變得清晰可見。
  • 及時進行錯誤處理。當遠程調用服務器發生錯誤時,如長時間,客戶端是如何應對的?

當存在疑問時,儘量的避免的使用RPC調用。如果條件允許的話,推薦使用異步的消息管道---而不是RPC---效果類似於阻塞,最終異步調用被延遲到下一個計算過程(意譯爲調度過程,方便理解)


回調隊列(Callback queue)


一般來講,在RabbitMQ上搭建RPC調用框架是非常容易的---客戶端發送請求消息,服務端迴應消息。爲了接收響應消息,我們需要在發送請求時,附帶一個回調隊列地址。可以使用默認隊列(Java客戶端特有的),具體如下:

callbackQueueName = channel.queueDeclare().getQueue();

BasicProperties props = new BasicProperties
                            .Builder()
                            .replyTo(callbackQueueName)
                            .build();

channel.basicPublish("", "rpc_queue", props, message.getBytes());

// ... then code to read a response message from the callback_queue ...

消息屬性(Message properties)

AMQP協議預定義了14個消息屬性。其中的大部分都是很少用到的,但是下面的幾個,希望各位看官能夠牢記:

  • deliveryMode:設置消息持久化功能時,設置爲整形數:2。其他任何值都是標示臨時的含義。關於這個屬性的具體內容可以在前文介紹的工作隊列教程中尋找。
  • contentType:爲了描述編碼mime-type。如常見的JSON格式。非常推薦的良好習慣,就是設置該屬性爲:application/json。
  • replyTo:通常情況下,用來指明一個回調隊列。
  • correlationId:用來關聯RPC的請求與相應。

綜上,我們需要在類中引入下面這句話:

import com.rabbitmq.client.AMQP.BasicProperties;

關聯ID(Correlation ID)

在上面介紹的方法中,我們暗示了需要爲每個RPC請求創建一個回調隊列。這種做法顯然是非常低效率的,但幸運的是,有一種更好的方式供我們選擇---我們可以爲所有的客戶端之創建一個回調隊列。

但是,這種做法又帶來的新的問題,隊列接收到一個響應時,無法確定其歸屬於哪一個請求。這正是關聯ID發揮作用的時機。我們將爲每一個請求與返回之間設置一個唯一的關聯ID。之後,當回調隊列中接收到響應時,我們再來觀察該屬性,並且基於它,我們就有辦法將請求與響應進行匹配。如果我們發現一個未知的關聯ID,就可以在保證安全的前提下,丟棄這條消息---因爲其不屬於我們已經記錄下的所發出的請求。

各位看官可能會問:爲什麼我們可以忽略掉隊列當中未知的消息,而不是產生一條錯誤。這是因爲:在服務器上發生競爭條件的可能性,儘管很小很小,但仍然有可能發生:RPC服務器,在我們剛發送完響應之後,發生宕機,但還沒來得及向請求方進行消息確認。如果這種情況發生了,重啓RPC調用將會再次發起這個請求。這就是爲什麼在客戶端上,我們就必須的完全的處理好重複響應,並且,RPC服務最好是冪等性的。


總結(Summary)


我們的RPC將會類似下面的流程進行工作:

  • 當客戶端啓動時,其將會創建一個匿名的特定的回調隊列。
  • 對於一個RPC調用,客戶端發送出的消息都帶有兩個屬性:replyTo,設置回調隊列。關聯ID(correlationId),對請求設置唯一的id值。
  • 請求被髮送到一個稱爲“rpc_queue”的隊列當中。
  • RPC worker(也稱之爲:server)在隊列上一直等待請求的發生。當發生請求時,其處理該任務,並使用relayTo指定的隊列,將請求結果以消息的形式發送到客戶端當中。
  • 客戶端在回調隊列當中等待數據。當有一條消息出現時,就會檢查關聯ID屬性(correlationId)。如果其能夠匹配到請求當中的任何一個,那麼就會將響應返回給應用程序。

綜上所述,我們來看看完整的工程吧,具體內容如下:


1.修改pom文件,具體內容請看前文,在此不再贅述。

2.創建RPCClient文件,具體內容如下:

package com.csdn.ingo.rabbitmq_1;
import com.rabbitmq.client.AMQP;  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
import com.rabbitmq.client.QueueingConsumer;  
import com.rabbitmq.client.AMQP.BasicProperties; 

public class RPCClient {  
    private Connection connection;  
    private Channel channel;  
    private String requestQueueName = "rpc_queue";  
    private String replyQueueName;  
    private QueueingConsumer consumer;  
  
    public RPCClient() throws Exception {  
        //• 先建立一個連接和一個通道,併爲回調聲明一個唯一的'回調'隊列  
        ConnectionFactory factory = new ConnectionFactory();  
        factory.setHost("localhost");  
        factory.setPort(AMQP.PROTOCOL.PORT);  
        connection = factory.newConnection();  
        channel = connection.createChannel();  
        //• 註冊'回調'隊列,這樣就可以收到RPC響應  
        replyQueueName = channel.queueDeclare().getQueue();  
        consumer = new QueueingConsumer(channel);  
        channel.basicConsume(replyQueueName, true, consumer);  
    }  
  
    //發送RPC請求  
    public String call(String message) throws Exception {  
        String response = null;  
        String corrId = java.util.UUID.randomUUID().toString();  
        //發送請求消息,消息使用了兩個屬性:replyto和correlationId  
        BasicProperties props = new BasicProperties.Builder()  
                .correlationId(corrId).replyTo(replyQueueName).build();  
        channel.basicPublish("", requestQueueName, props, message.getBytes());  
        //等待接收結果  
        while (true) {  
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();  
            //檢查它的correlationId是否是我們所要找的那個  
            if (delivery.getProperties().getCorrelationId().equals(corrId)) {  
                response = new String(delivery.getBody());  
                break;  
            }  
        }  
        return response;  
    }  
    public void close() throws Exception {  
        connection.close();  
    }  
}  
3.創建RPCMain文件,具體內容如下:

package com.csdn.ingo.rabbitmq_1;
public class RPCMain {  
  
    public static void main(String[] args) throws Exception {  
        RPCClient rpcClient = new RPCClient();  
        System.out.println(" [x] Requesting getMd5String(abc)");     
        String response = rpcClient.call("abc");  
        System.out.println(" [.] Got '" + response + "'");  
        rpcClient.close();  
    }  
}  
4.創建RPCServer文件,具體內容如下:

package com.csdn.ingo.rabbitmq_1;

import java.security.MessageDigest;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;


public class RPCServer {
	private static final String RPC_QUEUE_NAME = "rpc_queue";  
    public static void main(String[] args) throws Exception {  
        //• 先建立連接、通道,並聲明隊列  
        ConnectionFactory factory = new ConnectionFactory();  
        factory.setHost("localhost");  
        factory.setPort(AMQP.PROTOCOL.PORT);  
        Connection connection = factory.newConnection();  
        Channel channel = connection.createChannel();  
        channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);  
        //•可以運行多個服務器進程。通過channel.basicQos設置prefetchCount屬性可將負載平均分配到多臺服務器上。  
        channel.basicQos(1);  
        QueueingConsumer consumer = new QueueingConsumer(channel);  
        //打開應答機制autoAck=false  
        channel.basicConsume(RPC_QUEUE_NAME, false, consumer);  
        System.out.println(" [x] Awaiting RPC requests");  
        while (true) {  
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();  
            BasicProperties props = delivery.getProperties();  
            BasicProperties replyProps = new BasicProperties.Builder()  
                    .correlationId(props.getCorrelationId()).build();  
            String message = new String(delivery.getBody());  
            System.out.println(" [.] getMd5String(" + message + ")");  
            String response = getMd5String(message);  
            //返回處理結果隊列  
            channel.basicPublish("", props.getReplyTo(), replyProps,  
                    response.getBytes());  
            //發送應答   
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);  
        }  
    }  
    // 模擬RPC方法 獲取MD5字符串  
    public static String getMd5String(String str) {  
        MessageDigest md5 = null;  
        try {  
            md5 = MessageDigest.getInstance("MD5");  
        } catch (Exception e) {  
            System.out.println(e.toString());  
            e.printStackTrace();  
            return "";  
        }  
        char[] charArray = str.toCharArray();  
        byte[] byteArray = new byte[charArray.length];  
  
        for (int i = 0; i < charArray.length; i++)  
            byteArray[i] = (byte) charArray[i];  
        byte[] md5Bytes = md5.digest(byteArray);  
        StringBuffer hexValue = new StringBuffer();  
        for (int i = 0; i < md5Bytes.length; i++) {  
            int val = ((int) md5Bytes[i]) & 0xff;  
            if (val < 16)  
                hexValue.append("0");  
            hexValue.append(Integer.toHexString(val));  
        }  
        return hexValue.toString();  
    }  
}
5.測試方法,首先啓動Server,在運行main方法,觀察控制檯輸出即可。

6.特別備註:

上面這份源碼,摘自其他博文,再次表示感謝。

我們沒有斐波那契數列作爲演示,但原理一致,有興趣的看官可以在官方文檔中找到源碼,自行測試即可。

上面源碼中使用的方法在前文中均有解釋,有疑問的地方,請各位看官自行查看。

--------------------------------------------------------------------------------------------------------------------------------------------------------

至此,系統拆分解耦利器之消息隊列---RabbitMQ-RPC遠程調用 結束


參考資料:

官方文檔:http://www.rabbitmq.com/tutorials/tutorial-six-java.html


發佈了119 篇原創文章 · 獲贊 182 · 訪問量 59萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章