RabbitMq知識整理以及在java語言下的簡單例項

qq1076472549發表於2018-08-09

簡單介紹:

使用Erlang語言編寫,主用於在分散式系統中儲存轉發訊息,使用AMQP協議(ConnectionQueue,Channel,Exchange,Binding等概念,下面在mq介紹中解釋),相比於redis而言支援事物保證訊息可靠傳播,效能高,叢集穩定等優勢,但是redis更加輕量級,高敏感性。

一、元件介紹:

ConnectionFactory、Connection、Channel都是RabbitMQ對外提供的API中最基本的物件。Connection是RabbitMQ的socket連結,它封裝了socket協議相關部分邏輯。ConnectionFactory為Connection的製造工廠。 Channel是我們與RabbitMQ打交道的最重要的一個介面,我們大部分的業務操作是在Channel這個介面中完成的,包括定義Queue、定義Exchange、繫結Queue與Exchange、釋出訊息等。

1.Queue

Queue(佇列)是RabbitMQ的內部物件,用於儲存訊息;

多個消費者可以訂閱同一個Queue,這時Queue中的訊息會被平均分攤給多個消費者進行處理,而不是每個消費者都收到所有的訊息並處理。

2.Exchange

生產者將訊息傳送到Exchange(交換器,下圖中的X),由Exchange將訊息路由到一個或多個Queue中(或者丟棄)。

 

3.routing key

生產者在將訊息傳送給Exchange的時候,一般會指定一個routing key,來指定這個訊息的路由規則,而這個routing key需要與Exchange Type及binding key聯合使用才能最終生效。 在Exchange Type與binding key固定的情況下(在正常使用時一般這些內容都是固定配置好的),我們的生產者就可以在傳送訊息給Exchange時,通過指定routing key來決定訊息流向哪裡。

4.Binding

RabbitMQ中通過Binding將Exchange與Queue關聯起來,這樣RabbitMQ就知道如何正確地將訊息路由到指定的Queue了。

在繫結(Binding)Exchange與Queue的同時,一般會指定一個binding key;消費者將訊息傳送給Exchange時,一般會指定一個routing key;當binding key與routing key相匹配時,訊息將會被路由到對應的Queue中。 在繫結多個Queue到同一個Exchange的時候,這些Binding允許使用相同的binding key。 binding key 並不是在所有情況下都生效,它依賴於Exchange Type,比如fanout型別的Exchange就會無視binding key,而是將訊息路由到所有繫結到該Exchange的Queue。

5.Exchange Types

exchange怎麼和佇列queue繫結呢,按照什麼規則呢:

RabbitMQ常用的Exchange Type有fanout、direct、topic:

5.1 fanout

fanout型別的Exchange路由規則非常簡單,它會把所有傳送到該Exchange的訊息路由到所有與它繫結的Queue中。

5.2 direct

direct型別的Exchange路由規則也很簡單,它會把訊息路由到那些binding key與routing key完全匹配的Queue中。

5.3 topic

前面講到direct型別的Exchange路由規則是完全匹配binding key與routing key,但這種嚴格的匹配方式在很多情況下不能滿足實際業務需求。topic型別的Exchange在匹配規則上進行了擴充套件,它與direct型別的Exchage相似,也是將訊息路由到binding key與routing key相匹配的Queue中,但這裡的匹配規則有些不同,它約定:

  • routing key為一個句點號“. ”分隔的字串

  • binding key與routing key一樣也是句點號“. ”分隔的字串

  • binding key中可以存在兩種特殊字元“*”與“#”,用於做模糊匹配,其中“*”用於匹配一個單詞,“#”用於匹配多個單詞(可以是零個)

二、生產訊息(confirm模式的程式設計實現):

RabbitMq類:

public class RabbitMq
{
    public static final String exchangeName = "exchange";
    
    public static final String queueName = "queue";
    
    public static final String routingKey = "routingKey";
    
    public static final String bindingKey = "routingKey";
    
    public static void main(String[] args)
    {
        
        int count = 1;
        
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setPort(5672);
        
        // 建立生產者
        Sender producer = new Sender(factory, count, exchangeName, queueName, routingKey, bindingKey);
        producer.run();
    }
}

 

傳送訊息類:

public class Sender
{
    private ConnectionFactory factory;
    
    private int count;
    
    private String exchangeName;
    
    private String queueName;
    
    private String routingKey;
    
    private String bindingKey;
    
