研究一下RabbitMQ

消息中间件

消息中间件作用:异步解耦、流量消峰

一、RabbitMQ 工作模式

1.安装

  • 下载并安装erlang,下载地址:http://www.erlang.org/download

  • 配置erlang环境变量信息

    • 新增环境变量ERLANG_HOME=erlang的安装地址
    • 将%ERLANG_HOME%\bin加入到path中
  • 下载并安装RabbitMQ,下载地址:http://www.rabbitmq.com/download.html

    • 配置rabbitmq的环节变量:d:\rabbitmq_server-3.2.1/sbin
    • 安装可视化插件:rabbitmq-plugins.bat enable rabbitmq_management
    • 使用命令rabbitmqctl status检查是否正常
    • 启动或者停止rabbitmq: net start RabbitMQ / net stop RabbitMQ
    • 添加用户:
      • rabbitmqctl.bat add_user admin admin
      • rabbitmqctl.bat set_user_tags admin administrator
      • rabbitmqctl.bat set_permissions -p / admn “.” “.” “.*”
    • 需要修改:rabbitmq_server-3.7.8 /etc/rabbitmq.config.example,也可以复制一份修改名称rabbitmq.config。然后再环境变量中配置他的路径。修改内容如下
    /*
    {tcp_listeners,[{"127.0.0.1",5672},{"::1",5672}]}
    
    {loopback_users,[admin]}
    
    */
    

注意: RabbitMQ 它依赖于Erlang,需要先安装Erlang。

http://192.168.1.6:15672 默认账号:guest / guest

2.RabbitMQ

RabbitMQ官方教程:https://www.rabbitmq.com/getstarted.html

3.核心概念

  • virtual Hosts 虚拟主机

    • VirtualHost相当月一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的。exchange、queue、message不能互通
  • 生产者(Producer):发送消息的应用。

  • 消费者(Consumer):接收消息的应用。

  • 队列(Queue):存储消息的缓存。

  • 消息(Message):由生产者通过RabbitMQ发送给消费者的信息。

  • 连接(Connection):连接RabbitMQ和应用服务器的TCP连接。

  • 通道(Channel):连接里的一个虚拟通道。当你通过消息队列发送或者接收消息时,这个操作都是通过通道进行的。

  • 交换机(Exchange):交换机负责从生产者那里接收消息,并根据交换类型分发到对应的消息列队里。要实现消息的接收,一个队列必须到绑定一个交换机。

  • 绑定(Binding):绑定是队列和交换机的一个关联连接。

  • 路由键(Routing Key):路由键是供交换机查看并根据键来决定如何分发消息到列队的一个键。路由键可以说是消息的目的地址

当生产者发送消息时,它并不是直接把消息发送到队列里的,而是使用交换机(Exchange)来发送

4.工作模式

  • 简单模式:一个生产者,一个消费者

  • work模式:一个生产者,多个消费者,消费者进行手动应答,谁应答快,谁消费消息多。

  • 订阅模式:一个生产者发送的消息会被多个消费者获取。

  • 路由模式:发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key

  • topic模式:将路由键和某模式进行匹配,此时队列需要绑定在一个模式上,“#”匹配一个词或多个词,“*”只匹配一个词。

创建连接 ——> 获取连接 -------->创建通道 -----> 声明队列 ---->发送消息

消费端,通过监听方式,拉取队列对应的消息。

queueDeclare队列声明

channel通道

应答模式ACK

5 简单模式/work模式

5.1 依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

5.2生产者

package com.zx.rabbitmqdemo.simpleQueue;

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

public class ProductMessage {
    //声明一个队列
    private static final String QUEUE_NAME = "test_rabbitmq_simple";

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //设置地址
        connectionFactory.setPort(5672);
        connectionFactory.setPassword("admin");
        connectionFactory.setUsername("admin");
        connectionFactory.setHost("192.168.1.6");
        //设置虚拟主机
        connectionFactory.setVirtualHost("/");

