Fork me on GitLab

RabbitMQ入门到精通

RabbitMQ知识学习

简介

  Rabbit科技有限公司开发了RabbitMQ,并提供对其的支持。起初,Rabbit科技是LSHIFT和CohesiveFT在2007年成立的合资企业,2010年4月被VMware旗下的SpringSource收购。RabbitMQ在2013年5月成为GoPivotal的一部分。

  RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

RabbitMQ 的概念

  RabbitMQ 是一个消息中间件:它接受并转发消息。

你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是 一个快递站,一个快递员帮你传递快件。

  RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收, 存储和转发消息数据。

官网:https://www.rabbitmq.com/#features

四大核心概念

  • 生产者

    产生数据发送消息的程序是生产者

  • 交换机

    交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息 推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推 送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定

  • 队列

    队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存 储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可 以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式

  • 消费者

    消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费 者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。

安装

RabbitMQ和Rrlang的版本是有要求的,如果不知道该安装那个版本直接全部安装最新版就好。

Erang安装配置

Erlang下载地址:https://www.erlang.org/downloads

以管理员的身份运行Erlang应用,然后一直next就行。

 设置环境变量,新建ERLANG_HOME

使用cmd验证一下

RabbitMQ安装配置

RabbitMQ下载地址:https://www.rabbitmq.com/download.html

安装过程与erlang的安装过程相同。

RabbitMQ安装好后接下来安装RabbitMQ-Plugins。打开命令行cd,输入RabbitMQ的sbin目录

打开sbin目录,双击rabbitmq-server.bat

等几秒钟看到一个界面后,不用管它,访问http://localhost:15672

 

用户和密码都是guest

到此RabbitMQ已经安装完成。

docker安装

我这里使用docker安装时设置了账号和密码RABBITMQ_DEFAULT_USER=admin,RABBITMQ_DEFAULT_PASS=123456

docker run -id --name myrabbit -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=123456 -p 15672:15672 rabbitmq:3-management

Hello world

官方网站:https://www.rabbitmq.com/tutorials/tutorial-five-python.html

我们将用 Java 编写两个程序。发送单个消息的生产者和接收消息并打印出来的消费者
在下图中,“ P” 是我们的生产者,“ C” 是我们的消费者。中间的框是一个队列 RabbitMQ 代表使用者保留的消息缓冲区

连接的时候,需要开启 5672 端口

这个时候使用的其实是默认的直连交换机(DirectExchange),DirectExchange 的路由策略是将消息队列绑定到一个 DirectExchange 上,当一条消息到达 DirectExchange 时会被转发到与该条消息 routing key 相同的 Queue 上,例如消息队列名为 “hello-queue”,则 routingkey 为 “hello-queue” 的消息会被该消息队列接收。 

生产者

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

public class Producer1 {

    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        //创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        //channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        //创建连接
        Connection connection = factory.newConnection();
        //获取信道
        Channel channel = connection.createChannel();
        /**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 也就是是否用完就删除
         * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
         * 5.其他参数
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        String message = "hello world";
        /**
         * 发送一个消息
         * 1.发送到那个交换机
         * 2.路由的 key 是哪个
         * 3.其他的参数信息
         * 4.发送消息的消息体
         */
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println("消息发送完毕");

    }
}

消费者

import com.rabbitmq.client.*;

public class Consumer1 {

    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        System.out.println("等待接收消息.........");

        //推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println(message);
        };
        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println("消息消费被中断");
        };
        /**
         * 消费者消费消息 - 接受消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         * 4.消息被取消时的回调
         */
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }

}

从控制台打印的信息发现生产者发送的两次信息

