RabbitMQ指南

 

轉載自:http://www.importnew.com/24319.html

RabbitMQ是一個消息中間件,在一些需要異步處理、發佈/訂閱等場景的時候,使用RabbitMQ可以完成我們的需求。 下面是我在學習RabbitMQ的過程中的一些記錄,內容主要翻譯自RabbitMQ官網的Tutorials, 再加上我的一些個人理解。我將會用三篇文章來從RabbitMQ的Hello World介紹起,到最後的通過RabbitMQ實現RPC調用, 相信看完這三篇文章大家應該會對RabbitMQ的基本概念和使用有一定的瞭解。

說明:

  1. 由於RabbitMQ支持許多種語言的client,在這裏我使用的是Java語言的client。
  2. 所有的圖片均來自RabbitMQ官網。

 

目錄

Hello World

Work Queues

發佈/訂閱(Publish/Subscribe)

Routing

Topics

Remote procedure call (RPC)


Hello World

首先需要安裝RabbitMQ,關於RabbitMQ的安裝這裏就不贅述了,可以到RabbitMQ的官網去看相應的OS的安裝方法。 安裝完成後使用rabbitmq-server即可啓動RabbitMQ,RabbitMQ還提供了一個UI管理界面,本地默認的地址爲localhost:15672, 用戶名和密碼均爲guest。

安裝完成之後,按照慣例,先來完成一個簡單的Hello World的例子。 最簡單的一種消息發送的模型爲一個消息發送者(Producer)將消息發送到Queue中,另一端的消息接受者(Consumer)從Queue中接受消息, 大致模型如下圖所示:

RabbitMQ

先來看發送的代碼,新建一個類命名爲Send.java,代碼的第一步爲連接server

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("localhost");

Connection connection = factory.newConnection();

Channel channel = connection.createChannel();

connection抽象了socket的連接,並且爲我們處理了協議版本的協商、權限認證等等。這裏我們連接的是本地的中間件, 也就是localhost,接下來我們創建一個channel,這是大多數API完成任務的所在,也就是說我們的API操作基本都是通過channel來完成的。

channel.queueDeclare(QUEUE_NAME, false, false, false, null);

String message = "Hello World!";

channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

System.out.println(" [x] Sent '" + message + "'");

首先是通過channel來聲明一個queue,並且聲明queue的操作是冪等的,也即是說只有在這個queue不存在的情況下才會新創建一個queue。 這裏發送一個Hello World!的消息,實際傳遞的消息內容爲字節數組。

channel.close();

connection.close();

最後關閉channel和connection的連接,注意關閉的順序,是先關閉channel的連接,再關閉connection的連接。

完整的Send.java代碼

public class Send {



    private static final String QUEUE_NAME = "hello";



    public static void main(String[] args) {

        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("localhost");

        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();



        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        String message = "Hello World!";

        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

        System.out.println(" [x] Sent '" + message + "'");



        channel.close();

        connection.close();

    }



}

完成發送的代碼之後是接受消息的代碼,新建一個類爲Recv.java

public class Recv {



    private final static String QUEUE_NAME = "hello";



    public static void main(String[] argv) throws Exception {

      ConnectionFactory factory = new ConnectionFactory();

      factory.setHost("localhost");

      Connection connection = factory.newConnection();

      Channel channel = connection.createChannel();



      channel.queueDeclare(QUEUE_NAME, false, false, false, null);

      System.out.println(" [*] Waiting for messages. To exit press CTRL+C");



      Consumer consumer = new DefaultConsumer(channel) {

        @Override

        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)

            throws IOException {

          String message = new String(body, "UTF-8");

          System.out.println(" [x] Received '" + message + "'");

        }

      };

      channel.basicConsume(QUEUE_NAME, true, consumer);

    }

}

可以發現一開始的連接部分的代碼是相同的,在接收的時候我們也要聲明一個queue,注意這裏queue的名稱和之前發送消息聲明的queue的名稱必須是相同的, 否則就收不到消息了。

DefaultConsumer類實現了Consumer接口,由於發送消息是異步的,因此在這裏提供了一個callback來緩衝消息, 直到我們準備使用這些消息,最後分別運行Send.javaRecv.java,就能看到Hello World!消息了。

Work Queues

