Java Springboot整合RabbitMQ(六):(RPC)-b2b2c小程式電子商務

gung123發表於2020-03-06

在第二篇教程中我們介紹瞭如何使用工作佇列(work queue)在多個工作者(woker)中間分發耗時的任務。


可是如果我們需要將一個函式執行在遠端計算機上並且等待從那兒獲取結果時,該怎麼辦呢?這就是另外的故事了。

這種模式通常被稱為遠端過程呼叫(Remote Procedure Call)或者 RPC。


這篇教程中,我們會使用 RabbitMQ 來構建一個 RPC 系統:包含一個客戶端和一個 RPC 伺服器。現在的情況是,

我們沒有一個值得被分發的足夠耗時的任務,所以接下來,我們會建立一個模擬 RPC 服務來返回斐波那契數列。


客戶端介面(Client interface)

為了展示 RPC 服務如何使用,我們將把配置檔案的名稱從 “Sender” 和 “Receiver” 更改為 “Client” 和 “Server”,

當我們呼叫伺服器時,我們將獲得引數的斐波那契值。

Integer response = (Integer) template.convertSendAndReceive(exchange.getName(), "rpc", start++);
System.out.println(" [.] Got '" + response + "'");

儘管 RPC 在計算領域是一個常用模式,但它也經常被詬病。當一個問題被丟擲的時候,程式設計師往往意識不到這到底是由本地呼叫還是由較慢的 RPC 呼叫引起的。同樣的困惑還來自於系統的不可預測性和給除錯工作帶來的不必要的複雜性。跟軟體精簡不同的是,濫用 RPC 會導致不可維護的麵條程式碼.

考慮到這一點,牢記以下建議:

確保能夠明確的搞清楚哪個函式是本地呼叫的,哪個函式是遠端呼叫的。給你的系統編寫文件。保持各個元件間的依賴明確。處理錯誤案例。明瞭客戶端改如何處理 RPC 伺服器的當機和長時間無響應情況。

當對避免使用 RPC 有疑問的時候。如果可以的話,你應該儘量使用非同步管道來代替 RPC 類的阻塞。結果被非同步地推送到下一個計算場景。

回撥佇列(Callback queue)

一般來說透過 RabbitMQ 來實現 RPC 是很容易的。一個客戶端傳送請求資訊,伺服器端將其應用到一個回覆資訊中。為了接收到回覆資訊,客戶端需要在傳送請求的時候同時傳送一個回撥佇列(callback queue)的地址。


當我們使用上面的 convertSendAndReceive() 方法時,Spring AMQP 的 RabbitTemplate 為我們處理回撥佇列,使用 RabbitTemplate 時無需做任何其他設定。有關詳細解釋,請參閱請求 / 回覆訊息。


訊息屬性

AMQP 協議給訊息預定義了一系列的 14 個屬性。大多數屬性很少會用到,除了以下幾個:


deliveryMode(投遞模式):將訊息標記為持久的(值為 2)或暫存的(除了 2 之外的其他任何值)。

contentType(內容型別): 用來描述編碼的 mime-type。例如在實際使用中常常使用 application/json 來描述 JOSN 編碼型別。

replyTo(回覆目標):通常用來命名回撥佇列。

correlationId(關聯標識):用來將 RPC 的響應和請求關聯起來。

關聯標識(Correlation Id)

上邊介紹的方法中,我們建議給每一個 RPC 請求新建一個回撥佇列。這不是一個高效的做法,幸好這兒有一個更好的辦法 —— 我們可以為每個客戶端只建立一個獨立的回撥佇列。


這就帶來一個新問題,當此佇列接收到一個響應的時候它無法辨別出這個響應是屬於哪個請求的。correlationId 就是為了解決這個問題而來的。我們給每個請求設定一個獨一無二的值。稍後,當我們從回撥佇列中接收到一個訊息的時候,我們就可以檢視這條屬性從而將響應和請求匹配起來。如果我們接手到的訊息的 correlationId 是未知的,那就直接銷燬掉它,因為它不屬於我們的任何一條請求。


你也許會問,為什麼我們接收到未知訊息的時候不丟擲一個錯誤,而是要將它忽略掉?這是為了解決伺服器端有可能發生的競爭情況。儘管可能性不大,但 RPC 伺服器還是有可能在已將應答傳送給我們但還未將確認訊息傳送給請求的情況下死掉。如果這種情況發生,RPC 在重啟後會重新處理請求。這就是為什麼我們必須在客戶端優雅的處理重複響應,同時 RPC 也需要儘可能保持冪等性。


總結


我們的 RPC 如此工作:


當客戶端啟動的時候,它建立一個匿名獨享的回撥佇列。

在 RPC 請求中,客戶端傳送帶有兩個屬性的訊息:一個是設定回撥佇列的 replyTo 屬性,另一個是設定唯一值的 correlationId 屬性。

將請求傳送到一個 rpc_queue(tut.rpc) 佇列中。