        //获取连接
        Connection connection = connectionFactory.newConnection();
        //创建一个通道
        Channel channel = connection.createChannel();
        //申明一个队列
        // durable: 如果为true,消息持久化,服务器重启后消息还存在
        //exclusive:如果声明独占队列(仅限于此连接),则为true
        //autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
        //arguments:队列的其他属性(构造参数)
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        String msg = "生产者发送消息发送到队列:"+QUEUE_NAME+"中 ";
        /*
        发布消息:
        exchange:交换机
        queue_name:队列名
        props:消息路由头等的其他属性
        body:消息体,二进制数组
        */
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());

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

5.3 消费者

package com.zx.rabbitmqdemo.simpleQueue;

import com.rabbitmq.client.*;
import com.rabbitmq.client.impl.recovery.ConsumerRecoveryListener;

import java.io.UnsupportedEncodingException;


public class ConsumerMessage {
    //声明一个队列
    private static final String QUEUE_NAME = "test_rabbitmq_simple";

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //设置地址
        connectionFactory.setPort(5672);
        connectionFactory.setPassword("admin");
        connectionFactory.setUsername("admin");
        connectionFactory.setHost("192.168.1.6");
        //设置虚拟主机
        connectionFactory.setVirtualHost("/");

        //获取连接
        Connection connection = connectionFactory.newConnection();
        //创建一个通道
        Channel channel = connection.createChannel();

        //申明一个队列
        // durable: 如果为true,消息持久化,服务器重启后消息还存在
        //exclusive:如果声明独占队列(仅限于此连接),则为true
        //autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
        //arguments:队列的其他属性(构造参数)
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);



        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
            //TODO 工作模式,需要进行手动应答,谁应答块,谁消费消息多
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };
        //自动应答  简单队列模式
       // channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });

        channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });

        /*Consumer consumer = new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws UnsupportedEncodingException {
                String msg = new String(body,"utf-8");
                System.out.println(msg);
            }
        } ;

        // 3.监听队列
        channel.basicConsume(QUEUE_NAME, true, consumer);*/
    }
}

6.发布订阅模式[高级队列]

6.1 消息投递流程

  • .生产者投递消息到Exchange交换机[消息管道声明交换机,如果存在就创建一个交换机,并且把该管道绑定到该交换机]

  • 消费者通过管道声明队列,同时通过管道把该队列和交换机进行绑定【没有指定routingkey,相当于进行广播.】

  • 生产者投递消息到交换机,该交换机会把消息投送到和交换机绑定的队列,队列在投递或者消费者拉取消息

6.2 发布订阅原理

RoutingKey路由keyExchange交换机

p:product 、 x:exchange、多个队列 ; queue 绑定 exchange

Fanout exchange(扇型交换机)将消息路由给绑定到它身上的所有队列【默认交换机类型】

  • 一个生产者,多个消费者
  • 每一个消费者都有自己的一个队列
  • 生产者没有直接发消息到队列中,而是发送到交换机
  • 每个消费者的队列都绑定到交换机上
  • 消息通过交换机到达每个消费者的队列 【一对多,队列只要绑定了该交换机,消息投递到该交换机,队列都会收到消息】

注意:交换机没有存储消息功能,如果消息发送到没有绑定消费队列的交换机,消息则丢失

6.3 演示代码

public class RabbitmqUtil {

    public static Connection getConnection() throws IOException, TimeoutException {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //设置地址
        connectionFactory.setPort(5672);
        connectionFactory.setPassword("admin");
        connectionFactory.setUsername("admin");
        connectionFactory.setHost("192.168.1.6");
        //设置虚拟主机
        connectionFactory.setVirtualHost("/");

        //获取连接
        Connection connection = connectionFactory.newConnection();
        return connection;

    }
}

/**
 * 发布/订阅  publish / subscribe
 */