一个减少代码的连接工具

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class RabbitMqUtils {

    /**
     * 得到一个连接的 channel
     * @return
     * @throws Exception
     */
    public static Channel getChannel() {
        //创建一个连接工厂
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("127.0.0.1");
            factory.setPort(5672);
            factory.setUsername("guest");
            factory.setPassword("guest");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            return channel;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void close(Channel channel) {
        try {
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }

WorkQueues工作队列模式

在工人之间分配任务(竞争消费者模式

  

一个应用程序正在使用消息但是,它无法像将消息添加到通道一样快地处理消息。

消息传递客户端如何同时处理多个消息?

在单个通道上创建多个竞争消费者,以便消费者可以同时处理多个消息。

竞争消费者是多个消费者,它们都是为了从单个点对点通道接收消息而创建的。当通道传递消息时,任何消费者都可能收到它。消息传递系统的实现决定了哪个消费者实际接收到消息,但实际上消费者相互竞争成为接收者。一旦消费者收到一条消息,它就可以委托其应用程序的其余部分来帮助处理该消息。(此解决方案仅适用于点对点通道;发布-订阅通道上的多个消费者只是为每条消息创建更多副本。)

...

操作一波

两个消费同时向一个队列去消费信息,看看生产者在队列发送消息后会发生什么?

生产者:

import com.rabbitmq.client.Channel;
import com.work.rabbitmq.util.RabbitMqUtils;

import java.io.IOException;

public class ProducerWorkQueues {

    private final static String QUEUE_NAME = "work_queues";

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMqUtils.getChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        for (int i=0;i<10;i++) {
            String message = "work_queues"+i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        }
        System.out.println("消息发送完毕");
        RabbitMqUtils.close(channel);
    }
}

消费者1:

import com.rabbitmq.client.Channel;
import com.work.rabbitmq.util.RabbitMqUtils;

import java.io.IOException;

public class ConsumerWorkQueues1 {
    private final static String QUEUE_NAME = "work_queues";

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMqUtils.getChannel();

        channel.basicConsume(QUEUE_NAME, true, (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println("ConsumerWorkQueues1 抢占===>"+message);
        }, (consumerTag) -> {
            System.out.println("消息消费被中断");
        });

    }
}

消费者2:除名称外代码与上一致

生产者

 

 消费者1

 消费者2

Pub/Sub发布/订阅模式

一次向多个消费者发送消息

 

 

 

RabbitMQ 中消息传递模型的核心思想是生产者从不直接向队列发送任何消息。实际上,生产者通常根本不知道消息是否会被传递到任何队列。

相反,生产者只能向交换器发送消息。交换是一件非常简单的事情。一方面它接收来自生产者的消息,另一方面它将它们推送到队列中。交换必须确切地知道如何处理它收到的消息。是否应该将其附加到特定队列?它应该附加到许多队列中吗?或者它应该被丢弃。其规则由 交换类型定义。

有几种可用的交换类型:direct、topic、headers 和fanout

生产者

public class ProductPubSub {

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();


        /**
         * 创建交换机
         * 1、交换机名称
         * 2、交换机类型
         *  DIRECT("direct"), 定向
         *     FANOUT("fanout"), 扇形  、广播
         *     TOPIC("topic"), 通配符的方式
         *     HEADERS("headers"); 参数匹配
         *  3、是否持久化
         *  4、自动删除
         *  5、内部使用 一般false
         *  6、参数
         */
        String exchangeName = "test_fanout";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, true, false, false, null);
        // 创建队列
        String queueName1= "test_fanout1";
        String queueName2= "test_fanout2";
        channel.queueDeclare(queueName1, true, false, false, null);
        channel.queueDeclare(queueName2, true, false, false, null);
        /**
         * 绑定队列、交换机
         * 1、队列名称
         * 2、交换机名称
         * 3、路由的建 如果交换机为fanout ""
         *
         */
        channel.queueBind(queueName1, exchangeName, "");
        channel.queueBind(queueName2, exchangeName, "");
        // 发送消息
        String msg = "ProductPubSub sender msg !!!!";
        channel.basicPublish(exchangeName, "", null, msg.getBytes());

        RabbitMqUtils.close(channel);
    }
}

消费者1

import com.rabbitmq.client.Channel;
import com.work.rabbitmq.util.RabbitMqUtils;

import java.io.IOException;

public class ConsumerPubSub1 {

    private final static String QUEUE_NAME = "test_fanout1";

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMqUtils.getChannel();

        channel.basicConsume(QUEUE_NAME, true, (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println("ConsumerPubSub1===>"+message);
        }, (consumerTag) -> {
            System.out.println("消息消费被中断");
        });
    }
}

消费者2

import com.rabbitmq.client.Channel;
import com.work.rabbitmq.util.RabbitMqUtils;

import java.io.IOException;

public class ConsumerPubSub2 {

    private final static String QUEUE_NAME = "test_fanout2";

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMqUtils.getChannel();

        channel.basicConsume(QUEUE_NAME, true, (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println("ConsumerPubSub2===>"+message);
        }, (consumerTag) -> {
            System.out.println("消息消费被中断");
        });
    }
}

执行后可以发现消费者1、2都收到了生产的消息

Routing路由模式

有选择地接收消息

在这个设置中,我们可以看到绑定了两个队列的直接交换X。第一个队列使用绑定键orange进行绑定,第二个队列有两个绑定,一个使用绑定键black,另一个使用green。

在这样的设置中,使用路由键橙色发布到交换的消息 将被路由到队列Q1带有黑色 或绿色路由键的消息将发送到Q2所有其他消息将被丢弃。

 生产者
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.work.rabbitmq.util.RabbitMqUtils;