在第一部分的Hello World中通過一個命名的queue來傳遞消息,在這一部分,我們會創建Work Queue來將耗時的任務分發至多個worker。 假設一個消息就是一個耗時的任務,比如文件I/O等等,那麼可以通過幾個worker來共同完成這些工作。

RabbitMQ

在Web應用中這是非常有用的,因爲在一次非常短的HTTP請求窗口中完成一個非常複雜的任務是很困難的。

準備

這一部分是建立在上一部分Hello World的基礎之上的,我們將發送字符串來表示一些複雜的任務, 由於並沒有一些真實的複雜的工作,因此使用Thread.sleep()來模擬這是一個很耗時的任務, 並且在發送的字符串當中含有一個點號就表示這個任務需要耗時1秒,比如發送Hello...表示將要耗時3秒。

在前一部分的Send.java的基礎上做一些修改,得到一個新的類稱爲NewTask.java

String message = getMessage(argv);



channel.basicPublish("", "hello", null, message.getBytes());

System.out.println(" [x] Sent '" + message + "'");

getMessage方法爲從命令行中獲取參數

private static String getMessage(String[] strings){

    if (strings.length < 1)

        return "Hello World!";

    return joinStrings(strings, " ");

}



private static String joinStrings(String[] strings, String delimiter) {

    int length = strings.length;

    if (length == 0) return "";

    StringBuilder words = new StringBuilder(strings[0]);

    for (int i = 1; i < length; i++) {

        words.append(delimiter).append(strings[i]);

    }

    return words.toString();

}

我們之前的Recv.java也需要做一些變化,它需要模擬一些耗時的任務,消息內容中一個.表示1秒,並且它會處理消息, 我們稱它爲Worker.java

final Consumer consumer = new DefaultConsumer(channel) {

  @Override

  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

    String message = new String(body, "UTF-8");



    System.out.println(" [x] Received '" + message + "'");

    try {

      doWork(message);

    } finally {

      System.out.println(" [x] Done");

    }

  }

};

boolean autoAck = true; // acknowledgment is covered below

channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);

這裏有一個autoAck變量的作用在後面會提到。doWork方法就是模擬的耗時任務

private static void doWork(String task) throws InterruptedException {

    for (char ch: task.toCharArray()) {

        if (ch == '.') Thread.sleep(1000);

    }

}

循環發送

使用任務隊列其中的一個好處是可以非常方便的並行處理這些任務。如果我們在處理一些積壓的工作, 只需要增加更多的worker即可,非常容易擴展。

首先,來試試兩個worker實例的情況。很顯然,兩個worker都會接受到消息,但是具體的情況是怎麼樣的呢? 我們在控制檯啓動兩個實例,C1和C2表示兩個consumer,然後使用Producer來發送消息,一共發送五條消息,來看看具體的情況。

首先是第一個worker打印出的消息

[*] Waiting for messages. To exit press CTRL+C

[x] Received 'First message.'

[x] Received 'Third message...'

[x] Received 'Fifth message.....'

第二個worker打印出的消息

[*] Waiting for messages. To exit press CTRL+C

[x] Received 'Second message..'

[x] Received 'Fourth message....'

默認的,RabbitMQ會順序的把消息發送到下一個Consumer,上面打印出的消息也印證了這一點。 平均來說每個Consumer接收到的消息數量是相同的,這種發送消息的方式稱爲循環發送(round-robin), 思考下有三個或者更多Worker的情況。

消息接收(Message acknowledgment)

處理一個任務需要耗費幾秒鐘的時間。你也許想知道如果一個consumer在處理一個任務的時候只處理了一部分就掛了會出現什麼情況。 在我們現在的代碼下,一旦RabbitMQ將一個消息傳遞到consumer,它馬上會從內存中刪除這條消息, 也就是說如果殺掉了一個正在處理任務的worker,那麼將會失去所有的這個worker正在處理的所有消息, 同樣也會失去發送給這個worker但是還未處理的消息。

一般情況下,我們不希望丟失消息,如果某個worker掛了,能將消息發送給另一個worker來處理。 爲了確保消息不會丟失,RabbitMQ支持消息接收(message acknowledgments)。 當consumer確認收到某個消息,並且已經處理完成,RabbitMQ可以刪除它時,consumer會向RabbitMQ發送一個ack(nowledgement)