public class ProductMessage {
    private static final String EXCHANGE_NAME = "my_fanout";
    public static void main(String[] args) throws  Exception{
        Connection connection = RabbitmqUtil.getConnection();
        //创建通道
        Channel channel = connection.createChannel();
        //声明交换机,type = fanout,如果交换机不存在,声明的同时也绑定交换机.
       channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
       //创建消息
        String msg = "exchange  type fanout rabbitmq 发布订阅";
        //发送消息
        channel.basicPublish(EXCHANGE_NAME,"",null,msg.getBytes());
        //关闭通道
        channel.close();
        connection.close();
    }
}
public class ConsumerMessage01 {
    private static final String EXCHANGE_NAME = "my_fanout";
    private static final String QUEUE_SMS = "sms_queue_exchange_fanout";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitmqUtil.getConnection();
        //创建通道
        Channel channel = connection.createChannel();
        //消费者申明队列

        //申明一个队列
        // durable: 如果为true,消息持久化,服务器重启后消息还存在
        //exclusive:如果声明独占队列(仅限于此连接),则为true
        //autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
        //arguments:队列的其他属性(构造参数)
        channel.queueDeclare(QUEUE_SMS, false, false, false, null);
        //消费者绑定队列
        channel.queueBind(QUEUE_SMS,EXCHANGE_NAME,"");
        //消费者监听队列
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
            //TODO 工作模式,需要进行手动应答,谁应答块,谁消费消息多
            //channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };

        channel.basicConsume(QUEUE_SMS, true, deliverCallback, consumerTag -> { });
    }
}

public class ProductMessage {
    //声明一个队列
    private static final String QUEUE_NAME = "test_rabbitmq_simple";

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //设置地址
        connectionFactory.setPort(5672);
        connectionFactory.setPassword("admin");
        connectionFactory.setUsername("admin");
        connectionFactory.setHost("192.168.1.6");
        //设置虚拟主机
        connectionFactory.setVirtualHost("/");

        //获取连接
        Connection connection = connectionFactory.newConnection();
        //创建一个通道
        Channel channel = connection.createChannel();
        //申明一个队列
        // durable: 如果为true,消息持久化,服务器重启后消息还存在
        //exclusive:如果声明独占队列(仅限于此连接),则为true
        //autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
        //arguments:队列的其他属性(构造参数)
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        String msg = "生产者发送消息发送到队列:"+QUEUE_NAME+"中 ";
        /*
        发布消息:
        exchange:交换机
        queue_name:队列名
        props:消息路由头等的其他属性
        body:消息体,二进制数组
        */
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());

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

7.路由模式

路由模式:发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key

Direct exchange(直连交换机)是根据消息携带的路由键(routing key)将消息投递给对应队列的

exchange_type = direct

路由模式,exchange 会根据routingkey 进行投递消息到队列中。

  • 消息投递, 管道申明交换机的时候指定routingkey
  • 消费者通过管道声明队列,同时通过管道把该队列和交换机通过routingkey 进行绑定
  • 生产者投递消息到交换机,交换机根据routingkey 进行投递到指定的的队列。

一个队列可以绑定多个routingkey

7.1 演示代码

/**
 * 发布/订阅  publish / subscribe
 */
public class ProductMessage {
    private static final String EXCHANGE_NAME = "my_direct";
    private static final String ROUTING_KEY = "routing_key";
    public static void main(String[] args) throws  Exception{
        Connection connection = RabbitmqUtil.getConnection();
        //创建通道
        Channel channel = connection.createChannel();
        //声明交换机,type = fanout,如果交换机不存在,声明的同时也绑定交换机.
       channel.exchangeDeclare(EXCHANGE_NAME,"direct");
       //创建消息
        String msg = "exchange  type direct rabbitmq 路由策略routingkey";
        //发送消息
        channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,null,msg.getBytes());
        //关闭通道
        channel.close();
        connection.close();
    }
}
public class ConsumerMessage01 {
    private static final String EXCHANGE_NAME = "my_direct";
    private static final String QUEUE_SMS = "sms_queue_exchange_direct";
    private static final String ROUTING_KEY = "routing_key";
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitmqUtil.getConnection();
        //创建通道
        Channel channel = connection.createChannel();
        //消费者申明队列