    public Sender(ConnectionFactory factory, int count, String exchangeName, String queueName, String routingKey,
        String bindingKey)
    {
        this.factory = factory;
        this.count = count;
        this.exchangeName = exchangeName;
        this.queueName = queueName;
        this.routingKey = routingKey;
        this.bindingKey = bindingKey;
    }
    
    public void run()
    {
        Channel channel = null;
        try
        {
            Connection connection = factory.newConnection();
            channel = connection.createChannel();
            // 建立exchange,採用direct方式完全匹配
            channel.exchangeDeclare(exchangeName, "direct", true, false, null);
            // 建立佇列,true為持久化訊息
            channel.queueDeclare(queueName, true, false, false, null);
            // 繫結exchange和queue
            channel.queueBind(queueName, exchangeName, bindingKey);
            channel.confirmSelect();
            // 傳送持久化訊息
            for (int i = 0; i < count; i++)
            {
                // 第一個引數是exchangeName(預設情況下代理伺服器端是存在一個""名字的exchange的,
                // 因此如果不建立exchange的話我們可以直接將該引數設定成"",如果建立了exchange的話
                // 我們需要將該引數設定成建立的exchange的名字),第二個引數是路由鍵
                channel.basicPublish(exchangeName,
                    routingKey,
                    MessageProperties.PERSISTENT_BASIC,
                    ("第" + (i + 1) + "條訊息").getBytes());
            }
            long start = System.currentTimeMillis();
            channel.addConfirmListener(new ConfirmListener() //使用listener非同步確認效率高,還有序列confirm,批量序列效率較  低,程式設計簡單
            {
                
                @Override
                public void handleNack(long deliveryTag, boolean multiple)
                    throws IOException
                {
                    System.out.println("nack: deliveryTag = " + deliveryTag + " multiple: " + multiple);
                }
                
                @Override
                public void handleAck(long deliveryTag, boolean multiple)
                    throws IOException
                {
                    System.out.println("ack: deliveryTag = " + deliveryTag + " multiple: " + multiple);
                }
            });
            System.out.println("執行ConfirmListener耗費時間: " + (System.currentTimeMillis() - start) + "ms");
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

三,消費者程式碼:

public class ConsumerTest
{
    /**
     * 訂閱方式其實是向queue註冊consumer,通過rpc向queue server傳送註冊consumer的訊息,
     * rabbitMQ Server在收到訊息後,根據訊息的內容型別判斷這是一個訂閱訊息,
                             這樣當MQ 中queue有訊息時,會自動把訊息通過該socket(長連線)通道傳送出去。
     */
    public static void main(String[] args) throws IOException, ShutdownSignalException,
    ConsumerCancelledException, InterruptedException, TimeoutException {  

        // 建立連結工廠
        ConnectionFactory connFac = new ConnectionFactory() ;  

        //預設連結的主機名,RabbitMQ-Server安裝在本機,所以可以直接用127.0.0.1
        connFac.setHost("127.0.0.1");  

        //建立連結    
        Connection conn = connFac.newConnection() ;  

        //建立資訊管道  
        final Channel channel = conn.createChannel() ;  

        //定義Queue名稱  
        String queueName = RabbitMq.queueName;  
        //1.佇列名2.是否持久化,3是否侷限與連結,4不再使用是否刪除,5其他的屬性  是否持久化需要和生成者佇列設定的一樣否則報錯
        channel.queueDeclare(queueName, true, false, false, null) ;  

        // 同一時刻伺服器只會發一條訊息給消費者(能者多勞模式)
        channel.basicQos(1);
        //上面的部分,與Test01是一樣的  

        //宣告一個消費者,配置好獲取訊息的方式  
//       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("Customer Received '" + message + "'");
//            }  
//        };    
      
        boolean autoAck = false;//手動確認ACk
 
        //channel.basicConsume(queueName, autoAck, consumer) ;
        channel.basicConsume(queueName, autoAck, "myConsumerTag",
             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("Customer Received '" + message + "'");
                     long deliveryTag = envelope.getDeliveryTag();
                     // (process the message components here ...)
                     channel.basicAck(deliveryTag, false);
                 }
             });
    }
}

程式碼親測可以執行。並且持久化成功到本地,需要多個消費者可以用多執行緒實現。本文只作為互相學習使用,有問題歡迎大家指點!

 

 

 

 

 

 

 

 

 

 

 

相關文章