如果一個consumer掛了(channel關閉了、connection關閉了或者TCP連接斷了)而沒有發送ack,RabbitMQ就會知道這個消息沒有被完全處理, 將會對這條消息做re-queue處理。如果此時有另一個consumer連接,消息會被重新發送至另一個consumer。 使用這種方式可以保證消息不會丟失。

消息不會超時;RabbitMQ會在consumer掛了之後重新發送消息。即使處理消息耗時非常長也是沒有問題的。

消息接收是默認開啓的,在之前的例子中我們通過autoAck=true標誌顯式的關閉了它,true則表示自動接收,不需要發送ack。 現在是時候來開啓ack。當consumer處理完成之後,向rabbitMQ發送ack。

channel.basicQos(1); // accept only one unack-ed message at a time (see below)



final Consumer consumer = new DefaultConsumer(channel) {

  @Override

  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

    String message = new String(body, "UTF-8");



    System.out.println(" [x] Received '" + message + "'");

    try {

      doWork(message);

    } finally {

      System.out.println(" [x] Done");

      channel.basicAck(envelope.getDeliveryTag(), false);

    }

  }

};

boolean autoAck = false;

channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);

使用以上代碼能夠保證即使在一個worker處使消息的時候用CTRL+C來殺掉這個worker,也不會丟失消息。 在這個worker掛掉之後所有未接收(ack)的消息將被重新發送。

消息持久化

我們學習瞭如何在worker掛掉的情況下不丟消息,但是在RabbitMQ server停止之後消息還是會丟失。 如果不進行任何配置,在RabbitMQ退出或崩潰的時候,將會失去所有的queue和消息。 要保證在這種情況下消息不丟失需要做兩件事情:需要同時標誌queue和message是持久化的。

首先,需要確保RabbitMQ不會丟失queue

boolean durable = true;

channel.queueDeclare("task_queue", durable, false, false, null);

我們重新聲明一個queue(不能修改已經聲明爲不持久化的queue爲持久化),名字爲task_queue, 第二個布爾參數表示是否持久化的意思,這裏設置爲true, 包括consumer和producer聲明queue的時候都需要聲明durable爲true。現在,即使重啓RabbitMQ,task_queue這個queue也不會丟失了。

接下來我們將消息做持久化配置處理,通過設置MessageProperties(實現了BasicProperties)中的PERSISTENT_TEXT_PLAIN屬性。

channel.basicPublish("", "task_queue",

            MessageProperties.PERSISTENT_TEXT_PLAIN,

            message.getBytes());

公平分發(Fair dispatch)

在某種場景下有兩個worker,當所有奇數的消息處理起來都比較耗時,而偶數的消息處理起來都比較快, 這就會發生一個worker總是處於busy狀態,而另一個worker則總是處於空閒狀態,RabbitMQ並不知道這個情況, 仍然只是正常的發送消息。

出現這種情況的原因在於當消息在queue中的時候RabbitMQ只是發送這些消息而已,它不會去關注某個consumer未ack的消息的數量, 它只是盲目的將某個消息發送到某個consumer。

RabbitMQ

爲了處理這種情況我們可以使用basicQos方法來設置prefetchCount = 1。 這告訴RabbitMQy一次只給worker一條消息,換句話來說,就是直到worker發回ack,然後再向這個worker發送下一條消息。

int prefetchCount = 1;

channel.basicQos(prefetchCount);

完整的NewTask.java代碼

public class NewTask {



  private static final String TASK_QUEUE_NAME = "task_queue";



  public static void main(String[] argv)

                      throws java.io.IOException {



    ConnectionFactory factory = new ConnectionFactory();

    factory.setHost("localhost");

    Connection connection = factory.newConnection();

    Channel channel = connection.createChannel();



    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);



    String message = getMessage(argv);



    channel.basicPublish( "", TASK_QUEUE_NAME,

            MessageProperties.PERSISTENT_TEXT_PLAIN,

            message.getBytes());

    System.out.println(" [x] Sent '" + message + "'");



    channel.close();

    connection.close();

  }     

  //...

}

Worker.java

public class Worker {

  private static final String TASK_QUEUE_NAME = "task_queue";