        //申明一个队列
        // durable: 如果为true,消息持久化,服务器重启后消息还存在
        //exclusive:如果声明独占队列(仅限于此连接),则为true
        //autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
        //arguments:队列的其他属性(构造参数)
        channel.queueDeclare(QUEUE_SMS, false, false, false, null);
        //消费者绑定队列 绑定routingkey ,可以绑定多个
        //channel.queueBind(QUEUE_SMS,EXCHANGE_NAME,ROUTING_KEY_SMS);
        channel.queueBind(QUEUE_SMS,EXCHANGE_NAME,ROUTING_KEY);
        //消费者监听队列
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
            //TODO 工作模式,需要进行手动应答,谁应答块,谁消费消息多
            //channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };

        channel.basicConsume(QUEUE_SMS, true, deliverCallback, consumerTag -> { });
    }
}
public class ConsumerMessage02 {
    private static final String EXCHANGE_NAME = "my_direct";
    private static final String ROUTING_KEY = "routing_key";
    private static final String QUEUE_EMAIL = "email_queue_exchange_direct";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitmqUtil.getConnection();
        //创建通道
        Channel channel = connection.createChannel();
        //消费者申明队列

        //申明一个队列
        // durable: 如果为true,消息持久化,服务器重启后消息还存在
        //exclusive:如果声明独占队列(仅限于此连接),则为true
        //autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
        //arguments:队列的其他属性(构造参数)
        channel.queueDeclare(QUEUE_EMAIL, false, false, false, null);
        //消费者绑定队列
        //channel.queueBind(QUEUE_EMAIL,EXCHANGE_NAME,ROUTING_KEY);
        channel.queueBind(QUEUE_EMAIL,EXCHANGE_NAME,"");
        //消费者监听队列
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
            //TODO 工作模式,需要进行手动应答,谁应答块,谁消费消息多
          //  channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };

        channel.basicConsume(QUEUE_EMAIL, true, deliverCallback, consumerTag -> { });
    }
}

consumer02没有绑定routingkey,consumer01 绑定routingkey 。product申明交换机的同时指定routingkey

8.Topics模式

此模式实在路由key模式的基础上,使用了通配符来管理消费者接收消息

生产者P发送消息到交换机X,type=topic,交换机根据绑定队列的routing key的值进行通配符匹配;

符号#:匹配一个或者多个词lazy.# 可以匹配lazy.irs或者lazy.irs.cor
符号*:只能匹配一个词lazy. 可以匹配lazy.irs或者lazy.cor

9.交换机类型

  • Direct exchange(直连交换机)是根据消息携带的路由键(routing key)将消息投递给对应队列的
  • Fanout exchange(扇型交换机)将消息路由给绑定到它身上的所有队列
  • Topic exchange(主题交换机)队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列
  • Headers exchange(头交换机)类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
**
 * 发布/订阅  publish / subscribe
 */
public class ProductMessage {
    private static final String EXCHANGE_NAME = "my_topic";
    private static final String ROUTING_KEY = "routing_key_los.sms";
    public static void main(String[] args) throws  Exception{
        Connection connection = RabbitmqUtil.getConnection();
        //创建通道
        Channel channel = connection.createChannel();
        //声明交换机,type = fanout, direct  topic如果交换机不存在,声明的同时也绑定交换机.
       channel.exchangeDeclare(EXCHANGE_NAME,"topic");
       //创建消息
        String msg = "exchange  type topic rabbitmq routing_key_los.sms";
        //发送消息
        channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,null,msg.getBytes());
        //关闭通道
        channel.close();
        connection.close();
    }
}