import java.io.IOException;

public class ProducerRouting {


    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMqUtils.getChannel();

        String exchangeName = "test_direct";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true, false, false, null);

        String queueName1= "test_direct1";
        String queueName2= "test_direct2";
        channel.queueDeclare(queueName1, true, false, false, null);
        channel.queueDeclare(queueName2, true, false, false, null);


        channel.queueBind(queueName1, exchangeName, "error");

        channel.queueBind(queueName2, exchangeName, "error");
        channel.queueBind(queueName2, exchangeName, "info");
        channel.queueBind(queueName2, exchangeName, "waring");


        // 发送消息
        String msg = "ProducerRouting sender info msg !!!!";
        channel.basicPublish(exchangeName, "info", null, msg.getBytes());

        RabbitMqUtils.close(channel);
    }
}

消费者1

import com.rabbitmq.client.Channel;
import com.work.rabbitmq.util.RabbitMqUtils;

import java.io.IOException;

public class ConsumerRouting1 {
    private final static String QUEUE_NAME = "test_direct1";

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMqUtils.getChannel();

        channel.basicConsume(QUEUE_NAME, true, (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println("ConsumerRouting1===>"+message);
        }, (consumerTag) -> {
            System.out.println("消息消费被中断");
        });
    }
}

消费者2

import com.rabbitmq.client.Channel;
import com.work.rabbitmq.util.RabbitMqUtils;

import java.io.IOException;

public class ConsumerRouting2 {
    private final static String QUEUE_NAME = "test_direct2";

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMqUtils.getChannel();

        channel.basicConsume(QUEUE_NAME, true, (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println("ConsumerRouting2===>"+message);
        }, (consumerTag) -> {
            System.out.println("消息消费被中断");
        });
    }
}

可以看到运行了两次生产者代码向info中发送了1条信息

 在使用error 路由key发送一次信息,可以看到交换机同时向1,2中都推送了一条信息

消费者去看看消费到信息是不是一致

 Topics通配符模式

发送到主题交换的消息不能有任意的 routing_key - 它必须是单词列表,由点分隔。这些词可以是任何东西,但通常它们指定与消息相关的一些特征。一些有效的路由键示例:“ stock.usd.nyse ”、“ nyse.vmw ”、“ quick.orange.rabbit ”。路由键中可以有任意多的单词,最多为 255 个字节。

绑定键也必须采用相同的格式。主题交换背后的逻辑 类似于直接交换- 使用特定路由键发送的消息将被传递到与匹配绑定键绑定的所有队列。但是,绑定键有两个重要的特殊情况:

  • *(星号)可以只替换一个单词。
  • # (hash) 可以代替零个或多个单词。
 
Topic 主题模式可以实现 Pub/Sub发布与订阅模式和Routing 路由模式的功能,只是Topic在配置routing key的时候可以使用通配符,显得更加灵活。
 生产者
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.work.rabbitmq.util.RabbitMqUtils;

public class ProductTopic {
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();


        /**
         * 创建交换机
         * 1、交换机名称
         * 2、交换机类型
         *  DIRECT("direct"), 定向
         *     FANOUT("fanout"), 扇形  、广播
         *     TOPIC("topic"), 通配符的方式
         *     HEADERS("headers"); 参数匹配
         *  3、是否持久化
         *  4、自动删除
         *  5、内部使用 一般false
         *  6、参数
         */
        String exchangeName = "test_topic";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, true, false, false, null);
        // 创建队列
        String queueName1= "test_topic1";
        String queueName2= "test_topic2";
        channel.queueDeclare(queueName1, true, false, false, null);
        channel.queueDeclare(queueName2, true, false, false, null);
        /**
         * 绑定队列、交换机
         * 1、队列名称
         * 2、交换机名称
         * 3、路由的建 如果交换机为fanout ""
         *
         * 需求:1、所有error级别的日志
         *      2、order所有的日志
         *
         */
        channel.queueBind(queueName1, exchangeName, "*.error");
        channel.queueBind(queueName2, exchangeName, "order.*");
//        channel.queueBind(queueName2, exchangeName, "*");
        // 发送消息
        String msg = "ProductTopic sender msg RuntimeException.error !!!!";
        channel.basicPublish(exchangeName, "RuntimeException.error", null, msg.getBytes());

        RabbitMqUtils.close(channel);
    }
}

消费者

import com.rabbitmq.client.Channel;
import com.work.rabbitmq.util.RabbitMqUtils;

import java.io.IOException;

public class ConsumerTopic1 {
    private final static String QUEUE_NAME = "test_topic1";

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitMqUtils.getChannel();