  public static void main(String[] argv) throws Exception {

    ConnectionFactory factory = new ConnectionFactory();

    factory.setHost("localhost");

    final Connection connection = factory.newConnection();

    final Channel channel = connection.createChannel();



    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");



    channel.basicQos(1);



    final Consumer consumer = new DefaultConsumer(channel) {

      @Override

      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {

        String message = new String(body, "UTF-8");



        System.out.println(" [x] Received '" + message + "'");

        try {

          doWork(message);

        } finally {

          System.out.println(" [x] Done");

          channel.basicAck(envelope.getDeliveryTag(), false);

        }

      }

    };

    boolean autoAck = false;

    channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);

  }



  private static void doWork(String task) {

    for (char ch : task.toCharArray()) {

      if (ch == '.') {

        try {

          Thread.sleep(1000);

        } catch (InterruptedException _ignored) {

          Thread.currentThread().interrupt();

        }

      }

    }

  }

}

 

發佈/訂閱(Publish/Subscribe)

爲了說明這種模式,我們將創建一個簡單的log系統,它由兩部分組成——第一部分負責發送log消息,第二部分負責接收並且將消息打印出來。 在我們的log系統中每個運行着的接收程序都會接收到消息,在這種方式下我們可以有一個consumer負責將log持久化到磁盤, 同時由另一個consumer來將log打印到控制檯。本質上,發送log消息是對所有消息接收者的廣播。

Exchange

在之前的部分我們都是通過queue來發送和接收消息,現在是時候來介紹RabbitMQ完整的消息模型了。先讓我們來快速地回顧一下之前介紹過的幾個概念:

  • producer是用戶應用負責發送消息
  • queue是存儲消息的緩衝(buffer)
  • consumer是用戶應用負責接收消息

RabbitMQ的消息模型的核心思想是producer永遠不會直接發送任何消息到queue中,實際上,在很多情況下producer根本不知道一條消息是否被髮送到了哪個queue中。

在RabbitMQ中,producer只能將消息發送到一個exchange中。要理解exchange也非常簡單,它一邊負責接收producer發送的消息, 另一邊將消息推送到queue中。exchange必須清楚的知道在收到消息之後該如何進行下一步的處理,比如是否應該將這條消息發送到某個queue中? 還是應該發送到多個queue中?還是應該直接丟棄這條消息等等。用官方文檔上的一張圖可以更清楚地瞭解RabbitMQ的消息模型。

RabbitMQ Exchange

RabbitMQ中的exchange類型有這麼幾種:directtopicheaders以及fanout。這一小節將會主要介紹最後一種類型——fanout。 使用RabbitMQ的client來創建一個fanout類型的exchange,命令爲logs

channel.exchangeDeclare("logs","fanout");

fanout類型的exchange非常簡單,從名字也可以猜測出來,它會向所有的queue廣播所有收到的消息。這正是我們的log系統需要的。

在之前的部分我們對exchange一無所知,但是我們仍然可以將消息發送到queue中,這是因爲我們使用了默認的exchange,在代碼中使用空字符串(“”)表示。

channel.basicPublish("", "hello", null, message.getBytes());

第一個參數表示exchange的名字,使用空字符串表示使用默認的無名的exchange:如果有的話,消息將根據routingKey被髮送到指定的queue中。

現在,可以將消息發送到之前已經聲明過的exchange中

channel.basicPublish( "logs", "", null, message.getBytes());

臨時隊列

在之前的小節中使用queue都是指定了名字的(hello和task_queue),給queue命名是非常重要的,因爲我們需要將的workers指定到相同的queue上, 並且在consumer與producer之間也需要指定相同的queue。

但是這對我們的log系統來說不是必須的,我們需要監聽所有的log消息,而不是其中的一部分。我們也只關心現在的消息而不關注以前的消息, 爲了解決這個問題我們需要做兩件事情。

首先,無論何時連接到RabbitMQ server上都需要一個新的、空的queue。爲了做到這一點需要能夠使用一個隨機的名字來創建queue, 更好的方式是由server來爲我們選擇一個隨機的名字。

其次,一旦我們與consumer斷開連接,queue應該被自動刪除。

在Java client中,提供了一個無參數的queueDeclare()方來來創建一個非持久化的、獨有的並且是自動刪除的已命名的queue。

String queueName = channel.queueDeclare().getQueue();

queueName會包含一個隨機的queue名字,可能看起來類似amq.gen-JzTY20BRgKO-HjmUJj0wLg

綁定

binding

我們已經創建了一個fanout類型的exchange和一個queue。現在我們需要告訴exchange將消息發送到我們的queue中。 這種exchange和queue的關係稱爲綁定(binding)。

