消息模式 之 消息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
    在重写的方法中加处理消息的逻辑

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