消息模式 之 消息100%投遞、冪等性、Confirm確認消息、Return返回消息、自定義消費者

消息模式 之 消息100%投遞、冪等性、Confirm確認消息、Return返回消息、自定義消費者

本章知識:

1、消息如何保證100%的投遞?
2、冪等性概念
3、Confirm 確認消息
4、Return 返回消息
5、自定義消費者

消息100%的投遞

可參考 -------- RabbitMQ 快速入門:https://blog.csdn.net/qq_44641053/article/details/103401964

如何保障消息100%的投遞成功?

什麼意思呢?
就比如說我們購物下單,用戶在下單後,有訂單服務要發送消息到MQ系統,並不是消息發送過去了就是投遞成功了,還要在發送端收到 MQ 節點後,由Confirm 確認消息;
當然,不可能是每條消息都會投遞成功;那麼,那些沒有投遞成功的消息該怎麼辦呢?
這就需要用到補償機制(說白了有點像回調機制,消息回滾),由補償機制進行處理,確保消息能夠投遞成功。

( .o ~ ′ ︶ ` ~ o. )

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

  • 保障消息的成功發出

  • 保障MQ節點的成功接收

  • 發送端收到MQ節點(Broker)確認應答

  • 完善的消息進行補償機制

BAT/TMD 互聯網大廠的兩種解決方案:

  • 解決方案一:消息落庫,對消息狀態進行打標。

  • 解決方案二:消息的延遲投遞,做二次確認,回調檢查。

注意:BAT/TMD 指的是 百度,阿里,騰訊 / 頭條,美團,滴滴

消息落庫,對消息狀態進行打標:
在這裏插入圖片描述

消息的延遲投遞,做二次確認,回調檢查:

我們一般用這種方案。ヽ(○ ′ ▽ `○)~
在這裏插入圖片描述


冪等性概念

冪等性是什麼?

我們可以借鑑 數據庫的樂觀鎖機制
比如我們執行一條更新庫存的SQL語句:

	Update t_repository set count = count -1,version = version + 1 where version = 1
	
	Elasticsearch 也是嚴格遵循冪等性概念,每次數據更新,version+1

消費端 ------ 冪等性保障:

在海量訂單產生的業務高峯期,如何 避免消息的重複消費 問題?

消費實現冪等性,就意味着,我們的 消息永遠不會消費多次 ,即使我們收到了多條一樣的消息,消息也沒有多次消費。

業界主流的冪等性操作:

  • 唯一ID+指紋碼機制,利用數據庫主鍵去重

    利用數據庫主鍵去重

      	Select count(1) from T_order where ID=唯一ID+指紋碼
    

    好處:實現簡單
    壞處:高併發下有數據庫寫入的性能瓶頸
    解決方案:根據ID進行分庫分表進行算法路由

  • 利用Redis的原子性去實現

    使用Redis進行冪等,需要考慮的問題:

    1. 是否要 進行數據落庫,如果落庫的話,關鍵解決的問題是 數據庫和緩存如何做到原子性

    2. 如果 不進行落庫,那麼 都存儲到緩存中,如何設置定時同步策略


Confirm確認消息

理解 Confirm 消息確認機制:

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

在這裏插入圖片描述

如何實現 Confirm 確認消息?

步驟:

  • 第一步:在 Channel 上開啓確認模式

      channel.confirmSelect()
    
  • 第二步:在 Channel 上添加監聽

    addConfirmListener,監聽成功和失敗的返回結果,根據具體的結果對消息進行重新發送、或記錄日誌等後續處理!

消費端代碼:

package com.dj.rabbitmqapi.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.153.130");
        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.dj.rabbitmqapi.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.153.130");
        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! -----------");
                //發送短信的業務邏輯代碼寫在這裏面
            }
        });
    }
}

測試:

這裏要先用 Docker 啓動容器:
在這裏插入圖片描述
在這裏插入圖片描述
🆗 ,我們來測驗一下Comfirm機制:

注意:要先啓動Consumer,再啓動Producer!!!

如果是先啓動Producer的話,會報錯,因爲沒有隊列。

按順序啓動:

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述


Return返回消息

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

正常情況:
我們的消息生產者,通過指定一個 Exchange(交換機) 和 RoutingKey ,把消息送達到某一個隊列中去,然後我們的消費者監聽隊列,進行消費處理操作!

異常情況:
在某些情況下,如果我們在發送消息的時候,當前的 Exchange 不存在或者指定的路由key路由不到,這個時候如果我們需要監聽這種不可達的消息,就需要使用 Return Listener!

在基礎 API 中有一個關鍵的配置項
Mandatory
如果爲true,則監聽器會接收到路由不可達的消息,然後進行後續處理;(我們開發一般是用這個)
如果爲false,那麼Broker端自動刪除該消息!

在這裏插入圖片描述

消費端代碼:

package com.dj.rabbitmqapi.returnLinstener;

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.153.130");
        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.dj.rabbitmqapi.returnLinstener;

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.153.130");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

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

        String exchange = "test_return_exchange";
        String routingKey = "return.save";//正確的routingKey
        String routingKeyError = "abc.save";//錯誤的routingKey

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

測試:

啓動消費端:
在這裏插入圖片描述
啓動生產端:

消息不可達:
在這裏插入圖片描述
消息可達:
在這裏插入圖片描述
由此可見,Return Listener 會保證信息一定能發送成功(重新投遞)!


自定義消費者

我們一般就是在代碼中編寫 while 循環,進行 consumer.nextDelivery 方法進行獲取下一條消息,然後進行消費處理!

但是我們使用自定義的 Consumer 更加的方便,解耦性更加的強,也是實際工作中最常用的使用方式!

自定義消費端代碼:

package com.dj.rabbitmqapi.consumer;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;

/**
 * 自定義消費端
 */
public class MyConsumer extends DefaultConsumer {


    public MyConsumer(Channel channel) {
        super(channel);
    }

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

        //寫的是處理消息的方案
        System.err.println("----------- consume message ----------");
        System.err.println("consumerTag: " + consumerTag);
        System.err.println("envelope: " + envelope);
        System.err.println("properties: " + properties);
        System.err.println("body: " + new String(body));
    }
}

消費端調用:

package com.dj.rabbitmqapi.consumer;

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

/**
 * 消費端調用
 */
public class Consumer {
    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.153.130");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

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


        String exchangeName = "test_consumer_exchange";
        String routingKey = "consumer.#";
        String queueName = "test_consumer_queue";

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

        //之前是通過是 while 死循環處理
        //現在是通過自己定義的消費者去處理
        channel.basicConsume(queueName, true, new MyConsumer(channel));
        
    }
}

生產端調用:

package com.dj.rabbitmqapi.consumer;

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

/**
 * 生產端調用
 */
public class Producer {
    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.153.130");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

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

        String exchange = "test_consumer_exchange";//指定consumer交換機
        String routingKey = "consumer.save";

        String msg = "Hello RabbitMQ Consumer Message";

        for(int i =0; i<5; i ++){
            channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
        }

    }
}

測試:

啓動消費端:
在這裏插入圖片描述
在這裏插入圖片描述
啓動生產端:
在這裏插入圖片描述
~o(°~ω~°)0~\ノ完結撒花!


本章小結:

  1. 消息的100%投遞

    消息中間件:生產者、Broker、xiaofeizhe

    確保消息到達:Confirm機制、Return機制

    Confirm機制

    	生產端添加 ConfirmListener
    	nack
    	ack
    

    Return機制

    	生產端添加ReturnListener
    	nack
    	ack
    

    補償機制:…

  2. 冪等性的概念

    樂觀鎖 version

  3. 自定義消費者

    消費者 extends DefaultConsumer
    在重寫的方法中加處理消息的邏輯

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