channel.queueBind(queueName, "logs", "");

之後logs exchange將會把消息發送到我們的queue中。

完整的EmitLog.java代碼

public class EmitLog {



    private static final String EXCHANGE_NAME = "logs";



    public static void main(String[] argv)

                  throws java.io.IOException {



        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("localhost");

        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();



        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");



        String message = getMessage(argv);



        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());

        System.out.println(" [x] Sent '" + message + "'");



        channel.close();

        connection.close();

    }

    //...

}

可以看到,在創建連接之後聲明exchange。這一步是必要的,因爲將消息發送到一個不存在的exchange是被禁止的。

如果還沒有queue被綁定到exchange上,那麼消息將會丟失,但這對我們來說是可以接收的,如果沒有consumer正在監聽消息, 那麼可以安全的丟棄這些消息。

完整的ReceiveLogs.java代碼

public class ReceiveLogs {

  private static final String EXCHANGE_NAME = "logs";



  public static void main(String[] argv) throws Exception {

    ConnectionFactory factory = new ConnectionFactory();

    factory.setHost("localhost");

    Connection connection = factory.newConnection();

    Channel channel = connection.createChannel();



    channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

    String queueName = channel.queueDeclare().getQueue();

    channel.queueBind(queueName, EXCHANGE_NAME, "");



    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");



    Consumer consumer = new DefaultConsumer(channel) {

      @Override

      public void handleDelivery(String consumerTag, Envelope envelope,

                                 AMQP.BasicProperties properties, byte[] body) throws IOException {

        String message = new String(body, "UTF-8");

        System.out.println(" [x] Received '" + message + "'");

      }

    };

    channel.basicConsume(queueName, true, consumer);

  }

}

Routing

在上一小節中我們構建了一個簡單的log系統,可以向許多接收者廣播消息。在這一小節中我們將會對此增加一個特性——可以只訂閱消息的一部分。 舉例來說,可以只將critical級別的錯誤日誌持久化到磁盤,同時又能夠將所有的消息打印到控制檯。

綁定(binds)

在前一小節中已經介紹瞭如何創建綁定

channel.queueBind(queueName, EXCHANGE_NAME, "");

綁定是exchange和queue之間的一種關係,這可以簡單的理解爲:這個queue對這個exchange中的消息感興趣。

綁定可以使用一個額外的routingKey參數,爲了避免和basic_publish參數混淆,我們稱它爲binding key。 我們可以這樣來使用key創建一個綁定:

channel.queueBind(queueName, EXCHANGE_NAME, "black");

binding key的含義取決於不同的exchange類型,我們之前使用的fanout類型會直接忽略這個值。

Direct exchange

我們之前的log消息系統將所有的消息廣播到所有的consumer中。我們需要對此進行擴展,允許根據log的級別進行消息的過濾。 之前使用的fanout類型的exchange,沒有提供給我們類似的靈活性——它只能簡單的廣播所有的消息。

在這裏將會使用direct類型的exchange作爲代替。direct類型的exchange的路由算法很簡單——消息將會被傳遞到與它的routing key完全相同的 binding key的queue中。

還是使用一張圖來說明:

Routing

在圖中可以看到,有兩個queue被綁定到了direct類型的exchange X上。第一個queue使用bing key orange綁定,第二個queue使用了兩個binding key, 分別爲blackgreen

在這樣的情況下,使用routing key爲orange發送的消息將會被路由到queue Q1中,使用routing key爲black或者green的將會被路由到Q2中。 所有其他的消息將會被丟棄。

多重綁定(Multiple bindings)

Multiple bindings

將多個queue使用相同的binding key進行綁定也是可行的。在我們的例子中可以在X和Q1中間增加一個binding key black。 在這種情況下,direct類型的exchange的行爲將和fanout類似,它會向所有匹配的queue進行廣播,使用routing key爲black發送的消息將會同時被Q1Q2接收。

發送log

我們將會爲log系統使用這種模型。使用direct類型的exchange代替fanout。我們將會通過routing key提供log的嚴重級別。 使用這種方式可以選擇不同的log嚴重級別來接收消息。首先來看發送log的部分。

創建一個exchange:
 

channel.exchangeDeclare(EXCHANGE_NAME, "direct");

已經準備好發送消息:

channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());