public class ConsumerMessage01 {
    private static final String EXCHANGE_NAME = "my_topic";
    private static final String QUEUE_SMS = "sms_queue_exchange_topic";
    private static final String ROUTING_KEY = "routing_key_los.#";
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitmqUtil.getConnection();
        //创建通道
        Channel channel = connection.createChannel();
        //消费者申明队列

        //申明一个队列
        // durable: 如果为true,消息持久化,服务器重启后消息还存在
        //exclusive:如果声明独占队列(仅限于此连接),则为true
        //autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
        //arguments:队列的其他属性(构造参数)
        channel.queueDeclare(QUEUE_SMS, false, false, false, null);
        //消费者绑定队列 绑定routingkey ,可以绑定多个
        channel.queueBind(QUEUE_SMS,EXCHANGE_NAME,ROUTING_KEY);
        //消费者监听队列
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
            //TODO 工作模式,需要进行手动应答,谁应答块,谁消费消息多
            //channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };

        channel.basicConsume(QUEUE_SMS, true, deliverCallback, consumerTag -> { });
    }
}

public class ConsumerMessage02 {
    private static final String EXCHANGE_NAME = "my_topic";
    private static final String ROUTING_KEY = "routing_key_los.#";
    private static final String QUEUE_EMAIL = "email_queue_exchange_topic";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitmqUtil.getConnection();
        //创建通道
        Channel channel = connection.createChannel();
        //消费者申明队列

        //申明一个队列
        // durable: 如果为true,消息持久化,服务器重启后消息还存在
        //exclusive:如果声明独占队列(仅限于此连接),则为true
        //autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
        //arguments:队列的其他属性(构造参数)
        channel.queueDeclare(QUEUE_EMAIL, false, false, false, null);
        //消费者绑定队列
         channel.queueBind(QUEUE_EMAIL,EXCHANGE_NAME,ROUTING_KEY);

        //消费者监听队列
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
            //TODO 工作模式,需要进行手动应答,谁应答块,谁消费消息多
          //  channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };

        channel.basicConsume(QUEUE_EMAIL, true, deliverCallback, consumerTag -> { });
    }
}

二、Rabbitmq消息确认机制

  • 消息应答模式ACK,消费端默认是自动应答
  • 生产者投递消息,如何确保消息投递成功?
  • 如果Rabbitmq服务器宕机,消息会丢失吗? RbbitMQ消息会进行持久化.
// durable: 如果为true,消息持久化,服务器重启后消息还存在
//exclusive:如果声明独占队列(仅限于此连接),则为true
//autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
//arguments:队列的其他属性(构造参数)
channel.queueDeclare(QUEUE_NAME, true, false, false, null);

1.AMQP事物机制

  • 事务模式:
    • txSelect 将当前channel设置为transaction模式
    • txCommit 提交当前事务
    • txRollback 事务回滚
public class ProductMessage {
    //声明一个队列
    private static final String QUEUE_NAME = "test_rabbitmq_simple_tx";

    public static void main(String[] args) throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //设置地址
        connectionFactory.setPort(5672);
        connectionFactory.setPassword("admin");
        connectionFactory.setUsername("admin");
        connectionFactory.setHost("192.168.1.6");
        //设置虚拟主机
        connectionFactory.setVirtualHost("/");

        //获取连接
        Connection connection = connectionFactory.newConnection();
        //创建一个通道
        Channel channel = connection.createChannel();


