Rabbit MQ 進階

消息應答與消息持久化

消息應答

boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);

boolean autoAck = true;(自動確認模式)

一旦 Rabbit MQ 將消息分發給消費者,就會從內存中刪除,這種情況下,如果殺死正在執行的消費者,就會丟失正在處理的消息

boolean autoAck = false;(手動模式)

如果有一個消費者掛掉,就會交付給其它消費者,Rabbit MQ 支持消息應答,消費者發送一個消息應答,告訴 Rabbit MQ 這個消息我已經處理完成,你可以刪了,然後 Rabbit MQ 就刪除內存中的消息

消息應答默認是打開的,false

Message acknowledgment

大家想想如果我們的 Rabbit MQ 掛了,我們的消息仍然會丟失

消息的持久化

boolean durable = false;
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

我們將程序中的 boolean durable = false; 改成 true; 是不可以的,儘管代碼是正確的,它也不會運行成功,因爲我們已經定義了一個叫 work_queue 的消息隊列,這個 queue 是未持久化的,Rabbit MQ 不允許重新定義(不同參數)一個已存在的隊列

訂閱模式 publish/subscribe

模型

在這裏插入圖片描述

解讀:

1.一個生產者,多個消費者

2.每一個消費者都有自己的隊列

3.生產者沒有直接把消息發送到隊列 而是發到了交換機 轉發器 exchange

4.每個隊列都要綁定到交換機上

5.生產者發送的消息,經過交換機,到達隊列,就能實現一個消息被多個消費者消費

註冊 -> 郵件 -> 短信

生產者

public class Send {

    private final static String EXCHANGE_NAME = "exchange_fanout";

    public static void main(String[] args) throws Exception {
        // 獲取一個連接
        Connection connection = ConnectionUtils.getConnection();
        // 從連接中獲取一個通道
        Channel channel = connection.createChannel();

        // 聲明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        // 消息內容
        String message = "hello ps!";
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println("[x] Send '" + message + "'");

        channel.close();
        connection.close();
    }

}

在這裏插入圖片描述

消息哪去了???丟失了!!!因爲交換機沒有存儲的能力,在 Rabbit MQ 中只有隊列只有存儲能力,因爲這時候還沒有隊列綁定到交換機,所以數據丟失了

消費者 1

public class Receive1 {

    private final static String QUEUE_NAME = "queue_work1";

    private final static String EXCHANGE_NAME = "exchange_fanout";

    public static void main(String[] args) throws Exception {
        // 獲取一個連接
        Connection connection = ConnectionUtils.getConnection();
        // 從連接中獲取一個通道
        Channel channel = connection.createChannel();

        // 創建隊列聲明
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 綁定隊列到交換機
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 同一時刻服務器只會發一條消息給消費者
        channel.basicQos(1);

        // 保證一次只分發一個
        channel.basicQos(1);

        // 定義隊列的消費者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            /**
             * 獲取到到達的消息
             * @param consumerTag
             * @param envelope
             * @param properties
             * @param body
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[1] recv msg:" + msg);

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("[1] done");
                    // 手動回執
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };

        // 監聽隊列,自動應答改爲 false
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }
}

在這裏插入圖片描述

消費者 2

public class Receive2 {

    private final static String QUEUE_NAME = "queue_work2";

    private final static String EXCHANGE_NAME = "exchange_fanout";

    public static void main(String[] args) throws Exception {
        // 獲取一個連接
        Connection connection = ConnectionUtils.getConnection();
        // 從連接中獲取一個通道
        Channel channel = connection.createChannel();

        // 創建隊列聲明
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 綁定隊列到交換機
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 同一時刻服務器只會發一條消息給消費者
        channel.basicQos(1);

        // 保證一次只分發一個
        channel.basicQos(1);

        // 定義隊列的消費者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            /**
             * 獲取到到達的消息
             * @param consumerTag
             * @param envelope
             * @param properties
             * @param body
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[2] recv msg:" + msg);

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("[2] done");
                    // 手動回執
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };

        // 監聽隊列,自動應答改爲 false
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }

}

Exchange(交換機、轉發器)

一方面是接收生產者的消息,另一方面是向隊列推送消息

匿名轉發 “”

Fanout(不處理路由鍵)

在這裏插入圖片描述

Direct(處理路由鍵)

在這裏插入圖片描述

路由模式

模型

在這裏插入圖片描述

生產者

public class Send {

    private static final String EXCHANGE_NAME = "exchange_direct";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        String msg = "hello direct!";

        String routingKey = "error";
        channel.basicPublish(EXCHANGE_NAME, routingKey, null, msg.getBytes());

        System.out.println("send " + msg);

        channel.close();
        connection.close();
    }

}

消費者 1

public class Receive1 {

    private static final String EXCHANGE_NAME = "exchange_direct";
    private static final String QUEUE_NAME = "queue_direct_1";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

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

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[1] recv msg:" + msg);

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("[1] done");
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };

        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }

}

消費者 2

public class Receive2 {

    private static final String EXCHANGE_NAME = "exchange_direct";
    private static final String QUEUE_NAME = "queue_direct_2";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

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

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "warning");

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[2] recv msg:" + msg);

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("[2] done");
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };

        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }

}

Topic exchange

將路由鍵和某模式匹配

在這裏插入圖片描述
# 匹配一個或多個

* 匹配一個

Goods.#

模型

在這裏插入圖片描述