爲了簡單起見,我們假設日誌的級別只會爲’info,’warning’,’error’三者中的一個。

訂閱

接受消息部分將會和上一小節相同,除了一個例外——我們將會爲每個感興趣的嚴重級別創建新的綁定。

String queueName = channel.queueDeclare().getQueue();



for(String severity : argv){

  channel.queueBind(queueName, EXCHANGE_NAME, severity);

}

完整的EmitLogDirect.java代碼

public class EmitLogDirect {



    private static final String EXCHANGE_NAME = "direct_logs";



    public static void main(String[] argv)

                  throws java.io.IOException {



        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("localhost");

        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();



        channel.exchangeDeclare(EXCHANGE_NAME, "direct");



        String severity = getSeverity(argv);

        String message = getMessage(argv);



        channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());

        System.out.println(" [x] Sent '" + severity + "':'" + message + "'");



        channel.close();

        connection.close();

    }

    //..

}

完整的ReceiveLogsDirect.java代碼

public class ReceiveLogsDirect {



  private static final String EXCHANGE_NAME = "direct_logs";



  public static void main(String[] argv) throws Exception {

    ConnectionFactory factory = new ConnectionFactory();

    factory.setHost("localhost");

    Connection connection = factory.newConnection();

    Channel channel = connection.createChannel();



    channel.exchangeDeclare(EXCHANGE_NAME, "direct");

    String queueName = channel.queueDeclare().getQueue();



    if (argv.length < 1){

      System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]");

      System.exit(1);

    }



    for(String severity : argv){

      channel.queueBind(queueName, EXCHANGE_NAME, severity);

    }

    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");



    Consumer consumer = new DefaultConsumer(channel) {

      @Override

      public void handleDelivery(String consumerTag, Envelope envelope,

                                 AMQP.BasicProperties properties, byte[] body) throws IOException {

        String message = new String(body, "UTF-8");

        System.out.println(" [x] Received '" + envelope.getRoutingKey() + "':'" + message + "'");

      }

    };

    channel.basicConsume(queueName, true, consumer);

  }

}

可以在命令行中傳入感興趣的日誌的嚴重級別來綁定。

Topics

在log系統中可能不只是基於不同的日誌級別作訂閱,也可能會基於日誌的來源。你也許聽過Unix下名爲syslog的工具, 它把日誌按照嚴重級別(info/warn/crit…)和設備(auth/cron/ker…)進行路由。

這會給我們許多的靈活性,也許我們只想監聽’cron’中的’critical’級別的錯誤日誌,以及所有’kern’中的日誌。 爲了實現這種日誌系統,我們需要學習一個更復雜的topic類型的exchange。

Topic exchange

發送到topic exchange中的消息不能有一個任意的routing_key——它必須是一個使用點分隔的單詞列表。單詞可以是任意的, 但是通常會指定消息的一些特定。一些有效的routing key例子:”stock.usd.nyse”,”nyse.vmw”,”quick.orange.rabbit”。 routing key的長度限制爲255個字節數。

binding key也必須是相同的形式。topic exchange背後的邏輯類似於direct——一條使用特定的routing key發送的消息將會被傳遞至所有使用與該routing key相同的binding key進行綁定的隊列中。 然而,對binding key來說有兩種特殊的情況:

  1. *(star)可以代替任意一個單詞
  2. #(hash)可以代替0個或多個單詞

使用一張圖可以很簡單地來說明:

topic

在圖中,我們將要發送被描述的動物的消息。消息的routing key將由三個單詞組成(通過兩個點分隔)。routing key中的第一個單詞將描述速度, 第二個是顏色,第三個是物種:"<speed>.<colour>.<species>"

我們創建三個綁定:Q1使用binding key"*.orange.*"來綁定,Q2使用"*.*.rabbit"以及lazy.#綁定。

這些綁定可以被總結爲:

  • Q1對所有橘色的的動物感興趣
  • Q2想要接收所有關於兔子的消息以及所有關於lazy的動物的消息

一條使用routing key"quick.orange.rabbit"發送的消息將被同時傳遞到兩個隊列中。消息"lazy.orange.elephant"同樣如此。 另一方面,"quick.orange.fox"只會被第一個queue接收,"lazy.brown.fox"只會被第二個queue接收。 "lazy.pink.rabbit"只會被傳遞到Q2一次,即使它對兩個binding key都匹配。"quick.brown.fox"與兩個queue的binding key都不匹配, 因此將被丟棄。