        //申明一个队列
        // durable: 如果为true,消息持久化,服务器重启后消息还存在
        //exclusive:如果声明独占队列(仅限于此连接),则为true
        //autoDelete:如果声明的是自动删除队列,则为true(服务器将在不再使用时将其删除)
        //arguments:队列的其他属性(构造参数)
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        try {
            channel.txSelect();//开启事物
            String msg = "生产者发送消息发送到队列:" + QUEUE_NAME + "中 ";
        /*
        发布消息:
        exchange:交换机
        queue_name:队列名
        props:消息路由头等的其他属性
        body:消息体,二进制数组
        */
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
            //TODO 发送完消息之后 ,出现int i = 1/0的错误  ,消息会发送到rabbitmq服务器中

            int i = 1 / 0;
            channel.txCommit();//提交事物
        } catch (Exception e) {
            e.printStackTrace();
            channel.txRollback();//回滚事物
        } finally {

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

2.Confirm 模式

发送消息确认:用来确认生产者 producer 将消息发送到 brokerbroker 上的交换机 exchange 再投递给队列 queue的过程中,消息是否成功投递。

  • 消息从 producerrabbitmq broker有一个 confirmCallback 确认模式。

  • 消息从 exchangequeue 投递失败有一个 returnCallback 退回模式。

  • 我们可以利用这两个Callback来确保消的100%送达。

配置文件中配置开启confirmcallback / return callback 模式

三、Springboot整合RabbitMQ

1.SpringBoot整合RabbitMQ

2.消费者消费消息抛出异常

消费端如果出现业务上的异常,比如int i = 1/0,消费端默认会进行重试。RabbitMQ服务器上的消息没有被消费端消费。补偿机制是队列服务器(RabbitMQ服务器)发送的。

@RabbitListener 注解.底层使用AOP进行拦截,只要该方法没有抛出异常。会自动提交事物,RabbitMQ会删除消息。如果被AOP异常通知拦截,补货异常信息,会自动实现补偿机制,一致补偿到不抛出异常,该消息一致会缓存在RabbitMQ服务器上缓存。

修改补偿机制,默认间隔5s重试.可以在配置文件中配置重试时间间隔和重试次数.

    listener:
      simple:
        retry:
        ####开启消费者重试
          enabled: true
         ####最大重试次数
          max-attempts: 5
        ####重试间隔次数
          initial-interval: 3000

重试了配置的重试次数之后,就放弃消息重试,如果程序还在报异常,需要我们把消息转入到死信队列对,或者不用后续处理,RabbitMQ会把该消息删除。

3.重试机制

  • 场景:
    • 消费者获取到消息后,调用第三方接口,但接口暂时无法访问,是否需要重试? (需要重试机制)
      • 比如 邮件消费者接收到消息,调用第三方邮件接口(使用http协议,比如HttpUtils工具类发送请求)
    • 消费者获取到消息后,抛出数据转换异常,是否需要重试?(不需要重试机制)需要发布进行解决。
      • 如果消费者代码抛出异常是需要发布新版本才能解决的问题,那么不需要重试,重试也无济于事。应该采用日志记录+定时任务job健康检查+人工进行补偿

3.1 调用第三方接口自动实现补偿机制.

利用RabbitMQ的重试机制,去重复调用第三方接口。比如:消费者消费消息抛出异常处理的原理.

3.2 消费端如何解决幂等性

  • 生产者在发送消息的时候的需要设置一个全局唯一的ID放到消息头中,作为消息标识。同时存一份在redis中。消费端,从消息头获取消息ID,和缓存中取出该ID,并且删除该ID,然后进行比较。如果相等,进行下一步操作。
  • 使用业务状态进行排除幂等性。比如订单状态、支付状态

四、死信队列

1.死信队列场景

  • 队列长度已满
  • 消费者拒绝消费消息,或者消费值没有手动应答
  • 消息有过期时间,消息过期了。

在定义业务队列的时候,可以考虑指定一个死信交换机,并绑定一个死信队列,当消息变成死信时,该消息就会被发送到该死信队列上,这样就方便我们查看消息失败的原因了

channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); 丢弃消息

2.定义业务(普通)队列的时候指定参数

x-dead-letter-exchange: 用来设置死信后发送的交换机

x-dead-letter-routing-key:用来设置死信的routingKey

3.死信队列环境搭建

生产者

@Component
public class FanoutConfig {

