【譯】RabbitMQ系列(六)-RPC模式

RPC模式

在第二章中咱們學習瞭如何使用Work模式在多個worker之間派發時間敏感的任務。這種狀況是不涉及到返回值的,worker執行任務就好。若是涉及返回值,就要用到本章提到的RPC(Remote Procedure Call)了。java

本章咱們使用RabbitMQ來構建一個RPC系統:一個客戶端和一個可擴展的RPC服務端。咱們讓RPC服務返回一個斐波那契數組。json

Client interface

咱們建立一個簡單的客戶端類來演示如何使用RPC服務。call方法發送RPC請求,並阻塞知道結果返回。數組

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

RPC貼士
雖然RPC的使用在計算機領域很是廣泛,可是卻常常受到批評。主要問題是編碼人若是不注意使用的方法是本地仍是遠程時,每每會形成問題。每每讓系統變得不可預知,增長沒必要要的複雜性和調試的難度。對此咱們有以下幾點建議:安全

  • 是本地方法仍是遠程方法要一目瞭然
  • 把系統的依賴寫進文檔
  • 系統要處理好超時的問題

若是能夠儘可能使用異步的pipeline來替代像RPC這種阻塞的操做。服務器

Callback queue

在RabbitMQ上實現RPC是很是簡單的。客戶端發送一個request message,服務端迴應一個response message。爲了接受response message咱們須要在發送request message的時候附帶上'callback' queue的地址。咱們可使用默認的queue。app

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的屬性
AMQP 0-9-1協議預約義了14個消息屬性,其中大部分不多使用,下面的屬性較爲經常使用dom

  • deliverMode: 標記message爲持久(設置爲2)或其餘值。
  • contentType:message的編碼類型,咱們常用JSON編碼,則設置爲application/json
  • replyTo: 命名回調queue
  • correlationId:將RPC的請求和迴應關聯起來

須要引入新的類異步

import com.rabbitmq.client.AMQP.BasicProperties;

Correlaton Id

在上面的代碼中,每次RPC請求都會建立一個用於回調的臨時queue,咱們有更好的方法,咱們爲每個client建立一個回調queue。函數

可是這樣有新的問題,從回調queue中收到response沒法和相應的request關聯起來。這時候就是correlationId屬性發揮做用的時候了。爲每一個request中設置惟一的值,在稍後的回調queue中收到的response裏也有這個屬性,基於此,咱們就能夠關聯以前的request了。若是咱們遇到一個匹配不到的correlationId,那麼丟棄的行爲是安全的。學習

你可能會問,爲何咱們忽略這些沒法匹配的message,而不是當作一個錯誤處理呢?主要是考慮服務端的競態條件,若是RPC服務器在發送response以後就宕機了,可是卻沒有發送ack消息。那麼當RPC Server重啓以後,會繼續執行這個request。這就是爲何client須要冪等處理response。

Summary

圖片描述
咱們的RPC向下面這樣進行工做:

  • 對於一個RPC request,客戶端發送message時設置兩個屬性:replyTo設置成一個沒有名字的request獨有的queue;爲每一個request設置一個惟一的correlationId。
  • request發送到rpc_queue
  • RPC worker監聽rpc_queue。當有消息時,進行計算並經過replyTo指定的queue發送message給客戶端。
  • 客戶端監聽回調queue。當接收到message,則檢查correlationId。若是和以前的request匹配,則將消息返回給應用進行處理。

開始執行

斐波那契處理函數

private static int fib(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    return fib(n-1) + fib(n-2);
}

這是一個簡易的實現,若是傳入一個較大的值,將會是個災難。
RPC服務器的代碼爲RPCServer.java, 代碼是很簡單明確的

  • 先是創建connection,channel和聲明queue.
  • 設置prefetchCount,咱們基於請求頻繁程度,會啓動多個RPC Server
  • 使用basicConsume來接收,該方法提供回調參數設置(DeliverCallback).

RPC客戶端的代碼爲RPCClient.java,代碼略微有點複雜

  • 創建connection和channel。
  • call方法來發送RPC請求
  • 生成correlationId
  • 生成默認名字的queue用於reply,並訂閱它
  • 發送request message,設置參數replyTo和correlationId.
  • 而後返回並開始等待response到達
  • 由於消費者發送response是在另外一個線程中,咱們須要讓main線程阻塞,在這裏咱們使用BlockingQueue。
  • 消費者進行簡單的處理,爲每個response message檢查其correlationId,若是是,則將response添加進阻塞隊列
  • main函數阻塞在BlockingQueue返回
  • 將response返回給用戶

RPCClient.java完整代碼

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

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;

public class RPCClient implements AutoCloseable {

    private Connection connection;
    private Channel channel;
    private String requestQueueName = "rpc_queue";

    public RPCClient() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        connection = factory.newConnection();
        channel = connection.createChannel();
    }

    public static void main(String[] argv) {
        try (RPCClient fibonacciRpc = new RPCClient()) {
            for (int i = 0; i < 32; i++) {
                String i_str = Integer.toString(i);
                System.out.println(" [x] Requesting fib(" + i_str + ")");
                String response = fibonacciRpc.call(i_str);
                System.out.println(" [.] Got '" + response + "'");
            }
        } catch (IOException | TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String call(String message) throws IOException, InterruptedException {
        final String corrId = UUID.randomUUID().toString();

        String replyQueueName = channel.queueDeclare().getQueue();
        AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();

        channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));

        final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);

        String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
            if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                response.offer(new String(delivery.getBody(), "UTF-8"));
            }
        }, consumerTag -> {
        });

        String result = response.take();
        channel.basicCancel(ctag);
        return result;
    }

    public void close() throws IOException {
        connection.close();
    }
}

RPCServer.java完整代碼

import com.rabbitmq.client.*;

public class RPCServer {

    private static final String RPC_QUEUE_NAME = "rpc_queue";

    private static int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        return fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
            channel.queuePurge(RPC_QUEUE_NAME);

            channel.basicQos(1);

            System.out.println(" [x] Awaiting RPC requests");

            Object monitor = new Object();
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                        .Builder()
                        .correlationId(delivery.getProperties().getCorrelationId())
                        .build();

                String response = "";

                try {
                    String message = new String(delivery.getBody(), "UTF-8");
                    int n = Integer.parseInt(message);

                    System.out.println(" [.] fib(" + message + ")");
                    response += fib(n);
                } catch (RuntimeException e) {
                    System.out.println(" [.] " + e.toString());
                } finally {
                    channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes("UTF-8"));
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    // RabbitMq consumer worker thread notifies the RPC server owner thread
                    synchronized (monitor) {
                        monitor.notify();
                    }
                }
            };

            channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> { }));
            // Wait and be prepared to consume the message from RPC client.
            while (true) {
                synchronized (monitor) {
                    try {
                        monitor.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
相關文章
相關標籤/搜索