如果打破我們的約定,使用一個單詞或者四個單詞的routing key例如"orange""quick.orange.male.rabbit"發送消息將會發生什麼? 這些消息不會匹配任何綁定,因此會丟失。

但是對於"lazy.orange.male.rabbit",即使它有四個單詞,但是它與第二個queue的binding key匹配,因此將會被髮送到第二個queue中。

當一個queue使用"#"(hash)作爲binding key,那麼它將會接收所有的消息,忽略routing key,就好像使用了fanout exchange。 當特殊字符”*“(star)和”#“(hash)在綁定中沒有用到,topic exchange將會與direct exchange的行爲相同。

瞭解了topic exchange之後,我們將它用在我們的log系統中,我們定義的routing key將會有兩個單詞組成:"<facility>.<severity>"

完成的EmitLogTopic.java

public class EmitLogTopic {



    private static final String EXCHANGE_NAME = "topic_logs";



    public static void main(String[] argv)

                  throws Exception {



        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("localhost");

        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();



        channel.exchangeDeclare(EXCHANGE_NAME, "topic");



        String routingKey = getRouting(argv);

        String message = getMessage(argv);



        channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());

        System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");



        connection.close();

    }

    //...

}

完整的ReceiveLogsTopic.java:

public class ReceiveLogsTopic {

  private static final String EXCHANGE_NAME = "topic_logs";



  public static void main(String[] argv) throws Exception {

    ConnectionFactory factory = new ConnectionFactory();

    factory.setHost("localhost");

    Connection connection = factory.newConnection();

    Channel channel = connection.createChannel();



    channel.exchangeDeclare(EXCHANGE_NAME, "topic");

    String queueName = channel.queueDeclare().getQueue();



    if (argv.length < 1) {

      System.err.println("Usage: ReceiveLogsTopic [binding_key]...");

      System.exit(1);

    }



    for (String bindingKey : argv) {

      channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);

    }



    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");



    Consumer consumer = new DefaultConsumer(channel) {

      @Override

      public void handleDelivery(String consumerTag, Envelope envelope,

                                 AMQP.BasicProperties properties, byte[] body) throws IOException {

        String message = new String(body, "UTF-8");

        System.out.println(" [x] Received '" + envelope.getRoutingKey() + "':'" + message + "'");

      }

    };

    channel.basicConsume(queueName, true, consumer);

  }

}

運行的時候從命令行中輸入binding key來進行綁定,接收不同的消息。

Remote procedure call (RPC)

在第二小節中我們學習瞭如何使用Work Queues來在多個workers中分發耗時的任務。但是如果我們需要調用遠程計算機上的一個函數並等待結果返回呢? 這就是另外一個故事了。這種模式通常稱爲遠程過程調用或RPC。

在這一小節我們將使用RabbitMQ來構建一個RPC系統:一個客戶端和一個可擴展的RPC服務器。由於我們沒有實際的耗時任務用來分發, 因此我們將創建一個虛擬的RPC服務返回Fibonacci數。

Client interface

爲了說明RPC服務是如何使用的,我們將創建一個簡單的客戶端類。它將暴露一個名爲call的方法發送一次RPC請求並且阻塞直到結果返回:

FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();

String result = fibonacciRpc.call("4");

System.out.println( "fib(4) is " + result);

Callback queue

使用RabbitMQ來進行RPC是非常簡單的。客戶端發送一個請求到服務端,服務端接收後返回響應的消息。爲了接收到響應的消息,我們需要在請求中發送一個callback 的queue地址。我們可以使用默認的queue(在Java的client中它是exclusive的)。

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類型的編碼。例如使用JSON的話就這樣設置屬性:application/json
  • replyTo:一般用來命名一個回調queue。
  • correlationId:用來關聯RPC的請求和響應。

我們需要導入新的類:

import com.rabbitmq.client.AMQP.BasicProperties;

Correlation Id

在之前的方法中我們建議爲每個RPC請求創建一個回調queue。這顯得有點影響性能,幸運的是有一種更好的方式——每個客戶端只創建一個回調queue。 但這產生了一個新問題,無法將相應的Response和Request對應起來。這個時候就需要用到correlationId屬性。對於每個請求它都將有一個唯一的值。 當我們在回調queue中接收到消息之後,檢查該屬性,看是否與Request匹配。如果是一個未知的correlationId值,那麼我們可以安全的忽略這條消息, 因爲它不屬於我們的請求。