RPC 工作者(又名:伺服器)等待請求傳送到這個佇列中來。當請求出現的時候,它執行他的工作並且將帶有執行結果的訊息傳送給 replyTo 欄位指定的佇列。

客戶端等待回撥佇列裡的資料。當有訊息出現的時候,它會檢查 correlationId 屬性。如果此屬性的值與請求匹配,將它返回給應用。 整合到一起,瞭解springcloud架構可以加求求:三五三六二四七二五九

程式碼整合

斐波那契數列任務:

private int fib(int i) {
    return (i == 0 || i == 1) ? i : (fib(i - 2) + fib(i - 1));
}

我們定義一個斐波那契的方法,假定只有有效的正整數輸入。(不要指望它為大資料工作,這可能是最慢的遞迴實現)

配置類

@Profile({"tut6", "rpc"})
@Configuration
public class Tut6Config {
    @Profile("client")
    private static class ClientConfig {
        @Bean
        public DirectExchange exchange() {
            return new DirectExchange("tut.rpc");
        }
         @Bean
        public Tut6Client client() {
            return new Tut6Client();
        }
    }
   @Profile("server")
     private static class ServerConfig {
         @Bean
        public Queue queue() {
            return new Queue("tut.rpc.requests");
        }
         @Bean
        public DirectExchange exchange() {
            return new DirectExchange("tut.rpc");
        }
         @Bean
        public Binding binding(DirectExchange exchange, Queue queue) {
            return BindingBuilder.bind(queue)
                    .to(exchange)
                    .with("rpc");
        }
         @Bean
        public Tut6Server server() {
            return new Tut6Server();
        }
    }
}

服務端

public class Tut6Server {
   @RabbitListener(queues = "tut.rpc.requests")
    public int process(int in) {
        System.out.println(" [x] Received request for " + in);
        int result = fib(in);
        System.out.println(" [.] Returned " + result);
        return result;
    }
    /**
     * 斐波那契數
     *
     * @param i
     * @return
     */
    private int fib(int i) {
        return (i == 0 || i == 1) ? i : (fib(i - 2) + fib(i - 1));
    }
}

客戶端

public class Tut6Client {
   @Autowird 
    private AmqpTemplate template;
    @Autowird
    private DirectExchange exchange;
    private int start = 0;
    @Scheduled(fixedDelay = 1000, initialDelay = 500)
    public void send() {
        System.out.println(" [x] Requesting fib(" + start + ")");
        Integer response = (Integer) template
                .convertSendAndReceive(exchange.getName(), "rpc", start++);
        System.out.println(" [.] Got '" + response + "'");
    }
}

執行

maven 編譯


mvn clean package -Dmaven.test.skip=true

執行


java -jar target/rabbitmq-tutorial-0.0.1-SNAPSHOT.jar --spring.profiles.active=tut6,server --tutorial.client.duration=60000

java -jar target/rabbitmq-tutorial-0.0.1-SNAPSHOT.jar --spring.profiles.active=tut6,client --tutorial.client.duration=60000

輸出


// Client

Ready … running for 60000ms

[x] Requesting fib(0)

[.] Got ‘0’

[x] Requesting fib(1)

[.] Got ‘1’

[x] Requesting fib(2)

[.] Got ‘1’

[x] Requesting fib(3)

[.] Got ‘2’

[x] Requesting fib(4)

[.] Got ‘3’

[x] Requesting fib(5)

[.] Got ‘5’

[x] Requesting fib(6)

[.] Got ‘8’

[x] Requesting fib(7)

[.] Got ‘13’

[x] Requesting fib(8)

[.] Got ‘21’


// Server

Ready … running for 60000ms

[x] Received request for 0

[.] Returned 0

[x] Received request for 1

[.] Returned 1

[x] Received request for 2

[.] Returned 1

[x] Received request for 3

[.] Returned 2

[x] Received request for 4

[.] Returned 3

[x] Received request for 5

[.] Returned 5

[x] Received request for 6

[.] Returned 8

[x] Received request for 7

[.] Returned 13

[x] Received request for 8

[.] Returned 21

以上的設計不是唯一可能的實現一個 RPC 服務的,但它有一些重要的優點:


如果 RPC 伺服器速度太慢,則只需執行多個即可。嘗試在新的控制檯執行的第二個 RPCServer。

在客戶端,RPC 請求只傳送或接收一條訊息。不需要像 queueDeclare 這樣的非同步呼叫。所以 RPC 客戶端的單個請求只需要一個網路往返。

我們的程式碼依舊非常簡單,而且沒有試圖去解決一些複雜(但是重要)的問題,如:


當沒有伺服器執行時,客戶端如何作出反映。

客戶端是否需要實現類似 RPC 超時的東西。

如果伺服器發生故障,並且丟擲異常,應該被轉發到客戶端嗎?

在處理前,防止混入無效的資訊(例如檢查邊界)


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69952307/viewspace-2678811/,如需轉載,請註明出處,否則將追究法律責任。

相關文章