	/**
	 * 定义死信队列相关信息
	 */
	public final static String deadQueueName = "dead_queue";
	public final static String deadRoutingKey = "dead_routing_key";
	public final static String deadExchangeName = "dead_exchange";
	/**
	 * 死信队列 交换机标识符
	 */
	public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
	/**
	 * 死信队列交换机绑定键标识符
	 */
	public static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";

	// 邮件队列
	private String FANOUT_EMAIL_QUEUE = "fanout_email_queue";

	// 短信队列
	private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
	// fanout 交换机
	private String EXCHANGE_NAME = "fanoutExchange";

	// 1.定义邮件队列
	@Bean
	public Queue fanOutEamilQueue() {
		// 将普通队列绑定到死信队列交换机上
		Map<String, Object> args = new HashMap<>(2);
		args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);
		args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);
		Queue queue = new Queue(FANOUT_EMAIL_QUEUE, true, false, false, args);
		return queue;
	}

	// 2.定义短信队列
	@Bean
	public Queue fanOutSmsQueue() {
		return new Queue(FANOUT_SMS_QUEUE);
	}

	// 2.定义交换机
	@Bean
	FanoutExchange fanoutExchange() {
		return new FanoutExchange(EXCHANGE_NAME);
	}

	// 3.队列与交换机绑定邮件队列
	@Bean
	Binding bindingExchangeEamil(Queue fanOutEamilQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(fanOutEamilQueue).to(fanoutExchange);
	}

	// 4.队列与交换机绑定短信队列
	@Bean
	Binding bindingExchangeSms(Queue fanOutSmsQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(fanOutSmsQueue).to(fanoutExchange);
	}

	/**
	 * 配置死信队列
	 * 
	 * @return
	 */
	@Bean
	public Queue deadQueue() {
		Queue queue = new Queue(deadQueueName, true);
		return queue;
	}

	@Bean
	public DirectExchange deadExchange() {
		return new DirectExchange(deadExchangeName);
	}

	@Bean
	public Binding bindingDeadExchange(Queue deadQueue, DirectExchange deadExchange) {
		return BindingBuilder.bind(deadQueue).to(deadExchange).with(deadRoutingKey);
	}

}

消费者配置

@RabbitListener(queues = "fanout_email_queue")
	public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
		String messageId = message.getMessageProperties().getMessageId();
		String msg = new String(message.getBody(), "UTF-8");
		System.out.println("邮件消费者获取生产者消息msg:" + msg + ",消息id:" + messageId);
		JSONObject jsonObject = JSONObject.parseObject(msg);
		Integer timestamp = jsonObject.getInteger("timestamp");
		try {
			int result = 1 / timestamp;
			System.out.println("result:" + result);
			// 通知mq服务器删除该消息
			channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
		} catch (Exception e) {
			e.printStackTrace();
			// // 丢弃该消息
			channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
		}

	}
@Component
public class DeadConsumer {

	@RabbitListener(queues = "dead_queue")
	public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
		String messageId = message.getMessageProperties().getMessageId();
		String msg = new String(message.getBody(), "UTF-8");
		System.out.println("死信邮件消费者获取生产者消息msg:" + msg + ",消息id:" + messageId);
		channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

	}

}

五、MQ解决分布式事务

MQ解决分布式事务三个重要概念

  • 确保生产者消息一定要投递到MQ服务器中 Confirm机制

  • 确保消费者能够正确的消费消息,采用手动ACK(注意幂等)

  • 如何保证第一个事务一定要创建成功(在创建一个补单的队列,绑定同一个交换机,检查订单数据是否已经创建在数据库中 实现补偿机制)

生产者 一定确保消息投递到MQ服务器(使用)

补偿队列

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