        channel.basicConsume(QUEUE_NAME, true, (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println("ConsumerTopic1===>"+message);
        }, (consumerTag) -> {
            System.out.println("消息消费被中断");
        });
    }
}

可以看到,执行了一次生产者代码后在rabbitmq界面就可以看到queues中test_topic1已经有了一条可消费信息

SpringBoot整合RabbitMQ

SpringBoot提供了快速整合RabbitMQ的方式

基本信息再yml中配置,队列交互机以及绑定关系在配置类中使用Bean的方式配置

生产端直接注入RabbitTemplate完成消息发送

消费端直接使用@RabbitListener完成消息接收

 引入依赖
 <!--rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

生产者

在application.properties添加基础配置

# rabbitmq配置
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/ems

编写rabbitconfig文件

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMqConfig {

    private final static String EXCHANGE_NAME = "bootTopicExchanger";
    private final static String QUEUE_NAME = "bootTopicQueue";

    /**
     * 1、交换机
     * @return
     */
    @Bean("bootExchanger")
    public Exchange bootExchanger() {
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    /**
     *  2、队列
     * @return
     */
    @Bean("bootQueue")
    public Queue bootQueue() {
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

    /**
     * 3、绑定交换机/队列
     * @param exchange
     * @param queue
     * @return
     */
    @Bean
    public Binding bindingExchangerQueue(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchanger") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }

}

使用test测试一下试试

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() {
        // 发送信息
        rabbitTemplate.convertAndSend("bootTopicExchanger", "boot.info", "boot info msg !!!");
    }

}

完成后去rabbit界面查看

 消费者

在消费者服务里面添加前面的依赖和properties配置
定义RabbitMqListener监听器
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class RabbitMqListener {

    @RabbitListener(queues = "bootTopicQueue")
    public void ListenerQueue(Message message) {
        System.out.println(message);
        // business code info
    }
}

启动application可以看到已经把消费到的信息在控制台打印出来

 

RPC模式

  尽管 RPC 在计算中是一种非常常见的模式,但它经常受到批评。当程序员不知道函数调用是本地的还是缓慢的 RPC 时,就会出现问题。像这样的混乱会导致系统不可预测,并为调试增加不必要的复杂性。滥用 RPC 可能会导致无法维护的意大利面条式代码,而不是简化软件。

  关于 RabbitMQ 实现 RPC 调用,有的小伙伴可能会有一些误解,心想这还不简单?搞两个消息队列 queue_1 和 queue_2,首先客户端发送消息到 queue_1 上,服务端监听 queue_1 上的消息,收到之后进行处理;处理完成后,服务端发送消息到 queue_2 队列上,然后客户端监听 queue_2 队列上的消息,这样就知道服务端的处理结果了。

这种方式不是不可以,就是有点麻烦!RabbitMQ 中提供了现成的方案可以直接使用,非常方便。接下来我们就一起来学习下。

考虑到这一点,请考虑以下建议:

  • 确保清楚哪个函数调用是本地的,哪个是远程的。
  • 记录您的系统。明确组件之间的依赖关系。
  • 处理错误情况。RPC服务器长时间宕机时,客户端应该如何反应?

一个简单的案例:

基础的rabbit配置信息

server.port=8082

spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/ems
#确认方式
spring.rabbitmq.publisher-confirm-type=correlated
# 发送信息失败回退
spring.rabbitmq.publisher-returns=true
 消费者:

1、消息发送调用 sendAndReceive 方法,该方法自带返回值,返回值就是服务端返回的消息。

2、服务端返回的消息中,头信息中包含了 spring_returned_message_correlation 字段,这个就是消息发送时候的 correlation_id,通过消息发送时候的 correlation_id 以及返回消息头中的 spring_returned_message_correlation 字段值,我们就可以将返回的消息内容和发送的消息绑定到一起,确认出这个返回的内容就是针对这个发送的消息的。

这就是整个客户端的开发,其实最最核心的就是 sendAndReceive 方法的调用。调用虽然简单,但是准备工作还是要做足够。例如如果我们没有在 application.properties 中配置 correlated,发送的消息中就没有 correlation_id,这样就无法将返回的消息内容和发送的消息内容关联起来。

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMqConfig {

    /**
     * 发送队列
     */
    public static final String RPC_MSG_QUEUE = "rpc_msg_queue";
    /**
     * 回复队列
     */
    public static final String RPC_REPLY_QUEUE = "rpc_reply_queue";
    /**
     * topic交换机
     */
    public static final String RPC_EXCHANGE = "rpc_topic_exchange";

    @Bean
    public Queue msgQueue() {
        return QueueBuilder.durable(RPC_MSG_QUEUE).build();
    }

    @Bean
    public Queue replyQueue() {
        return QueueBuilder.durable(RPC_REPLY_QUEUE).build();
    }

    @Bean
    public Exchange topicExchange() {
        return ExchangeBuilder.topicExchange(RPC_EXCHANGE).build();
    }

    @Bean
    public Binding msgBinding() {
        return BindingBuilder.bind(msgQueue()).to(topicExchange()).with(RPC_MSG_QUEUE).noargs();
    }

    @Bean
    public Binding replyBinding() {
        return BindingBuilder.bind(replyQueue()).to(topicExchange()).with(RPC_REPLY_QUEUE).noargs();
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setReplyAddress(RPC_REPLY_QUEUE);
        rabbitTemplate.setReplyTimeout(3000);
        return rabbitTemplate;
    }

    @Bean
    public SimpleMessageListenerContainer replyContainer(ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer(connectionFactory);
        simpleMessageListenerContainer.setQueueNames(RPC_REPLY_QUEUE);
        simpleMessageListenerContainer.setMessageListener(rabbitTemplate(connectionFactory));
        return simpleMessageListenerContainer;
    }

}

添加一个controller用来发起调用

@RestController
@RequestMapping
public class HelloWordController {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @GetMapping("/send")
    public void send(String msg) {
        // 消息对象
        Message message = MessageBuilder.withBody(msg.getBytes()).build();
        // server 给出的响应
        Message receive = rabbitTemplate.sendAndReceive(RabbitMqConfig.RPC_EXCHANGE, RabbitMqConfig.RPC_MSG_QUEUE, message);
        System.out.println(receive);
        if (null != receive) {
            // 发送的id
            String correlationId = message.getMessageProperties().getCorrelationId();
            System.out.println("发送的id" + correlationId);
            String correlationId1 = receive.getMessageProperties().getCorrelationId();
            System.out.println("返回的id" + correlationId1);
        }
    }
}

生产者(服务端):只需要监听下发送信息的队列并响应到对应的队列

import com.rabbit.server.config.RabbitMqConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class RpcServer {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @RabbitListener(queues = RabbitMqConfig.RPC_MSG_QUEUE)
    public void process(Message message) {
        byte[] body = message.getBody();
        // 一条新的消息
        Message message1 = MessageBuilder.withBody(("RabbitMqServerApplication收到了你发送的信息===>" + body.toString()).getBytes()).build();
        CorrelationData correlationData = new CorrelationData(message.getMessageProperties().getCorrelationId());
        rabbitTemplate.sendAndReceive(RabbitMqConfig.RPC_EXCHANGE, RabbitMqConfig.RPC_REPLY_QUEUE, message1, correlationData);
    }
}

 RabbitMQ信息有效期

TTL 的设置有两种不同的方式:

  1. 在声明队列的时候,我们可以在队列属性中设置消息的有效期,这样所有进入该队列的消息都会有一个相同的有效期。
  2. 在发送消息的时候设置消息的有效期,这样不同的消息就具有不同的有效期。

那如果两个都设置了呢?

以时间短的为准。easyヽ( ̄▽ ̄)ノ 

 单条信息设置过期时间

 只需要在构建发送的消息时setExpiration("时间")就完成了单条信息设置过期时间
当在过期时间内没有消费者去消费他时,会自动在设定时间后删除
Message message = MessageBuilder.withBody(msg.getBytes())
// 设置过期时间毫秒
.setExpiration("3000")
.build();

 消息队列设置过期时间

 在创建队列时设置一个过期时间,在设置时间内没有去队列消费,所有消息都会过期

HashMap<String, Object> arg = new HashMap<>();
arg.put("x-message-ttl", 1000);
return new Queue(TTL_QUEUE, true, false, false, arg);

死信交换机

死信队列:DLX,dead-letter-exchange

利用DLX,当消息在一个队列中变成死信 (Dead Message) 之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX

死信交换机用来接收死信消息的,那什么是死信消息呢?一般消息变成死信消息有如下几种情况:

1、消息被拒绝(Basic.Reject/Basic.Nack) ,井且设置requeue 参数为false

2、消息过期

3、队列达到最大长度

当消息在一个队列中变成了死信消息后,此时就会被发送到 DLX,绑定 DLX 的消息队列则称为死信队列。

DLX 本质上也是一个普普通通的交换机,我们可以为任意队列指定 DLX,当该队列中存在死信时,RabbitMQ 就会自动的将这个死信发布到 DLX 上去,进而被路由到另一个绑定了 DLX 的队列上(即死信队列)。

绑定了死信交换机的队列就是死信队列

 来个例子:
  首先我们来创建一个死信交换机,接着创建一个死信队列,再将死信交换机和死信队列绑定到一起
public static final String DLX_EXCHANGE_NAME = "dlx_exchange_name";
public static final String DLX_QUEUE_NAME = "dlx_queue_name";
public static final String DLX_ROUTING_KEY = "dlx_routing_key";

/**
 * 配置死信交换机
 *
 * @return
 */
@Bean
DirectExchange dlxDirectExchange() {
    return new DirectExchange(DLX_EXCHANGE_NAME, true, false);
}
/**
 * 配置死信队列
 * @return
 */
@Bean
Queue dlxQueue() {
    return new Queue(DLX_QUEUE_NAME);
}
/**
 * 绑定死信队列和死信交换机
 * @return
 */
@Bean
Binding dlxBinding() {
    return BindingBuilder.bind(dlxQueue()).to(dlxDirectExchange()).with(DLX_ROUTING_KEY);
}

接下来为消息队列配置死信交换机,实际其实就两个参数

x-dead-letter-exchange:配置死信交换机。

x-dead-letter-routing-key:配置死信 routing_key

将来发送到这个消息队列上的消息,如果发生了 nack、reject 或者过期等问题,就会被发送到 DLX 上,进而进入到与 DLX 绑定的消息队列上。

死信消息队列的消费和普通消息队列的消费并无二致:

@Bean
Queue queue() {
    Map<String, Object> args = new HashMap<>();
    //设置消息过期时间
    args.put("x-message-ttl", 0);
    //设置死信交换机
    args.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
    //设置死信 routing_key
    args.put("x-dead-letter-routing-key", DLX_ROUTING_KEY);
    return new Queue("demo_java" , true, false, false, args);
}

RabbitMQ 实现延迟队列

  定时任务各种各样,常见的定时任务例如日志备份,我们可能在每天凌晨 3 点去备份,这种固定时间的定时任务我们一般采用 cron 表达式就能轻松的实现,还有一些比较特殊的定时任务,向大家看电影中的定时炸弹,3分钟后爆炸,这种定时任务就不太好用 cron 去描述,因为开始时间不确定,我们开发中有的时候也会遇到类似的需求,例如:

1、在电商项目中,当我们下单之后,一般需要 20 分钟之内或者 30 分钟之内付款,否则订单就会进入异常处理逻辑中,被取消,那么进入到异常处理逻辑中,就可以当成是一个延迟队列。

2、安全工单超过 24 小时未处理,则自动拉企业微信群提醒相关责任人
 
 。。。。。
 
 很多场景下我们都需要延迟队列

用插件

首先我们需要下载 rabbitmq_delayed_message_exchange 插件,这是一个 GitHub 上的开源项目,我们直接下载。
选择适合自己的版本,我这里选择最新的
下载完成后在命令行执行如下命令将下载文件拷贝到 Docker 容器中去
docker cp ./rabbitmq_delayed_message_exchange-3.10.7.ez some-rabbit:/plugins

接下来再执行如下命令进入到 RabbitMQ 容器中

docker exec -it some-rabbit /bin/bash
 进入到容器之后,执行如下命令启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

启用成功之后,还可以通过命令查看所有安装的插件,看看是否有我们刚刚安装过的插件rabbitmq-plugins list

 这里主要是交换机的定义有所不同,小伙伴们需要注意。
@Bean
    CustomExchange customExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(EXCHANGE_NAME, EXCHANGE_TYPE, true, false,args);
    }

这里我们使用的交换机是 CustomExchange,这是一个 Spring 中提供的交换机,创建 CustomExchange 时有五个参数,含义分别如下:

  • 交换机名称。
  • 交换机类型,这个地方是固定的。
  • 交换机是否持久化。
  • 如果没有队列绑定到交换机,交换机是否删除。
  • 其他参数。

最后一个 args 参数中,指定了交换机消息分发的类型,这个类型就是大家熟知的 direct、fanout、topic 以及 header 几种,用了哪种类型,将来交换机分发消息就按哪种方式来。

DLX 实现延迟队列

假如一条消息需要延迟 30 分钟执行,我们就设置这条消息的有效期为 30 分钟,同时为这条消息配置死信交换机和死信 routing_key,并且不为这个消息队列设置消费者,那么 30 分钟后,这条消息由于没有被消费者消费而进入死信队列,此时我们有一个消费者就在“蹲点”这个死信队列,消息一进入死信队列,就立马被消费了。

这就是延迟队列的实现思路,是不是比上面那个要简单许多。

 消息可靠性

RabbitMQ 消息发送机制

大家知道,RabbitMQ 中的消息发送引入了 Exchange(交换机)的概念,消息的发送首先到达交换机上,然后再根据既定的路由规则,由交换机将消息路由到不同的 Queue(队列)中,再由不同的消费者去消费。

 

大致的流程就是这样,所以要确保消息发送的可靠性,主要从两方面去确认:

  1. 消息成功到达 Exchange
  2. 消息成功到达 Queue

如果能确认这两步,那么我们就可以认为消息发送成功了。

如果这两步中任一步骤出现问题,那么消息就没有成功送达,此时我们可能要通过重试等方式去重新发送消息,多次重试之后,如果消息还是不能到达,则可能就需要人工介入了。

经过上面的分析,我们可以确认,要确保消息成功发送,我们只需要做好三件事就可以了:

  1. 确认消息到达 Exchange。
  2. 确认消息到达 Queue。
  3. 开启定时任务,定时投递那些发送失败的消息。

RabbitMQ 的努力

上面提出的三个步骤,第三步需要我们自己实现,前两步 RabbitMQ 则有现成的解决方案。

如何确保消息成功到达 RabbitMQ?RabbitMQ 给出了两种方案:

  1. 开启事务机制
  2. 发送方确认机制

这是两种不同的方案,不可以同时开启,只能选择其中之一,如果两者同时开启,则会报如下错误

我们分别来看。以下所有案例都在 Spring Boot 中展开,文末可以下载相关源码

开启事务机制

Spring Boot 中开启 RabbitMQ 事务机制的方式如下:

首先需要先提供一个事务管理器

@Bean
RabbitTransactionManager transactionManager(ConnectionFactory connectionFactory) {
    return new RabbitTransactionManager(connectionFactory);
}

在消息生产者上面做两件事:添加事务注解并设置通信信道为事务模式

  1. 发送消息的方法上添加 @Transactional 注解标记事务。
  2. 调用 setChannelTransacted 方法设置为 true 开启事务模式。
@Service
public class MsgService {
    @Autowired
    RabbitTemplate rabbitTemplate;

    @Transactional
    public void send() {
        rabbitTemplate.setChannelTransacted(true);
        rabbitTemplate.convertAndSend(RabbitConfig.JAVABOY_EXCHANGE_NAME,RabbitConfig.JAVABOY_QUEUE_NAME,"hello rabbitmq!".getBytes());
        int i = 1 / 0;
    }
}

当我们开启事务模式之后,RabbitMQ 生产者发送消息会多出四个步骤:

  1. 客户端发出请求,将信道设置为事务模式。
  2. 服务端给出回复,同意将信道设置为事务模式。
  3. 客户端发送消息。
  4. 客户端提交事务。
  5. 服务端给出响应,确认事务提交。
上面的步骤,除了第三步是本来就有的,其他几个步骤都是平白无故多出来的。所以大家看到,事务模式其实效率有点低,这并非一个最佳解决方案。我们可以想想,什么项目会用到消息中间件?一般来说都是一些高并发的项目,这个时候并发性能尤为重要。
所以,RabbitMQ 还提供了发送方确认机制(publisher confirm)来确保消息发送成功,这种方式,性能要远远高于事务模式

发送方确认机制(publisher-confirmg工作模式)

单条消息处理
我们移除刚刚关于事务的代码,然后在 application.properties 中配置开启消息发送方确认机制
#correlated:表示成功发布消息到交换器后会触发的回调方法
spring.rabbitmq.publisher-confirm-type=correlated spring.rabbitmq.publisher-returns=true

接下来我们要开启两个监听

  1. 定义配置类,实现 RabbitTemplate.ConfirmCallback 和 RabbitTemplate.ReturnsCallback 两个接口,这两个接口,前者的回调用来确定消息到达交换器,后者则会在消息路由到队列失败时被调用。
  2. 定义 initRabbitTemplate 方法并添加 @PostConstruct 注解,在该方法中为 rabbitTemplate 分别配置这两个 Callback。
@Configuration
public class RabbitConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
    public static final String JAVABOY_EXCHANGE_NAME = "javaboy_exchange_name";
    public static final String JAVABOY_QUEUE_NAME = "javaboy_queue_name";
    private static final Logger logger = LoggerFactory.getLogger(RabbitConfig.class);
    @Autowired
    RabbitTemplate rabbitTemplate;
    @Bean
    Queue queue() {
        return new Queue(JAVABOY_QUEUE_NAME);
    }
    @Bean
    DirectExchange directExchange() {
        return new DirectExchange(JAVABOY_EXCHANGE_NAME);
    }
    @Bean
    Binding binding() {
        return BindingBuilder.bind(queue())
                .to(directExchange())
                .with(JAVABOY_QUEUE_NAME);
    }

    @PostConstruct
    public void initRabbitTemplate() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.info("{}:消息成功到达交换器",correlationData.getId());
        }else{
            logger.error("{}:消息发送失败", correlationData.getId());
        }
    }

    @Override
    public void returnedMessage(ReturnedMessage returned) {
        logger.error("{}:消息未成功路由到队列",returned.getMessage().getMessageProperties().getMessageId());
    }
}