商品:發佈 修改 刪除 查詢

生產者

public class Send {

    private final static String EXCHANGE_NAME = "exchange_topic";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        String msgString = "商品...";
        channel.basicPublish(EXCHANGE_NAME, "goods.add", null, msgString.getBytes());
        System.out.println("---send " + msgString);

        channel.close();
        connection.close();
    }

}

消費者 1

public class Receive1 {

    private static final String EXCHANGE_NAME = "exchange_topic";
    private static final String QUEUE_NAME = "queue_topic_1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.add");

        channel.basicQos(1);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[1] recv msg:" + msg);

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("[1] done");
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };

        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }

}

消費者 2

public class Receive2 {

    private static final String EXCHANGE_NAME = "exchange_topic";
    private static final String QUEUE_NAME = "queue_topic_2";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[2] recv msg:" + msg);

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("[2] done");
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };

        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }

}

Rabbit MQ 的消息確認機制(事務 + confirm)

在 Rabbit MQ 中,我們可以持久化數據解決 Rabbit MQ 服務器異常的數據丟失問題

問題:生產者將消息發送出去之後,消息到底有沒有到達 Rabbit MQ 服務器,默認的情況是不知道的

兩種方式:

1.AMQP 實現了事務機制

2.Confirm 模式

事務機制

txSelect txCommit txRollBack

txSelect:用戶將當前 channel 設置成 transaction 模式

txCommit:用於提交事務

txRollBack:回滾事務

生產者

public class TxSend {

    private static final String QUEUE_NAME = "queue_tx";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();
        
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        String msgString = "hello tx message";

        try {
            channel.txSelect();
            channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes());
            System.out.println("send " + msgString);
            channel.txCommit();
        } catch (Exception e) {
            channel.txRollback();
            System.out.println("send message txRollBack");
        } finally {
            channel.close();
            connection.close();
        }
    }

}

消費者

public class TxReceive {

    private static final String QUEUE_NAME = "queue_tx";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("[recv] tx msg:" + new String(body, "utf-8"));
            }
        });
    }

}

此種模式還是很耗時的,採用這種方式降低了 Rabbit MQ 的消息吞吐量

Confirm 模式

生產者端 confirm 模式的實現原理

生產者將信道設置成 confirm 模式,一旦信道進入 confirm 模式,所有在該信道上面發佈的消息,都會被指派爲一個唯一的 ID(從 1 開始),一旦消息被投遞到所有匹配的隊列之後,broker 就會發送一個確認給生產者(包含消息的唯一 ID),這就使得生產者知道消息已經正確到達目的隊列了,如果消息和隊列是可持久化的,那麼確認消息會將消息寫入磁盤之後發出,broker 回傳給生產者的確認消息中,deliver-tag 域中包含了確認消息的序列號,此外,broker 也可以設置 basic.ack 的 multiple 域,表示到這個序列號之前的所有消息都已經得到了處理

Confirm 模式最大的好處在於它是異步的

Nack

開啓 confirm 模式

channel.confirmSelect()

編程模式:

1.普通:發一條 waitForConfirms()

2.批量的:發一批 waitForConfirms()

3.異步 confirm 模式:提供一個回調方法

Confirm 單條

public class Send1 {

    private static final String QUEUE_NAME = "queue_confirm1";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

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

        channel.confirmSelect();
        String msgString = "hello confirm message!";
        channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes());

        if (!channel.waitForConfirms()) {
            System.out.println("message send failed");
        } else {
            System.out.println("message send ok");
        }

        channel.close();
        connection.close();
    }

}

批量

public class Send2 {

    private static final String QUEUE_NAME = "queue_confirm2";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

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

        channel.confirmSelect();
        String msgString = "hello confirm message!";

        for (int i = 0; i < 10; i++) {
            channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes());
        }

        if (!channel.waitForConfirms()) {
            System.out.println("message send failed");
        } else {
            System.out.println("message send ok");
        }

        channel.close();
        connection.close();
    }

}

異步模式

Channel 對象提供的 confirmListener() 回調方法只包含 deliveryTag(當前 Channel 發出的消息序號),我們需要自己爲每一個 Channel 維護一個 unconfirm 的消息序號集合,每 publish 一條數據,集合中元素加 1,沒回調一次 handlerAck() 方法,unconfirm 集合刪掉相應的一條(multiple=false)或多條(multiple=true)記錄,從程序運行效率上看,這個 unconfirm 集合最好採用有序集合 SortedSet 存儲結構

public class Send3 {

    private static final String QUEUE_NAME = "queue_confirm3";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

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

        channel.confirmSelect();
        final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());

        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long l, boolean b) throws IOException {
                if (b) {
                    System.out.println("---handleAck---multiple");
                    confirmSet.headSet(l + 1).clear();
                } else {
                    System.out.println("---handleAck---multiple false");
                    confirmSet.remove(l);
                }
            }

            @Override
            public void handleNack(long l, boolean b) throws IOException {
                if (b) {
                    System.out.println("---handleNack---multiple");
                    confirmSet.headSet(l + 1).clear();
                } else {
                    System.out.println("---handleNack---multiple false");
                    confirmSet.remove(l);
                }
            }
        });

        String msgStr = "ssssss";

        while (true) {
            long seqNo = channel.getNextPublishSeqNo();
            channel.basicPublish("", QUEUE_NAME, null, msgStr.getBytes());
            confirmSet.add(seqNo);
        }
    }

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