你也許會問,爲什麼我們應該忽略回調queue中未知的消息而不是拋出異常?這是因爲服務端可能會出現競爭條件。儘管不太常見,但是也有可能RPC server在發送響應後掛了, 並且也沒有接收到客戶端發送的ack。如果發生了這種情況,RPC server在重啓後將會重新處理這個請求。這就是爲什麼在客戶端我們需要優雅的處理重複的響應, RPC應該是冪等的。

Summary

RPC

我們的RPC整個過程是這樣的:

  1. 當客戶端啓動,它創建一個匿名的並且是exclusive的回調queue。
  2. 在一次RPC請求中,客戶端發送的消息有兩個屬性:replyTo,放置的是回調queue的信息。correlationId,放置的是每個請求唯一的值。
  3. 請求被髮送到一個rpc_queue中。
  4. RPC服務端在queue的另一端等待請求。當請求到來時,它處理任務並將消息的結果發送回客戶端,使用replyTo中設置的queue。
  5. 客戶端在回調queue中等待響應的數據,當消息出現時,它先檢查correlationId屬性。如果匹配的話就將結果返回到應用中。

最後來看一下完整的代碼實現。

Fibonacci函數:

private static int fib(int n) throws Exception {

    if (n == 0) return 0;

    if (n == 1) return 1;

    return fib(n-1) + fib(n-2);

}

完整的RPCServer.java代碼

private static final String RPC_QUEUE_NAME = "rpc_queue";



ConnectionFactory factory = new ConnectionFactory();

factory.setHost("localhost");



Connection connection = factory.newConnection();

Channel channel = connection.createChannel();



channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);



channel.basicQos(1);



QueueingConsumer consumer = new QueueingConsumer(channel);

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

    int n = Integer.parseInt(message);



    System.out.println(" [.] fib(" + message + ")");

    String response = "" + fib(n);



    channel.basicPublish( "", props.getReplyTo(), replyProps, response.getBytes());



    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

}

server端的代碼非常直觀:

  • 首先創建一個連接、channel和聲明一個queue。
  • 我們也許想要運行不止一個服務端進程。爲了在多個server間做到負載均衡,通過channel.basicQos設置prefetchCount
  • 我們使用basicConsume來進入queue。然後使用無限循環來等待請求的消息,處理之後再返回響應。

完整的RPCClient.java代碼

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");

    connection = factory.newConnection();

    channel = connection.createChannel();



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

    consumer = new QueueingConsumer(channel);

    channel.basicConsume(replyQueueName, true, consumer);

}



public String call(String message) throws Exception {

    String response = null;

    String corrId = java.util.UUID.randomUUID().toString();



    BasicProperties props = new BasicProperties

                                .Builder()

                                .correlationId(corrId)

                                .replyTo(replyQueueName)

                                .build();



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



    while (true) {

        QueueingConsumer.Delivery delivery = consumer.nextDelivery();

        if (delivery.getProperties().getCorrelationId().equals(corrId)) {

            response = new String(delivery.getBody());

            break;

        }

    }



    return response;

}



public void close() throws Exception {

    connection.close();

}

客戶端代碼有一點點的複雜:

  • 我們創建連接和channel,以及聲明一個exclusive的回調queue用來接收響應的消息。
  • 訂閱回調queue,這樣就可以接收到RPC服務端響應的消息。
  • call方法發出一個RPC請求。
  • 我們首先生成一個唯一的correlationId數字並且保存它——在while循環中使用它來匹配相應的response。
  • 下一步,發送請求的消息,使用兩個屬性:replyTocorrelationId
  • 之後就是等待響應的消息返回。
  • 在while循環中做了一些簡單的工作,檢查響應的消息的correlationId是否與Request相匹配。如果是的話,則保存響應。
  • 最終向用戶返回響應。

發送客戶端請求:

RPCClient fibonacciRpc = new RPCClient();



System.out.println(" [x] Requesting fib(30)");

String response = fibonacciRpc.call("30");

System.out.println(" [.] Got '" + response + "'");



fibonacciRpc.close();

這樣就通過RabbitMQ簡單的實現了RPC的通信。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章