接下来我们对消息发送进行测试。首先我们尝试将消息发送到一个不存在的交换机中

rabbitTemplate.convertAndSend("RabbitConfig.JAVABOY_EXCHANGE_NAME",RabbitConfig.JAVABOY_QUEUE_NAME,"hello rabbitmq!".getBytes(),new CorrelationData(UUID.randomUUID().toString()));

注意第一个参数是一个字符串,不是变量,这个交换器并不存在,此时控制台会报如下错误

 

 接下来我们给定一个真实存在的交换器,但是给一个不存在的队列

rabbitTemplate.convertAndSend(RabbitConfig.JAVABOY_EXCHANGE_NAME,"RabbitConfig.JAVABOY_QUEUE_NAME","hello rabbitmq!".getBytes(),new CorrelationData(UUID.randomUUID().toString()));

此时第二个参数是一个字符串,不是变量。可以看到,消息虽然成功达到交换器了,但是没有成功路由到队列(因为队列不存在)

消息批量处理

其实这就是 publisher-confirm 模式。相比于事务,这种模式下的消息吞吐量会得到极大的提升。

失败重试

失败重试分两种情况,一种是压根没找到 MQ 导致的失败重试,另一种是找到 MQ 了,但是消息发送失败了。

自带重试机制
前面所说的事务机制和发送方确认机制,都是发送方确认消息发送成功的办法。如果发送方一开始就连不上 MQ,那么 Spring Boot 中也有相应的重试机制,但是这个重试机制就和 MQ 本身没有关系了,这是利用 Spring 中的 retry 机制来完成的
#开启重试机制
spring.rabbitmq.template.retry.enabled=true
#重试起始间隔时间
spring.rabbitmq.template.retry.initial-interval=1000ms #最大重试次数
spring.rabbitmq.template.retry.max
-attempts=10 #最大重试间隔时间
spring.rabbitmq.template.retry.max
-interval=10000ms #间隔时间乘数。(这里配置间隔时间乘数为 2,则第一次间隔时间 1 秒,第二次重试间隔时间 2 秒,第三次 4 秒,以此类推)
spring.rabbitmq.template.retry.multiplier
=2

