RabbitMQ訊息模式

XiaoZJun發表於2021-01-04

1、訊息如何保證100%的投遞?

什麼是生產端的可靠性投遞?

保障訊息的成功發出
保障MQ節點的成功接收
傳送端收到MQ節點(Broker)確認應答
完善的訊息進行補償機制

BAT/TMD網際網路大廠的解決方案:

訊息落庫,對訊息狀態進行打標
訊息的延遲投遞,做二次確認,回撥檢查
在這裡插入圖片描述
在這裡插入圖片描述

2、冪等性概念

冪等性是什麼?

我們可以借鑑資料庫的樂觀鎖機制
比如我們執行一條更新庫存的SQL語句
Update t_repository set count = count -1,version = version + 1 where version = 1
Elasticsearch也是嚴格遵循冪等性概念,每次資料更新,version+1(博主部落格前面有提到)

消費端-冪等性保障

在海量訂單產生的業務高峰期,如何避免訊息的重複消費問題?
消費實現冪等性,就意味著,我們的訊息永遠不會消費多次,即使我們收到了多條一樣的訊息

業界主流的冪等性操作

唯一ID+指紋碼機制,利用資料庫主鍵去重
利用Redis的原子性去實現

唯一ID+碼 機制

唯一ID+指紋碼機制,利用資料庫主鍵去重
Select count(1) from T_order where ID=唯一ID+指紋碼
好處:實現簡單
壞處:高併發下有資料庫寫入的效能瓶頸
解決方案:根據ID進行分庫分表進行演算法路由

利用Redis的原子性去實現

使用Redis進行冪等,需要考慮的問題
第一:我們是否要進行資料落庫,如果落庫的話,關鍵解決的問題是資料庫和快取如何做到原子性?
第二:如果不進行落庫,那麼都儲存到快取中,如何設定定時同步策略?

3、Confirm確認訊息

理解Confirm訊息確認機制

訊息的確認,是指生產者投遞訊息後,如果Broker收到訊息,則會給我們生產者 一個應答。
生產者進行接收應答,用來確定這條訊息是否正常的傳送到Broker,這種方式也是訊息的可靠性投遞的核心保障

在這裡插入圖片描述
在這裡插入圖片描述
如何實現Confirm確認訊息?

第一步:在Channel上開啟確認模式:channel.confirmSelect()
第二步:在channel上新增監聽:addConfirmListener,監聽成功和失敗的返回結果,根據具體的結果對訊息進行重新傳送、或記錄日誌等後續處理!

消費端程式碼

package com.zhoujun.rabblitmqapi.confirm;

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


public class Consumer {
    public static void main(String[] args) throws Exception {
        //1 建立ConnectionFactory
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.147.146");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        //2 獲取C    onnection
        Connection connection = connectionFactory.newConnection();

        //3 通過Connection建立一個新的Channel
        Channel channel = connection.createChannel();

        String exchangeName = "test_confirm_exchange";
        String routingKey = "confirm.#";
        String queueName = "test_confirm_queue";

        //4 宣告交換機和佇列 然後進行繫結設定, 最後制定路由Key
        channel.exchangeDeclare(exchangeName, "topic", true);
        channel.queueDeclare(queueName, true, false, false, null);
        channel.queueBind(queueName, exchangeName, routingKey);

        //5 建立消費者
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName, true, queueingConsumer);

        while(true){
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
            String msg = new String(delivery.getBody());

            System.err.println("消費端: " + msg);
        }


    }
}

服務方提供程式碼

package com.zhoujun.rabblitmqapi.confirm;

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

import java.io.IOException;


public class Producer {
    public static void main(String[] args) throws Exception {
        //1 建立ConnectionFactory
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.147.146");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        //2 獲取C    onnection
        Connection connection = connectionFactory.newConnection();

        //3 通過Connection建立一個新的Channel
        Channel channel = connection.createChannel();


        //4 指定我們的訊息投遞模式: 訊息的確認模式
        channel.confirmSelect();

        String exchangeName = "test_confirm_exchange";
        String routingKey = "confirm.save";

        //5 傳送一條訊息
        String msg = "Hello RabbitMQ Send confirm message!";
        channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());

        //6 新增一個確認監聽
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("-------no ack!-----------");
            }

            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("-------ack!-----------");
            }
        });
    }
}

4、Return返回訊息

Return Listener用於處理一些不可路由的訊息!

正常情況:我們的訊息生產者,通過指定一個Exchange和RoutingKey,把訊息送達到某一個佇列中去,然後我們的消費者監聽佇列,進行消費處理操作!
異常情況:在某些情況下,如果我們在傳送訊息的時候,當前的Exchange不存在或者指定的路由key路由不到,這個時候如果我們需要監聽這種不可達的訊息,就需要使用Return Listener!

在基礎API中有一個關鍵的配置項
Mandatory:如果為true,則監聽器會接收到路由不可達的訊息,然後進行後續處理,如果為false,那麼Broker端自動刪除該訊息!
在這裡插入圖片描述
消費端程式碼

package com.zhoujun.rabblitmqapi.returnlistener;

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


public class Consumer {
    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.147.146");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        String exchangeName = "test_return_exchange";
        String routingKey = "return.#";
        String queueName = "test_return_queue";

        channel.exchangeDeclare(exchangeName, "topic", true, false, null);
        channel.queueDeclare(queueName, true, false, false, null);
        channel.queueBind(queueName, exchangeName, routingKey);

        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);

        channel.basicConsume(queueName, true, queueingConsumer);

        while(true){
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.err.println("消費者: " + msg);
        }

    }
}

生產端程式碼

package com.zhoujun.rabblitmqapi.returnlistener;

import com.rabbitmq.client.*;

import java.io.IOException;


public class Producer {
    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.147.146");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();

        String exchange = "test_return_exchange";
        String routingKey = "return.save";
        String routingKeyError = "abc.save";

        String msg = "Hello RabbitMQ Return Message";


        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange,
                                     String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {

                System.err.println("---------handle  return----------");
                System.err.println("replyCode: " + replyCode);
                System.err.println("replyText: " + replyText);
                System.err.println("exchange: " + exchange);
                System.err.println("routingKey: " + routingKey);
                System.err.println("properties: " + properties);
                System.err.println("body: " + new String(body));
            }
        });

        //訊息投遞成功,會被消費者所消費
//        channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
        //訊息不可達,將觸發ReturnListener
         channel.basicPublish(exchange, routingKeyError, true, null, msg.getBytes());
    }
}

相關文章