配置完成后,再次启动 Spring Boot 项目,然后关掉 MQ,此时尝试发送消息,就会发送失败,进而导致自动重试

业务重试

业务重试主要是针对消息没有到达交换器的情况。如果消息没有成功到达交换器,根据我们第二小节的讲解,此时就会触发消息发送失败回调

整体思路是这样:
1、首先创建一张表,用来记录发送到中间件上的消息,每次发送消息的时候,就往数据库中添加一条记录。
  • status:表示消息的状态,有三个取值,0,1,2 分别表示消息发送中、消息发送成功以及消息发送失败。
  • tryTime:表示消息的第一次重试时间(消息发出去之后,在 tryTime 这个时间点还未显示发送成功,此时就可以开始重试了)。
  • count:表示消息重试次数。

2、在消息发送的时候,我们就往该表中保存一条消息发送记录,并设置状态 status 为 0,tryTime 为 1 分钟之后。

3、在 confirm 回调方法中,如果收到消息发送成功的回调,就将该条消息的 status 设置为1(在消息发送时为消息设置 msgId,在消息发送成功回调时,通过 msgId 来唯一锁定该条消息)。

4、另外开启一个定时任务,定时任务每隔 10s 就去数据库中捞一次消息,专门去捞那些 status 为 0 并且已经过了 tryTime 时间记录,把这些消息拎出来后,首先判断其重试次数是否已超过 3 次,如果超过 3 次,则修改该条消息的 status 为 2,表示这条消息发送失败,并且不再重试。对于重试次数没有超过 3 次的记录,则重新去发送消息,并且为其 count 的值+1。

当然这种思路有两个弊端:

  1. 去数据库走一遭,可能拖慢 MQ 的 Qos,不过有的时候我们并不需要 MQ 有很高的 Qos,所以这个应用时要看具体情况。
  2. 按照上面的思路,可能会出现同一条消息重复发送的情况,不过这都不是事,我们在消息消费时,解决好幂等性问题就行了。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
posted @ 2022-09-01 17:09  隐琳琥  阅读(209)  评论(0编辑  收藏  举报