RabbitMQ入门及笔记

RabbitMQ

文章目录

  • RabbitMQ
    • 1. RabbitMQ的安装
    • 2. RabbitMQ的相关概念
      • 2.1 RabbitMQ的概念
      • 2.2 四大核心概念
      • 2.3 RabbitMQ 核心部分
      • 2.4 工作原理及介绍
    • 3. 入门案例
      • 3.1 RabbitMQ入门案例 - fanout模式
      • 3.2 RabbitMQ入门案例 - Direct模式
      • 3.3 RabbitMQ入门案例 - Topic模式
      • 3.4 RabbitMQ入门案例 - Work模式
      • 3.5 小结
    • 4. 消息应答
      • 4.1 概念
      • 4.2 自动应答(谨慎使用)
      • 4.3 消息应答的方法
      • 4.4 Multiple的解释
      • 4.5 消息自动重新入队
      • 4.6 消息手动应答
    • 5. 过期时间TTL
      • 5.1 概述
      • 5.2 两种方法的区别
      • 5.3 两只方法的实现
        • 5.3.1 设置队列TTL
        • 5.3.2 设置消息TTL
    • 6. 发布确认
      • 6.1 发布确认原理
      • 6.2 发布确认的策略
        • 6.2.1 开启发布确认的方法
        • 6.2.2 单个确认发布
        • 6.2.3 批量确认发布
        • 6.2.4 异步确认发布
        • 6.2.5 如何处理异步未确认消息
        • 6.2.6 以上3种发布确认速度对比
    • 7. 死信队列
      • 7.1 死信的概念
      • 7.2 死信的来源
      • 7.3 死信的实现原理
      • 7.4 在rabbitMQ管理界面中结果
    • 8. 延迟队列
      • 8.1 延迟队列概念
      • 8.2 延迟队列使用场景
      • 8.3 Rabbitmq 插件实现延迟队列
      • 8.4 总结
    • 9. springboot整合RabbitMQ案例
      • 9.1 fanout模式
      • 9.2 其他
    • 10. RabbitMQ持久化
      • 10.1 概念
      • 10.2 RabbitMQ持久化消息
      • 10.3 RabbitMQ非持久化消息
      • 10.4 RabbitMQ持久化分类
      • 10.5 RabbitMQ队列持久化的代码实现
      • 10.6 RabbitMQ消息持久化
      • 10.7 RabbitMQ交换机持久化
    • 11. RabbitMQ其他知识点
      • 11.1 幂等性
      • 11.2 优先级队列
      • 11.3 惰性队列
    • 12. RabbitMQ的集群
      • 12.1 RabbitMQ集群的概述
      • 12.2 集群的搭建
      • 12.3 单机多实例搭建
        • 12.3.1 启动第一个节点rabbit-1
        • 12.3.2 启动第二个节点rabbit-2
        • 12.3.3 验证启动
        • 12.3.4 rabbit-1操作作为主节点
        • 12.3.5 rabbit2操作为从节点
        • 12.3.6 验证集群状态
        • 12.3.7 Web监控
      • 12.4 多机部署的方式
      • 12.5 Springboot整合rabbitmq集群配置
    • 13. 分布式事务
      • 13.1 分布式事务的方式
        • 13.1.1两阶段提交(2PC)需要数据库产商的支持,java组件有atomikos等。
        • 13.1.2 补偿事务(TCC)
        • 13.1.3 本地消息表(异步确保)
        • 13.1.4 MQ 事务消息 异步场景,通用性较强,拓展性较高
        • 13.4.5 总结
    • 14. RabbitMQ集群监控
      • 14.1 管理界面监控
      • 14.2 tracing日志监控
        • 14.2.1 消息追踪启用与查看
        • 14.2.2 日志追踪
      • 14.3 Zabbix 监控RabbitMQ
    • 15. 关于RabbitMQ的面试题

1. RabbitMQ的安装

请查看我的这篇文章——RabbitMQ的保姆级安装

2. RabbitMQ的相关概念

2.1 RabbitMQ的概念

  • RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是一个快递站,一个快递员帮你传递快件。RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据

2.2 四大核心概念

  • 生产者(producer):产生数据发送消息的程序是生产者
  • 交换机(exchange):交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定
  • 队列(queue):队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式
  • 消费者(comsumer):消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。

2.3 RabbitMQ 核心部分

RabbitMQ入门及笔记_第1张图片

RabbitMQ入门及笔记_第2张图片

2.4 工作原理及介绍

RabbitMQ入门及笔记_第3张图片

  • Broker:接收和分发消息的应用,RabbitMQ Server 就是 Message Broker

  • Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等

  • Connection:publisher/consumer 和 broker 之间的 TCP 连接

  • Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的Connection 极大减少了操作系统建立 TCP connection 的开销

    面试题:rabbitmq为什么是基于channel(通道)去处理而不是连接?
    一个应用有多个线程需要从rabbitmq中消费,或是生产消息,如果建立很多个Connection连接,对操作系统而言,建立和销毁tcp连接是很昂贵的开销,如果遇到使用高峰,性能瓶颈也随之显现。
    rabbitmq采用类似nio的做法,连接tcp连接复用,不仅可以减少性能开销,同时也便于管理。
    
  • Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout(multicast)

  • Queue:消息最终被送到这里等待 consumer 取走

  • Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

3. 入门案例

3.1 RabbitMQ入门案例 - fanout模式

  • RabbitMQ的模式之发布订阅模式
  • 特点:
    • web操作查看视频
    • 类型:fanout
    • 特点:Fanout—发布与订阅模式,是一种广播机制,它是没有路由key的模式。

RabbitMQ入门及笔记_第4张图片

代码实现:

//生产者
public class Producer {
    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("192.168.10.100");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 6: 准备发送消息的内容
            String message = "你好,fanout";
            String  exchangeName = "fanout-exchange";
            String routingKey = "";
            // 7: 发送消息给中间件rabbitmq-server
            // @params1: 交换机exchange
            // @params2: 队列名称/routingkey
            // @params3: 属性配置
            // @params4: 发送消息的内容
            channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
            System.out.println("消息发送成功!");
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}
//消费者
public class Consumer {
    private static Runnable runnable = () -> {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("192.168.10.100");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        //获取队列的名称
        final String queueName = Thread.currentThread().getName();
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            /*
             *  如果队列不存在,则会创建
             *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
             *
             *  @params1: queue 队列的名称
             *  @params2: durable 队列是否持久化
             *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
             *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
             *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
             * */
            // 这里如果queue已经被创建过一次了,可以不需要定义
            //channel.queueDeclare("queue1", false, false, false, null);
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
            finalChannel.basicConsume(queueName, true, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    System.out.println(queueName + ":收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                }
            });
            System.out.println(queueName + ":开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    };
    public static void main(String[] args) {
        // 启动三个线程去执行
        new Thread(runnable, "queue1").start();
        new Thread(runnable, "queue2").start();
        new Thread(runnable, "queue3").start();
    }
}

3.2 RabbitMQ入门案例 - Direct模式

  • Direct模式RabbitMQ的模式之Direct模式
  • 特点:
    • web操作查看视频
    • 类型:direct
    • 特点:Direct模式是fanout模式上的一种叠加,增加了路由RoutingKey的模式。

RabbitMQ入门及笔记_第5张图片

代码实现

//生产者
public class Producer {
    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("192.168.10.100");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 6: 准备发送消息的内容
            String message = "你好,direct";
            String  exchangeName = "direct-exchange";
            String routingKey1 = "testkey";
            String routingKey2 = "testkey2";
            // 7: 发送消息给中间件rabbitmq-server
            // @params1: 交换机exchange
            // @params2: 队列名称/routingkey
            // @params3: 属性配置
            // @params4: 发送消息的内容
            channel.basicPublish(exchangeName, routingKey1, null, message.getBytes());
            channel.basicPublish(exchangeName, routingKey2, null, message.getBytes());
            System.out.println("消息发送成功!");
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}
//消费者
public class Consumer {
    private static Runnable runnable = () -> {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("192.168.10.100");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        //获取队列的名称
        final String queueName = Thread.currentThread().getName();
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            /*
             *  如果队列不存在,则会创建
             *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
             *
             *  @params1: queue 队列的名称
             *  @params2: durable 队列是否持久化
             *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
             *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
             *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
             * */
            // 这里如果queue已经被创建过一次了,可以不需要定义
            //channel.queueDeclare("queue1", false, false, false, null);
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
            finalChannel.basicConsume(queueName, true, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    System.out.println(queueName + ":收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                }
            });
            System.out.println(queueName + ":开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    };
    public static void main(String[] args) {
        // 启动三个线程去执行
        new Thread(runnable, "queue1").start();
        new Thread(runnable, "queue2").start();
        new Thread(runnable, "queue3").start();
    }
}

3.3 RabbitMQ入门案例 - Topic模式

  • RabbitMQ的模式之Topic模式
  • 特点
    • web操作查看视频
    • 类型:topic
    • 特点:Topic模式是direct模式上的一种叠加,增加了模糊路由RoutingKey的模式

代码实现

//生产者
public class Producer {
    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("192.168.10.100");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 6: 准备发送消息的内容
            String message = "你好,Topic";
            String  exchangeName = "topic-exchange";
            String routingKey1 = "com.course.order";//都可以收到 queue1 queue2 queue3
            String routingKey2 = "com.order.user";//都可以收到 queue1 queue3
            // 7: 发送消息给中间件rabbitmq-server
            // @params1: 交换机exchange
            // @params2: 队列名称/routingkey
            // @params3: 属性配置
            // @params4: 发送消息的内容
            channel.basicPublish(exchangeName, routingKey1, null, message.getBytes());
            System.out.println("消息发送成功!");
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}
//消费者
public class Consumer {
    private static Runnable runnable = () -> {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("192.168.10.100");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        //获取队列的名称
        final String queueName = Thread.currentThread().getName();
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            /*
             *  如果队列不存在,则会创建
             *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
             *
             *  @params1: queue 队列的名称
             *  @params2: durable 队列是否持久化
             *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
             *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
             *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
             * */
            // 这里如果queue已经被创建过一次了,可以不需要定义
            //channel.queueDeclare("queue1", false, false, false, null);
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
            finalChannel.basicConsume(queueName, true, new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    System.out.println(queueName + ":收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                }
            }, new CancelCallback() {
                @Override
                public void handle(String s) throws IOException {
                }
            });
            System.out.println(queueName + ":开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    };
    public static void main(String[] args) {
        // 启动三个线程去执行
        new Thread(runnable, "queue1").start();
        new Thread(runnable, "queue2").start();
        new Thread(runnable, "queue3").start();
    }
}

3.4 RabbitMQ入门案例 - Work模式

  • 当有多个消费者时,我们的消息会被哪个消费者消费呢,我们又该如何均衡消费者消费信息的多少呢?

  • 主要有两种模式:

    1. 轮询模式的分发:一个消费者一条,按均分配。(与消费者处理消息的能力无关)
    2. 公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;能者多劳
  • 轮询模式(Round-Robin)代码实现

    //生产者
    public class Producer {
        public static void main(String[] args) {
            // 1: 创建连接工厂
            ConnectionFactory connectionFactory = new ConnectionFactory();
            // 2: 设置连接属性
            connectionFactory.setHost("192.168.10.100");
            connectionFactory.setPort(5672);
            connectionFactory.setVirtualHost("/");
            connectionFactory.setUsername("admin");
            connectionFactory.setPassword("admin");
            Connection connection = null;
            Channel channel = null;
            try {
                // 3: 从连接工厂中获取连接
                connection = connectionFactory.newConnection("生产者");
                // 4: 从连接中获取通道channel
                channel = connection.createChannel();
                // 6: 准备发送消息的内容
                //=================end topic模式=================
                for (int i = 1; i <= 20; i++) {
                    //消息的内容
                    String msg = "轮询模式:" + i;
                    // 7: 发送消息给中间件rabbitmq-server
                    // @params1: 交换机exchange
                    // @params2: 队列名称/routingkey
                    // @params3: 属性配置
                    // @params4: 发送消息的内容
                    channel.basicPublish("", "queue1", null, msg.getBytes());
                }
                System.out.println("消息发送成功!");
            } catch (Exception ex) {
                ex.printStackTrace();
                System.out.println("发送消息出现异常...");
            } finally {
                // 7: 释放连接关闭通道
                if (channel != null && channel.isOpen()) {
                    try {
                        channel.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
                if (connection != null) {
                    try {
                        connection.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }
    
    //消费者-Wokr1
    public class Work1 {
        public static void main(String[] args) {
            // 1: 创建连接工厂
            ConnectionFactory connectionFactory = new ConnectionFactory();
            // 2: 设置连接属性
            connectionFactory.setHost("192.168.10.100);
            connectionFactory.setPort(5672);
            connectionFactory.setVirtualHost("/");
            connectionFactory.setUsername("admin");
            connectionFactory.setPassword("admin");
            Connection connection = null;
            Channel channel = null;
            try {
                // 3: 从连接工厂中获取连接
                connection = connectionFactory.newConnection("消费者-Work1");
                // 4: 从连接中获取通道channel
                channel = connection.createChannel();
                // 5: 申明队列queue存储消息
                /*
                 *  如果队列不存在,则会创建
                 *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
                 *
                 *  @params1: queue 队列的名称
                 *  @params2: durable 队列是否持久化
                 *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
                 *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
                 *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
                 * */
                // 这里如果queue已经被创建过一次了,可以不需要定义
    //            channel.queueDeclare("queue1", false, false, false, null);
                // 同一时刻,服务器只会推送一条消息给消费者
                // 6: 定义接受消息的回调
                Channel finalChannel = channel;
                finalChannel.basicQos(1);
                finalChannel.basicConsume("queue1", true, new DeliverCallback() {
                    @Override
                    public void handle(String s, Delivery delivery) throws IOException {
                        try{
                            System.out.println("Work1-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                            Thread.sleep(2000);
                        }catch(Exception ex){
                            ex.printStackTrace();
                        }
                    }
                }, new CancelCallback() {
                    @Override
                    public void handle(String s) throws IOException {
                    }
                });
                System.out.println("Work1-开始接受消息");
                System.in.read();
            } catch (Exception ex) {
                ex.printStackTrace();
                System.out.println("发送消息出现异常...");
            } finally {
                // 7: 释放连接关闭通道
                if (channel != null && channel.isOpen()) {
                    try {
                        channel.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
                if (connection != null && connection.isOpen()) {
                    try {
                        connection.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }
    
    //消费者-Wokr2
    public class Work2 {
        public static void main(String[] args) {
            // 1: 创建连接工厂
            ConnectionFactory connectionFactory = new ConnectionFactory();
            // 2: 设置连接属性
            connectionFactory.setHost("47.104.141.27");
            connectionFactory.setPort(5672);
            connectionFactory.setVirtualHost("/");
            connectionFactory.setUsername("admin");
            connectionFactory.setPassword("admin");
            Connection connection = null;
            Channel channel = null;
            try {
                // 3: 从连接工厂中获取连接
                connection = connectionFactory.newConnection("消费者-Work2");
                // 4: 从连接中获取通道channel
                channel = connection.createChannel();
                // 5: 申明队列queue存储消息
                /*
                 *  如果队列不存在,则会创建
                 *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
                 *
                 *  @params1: queue 队列的名称
                 *  @params2: durable 队列是否持久化
                 *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
                 *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
                 *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
                 * */
                // 这里如果queue已经被创建过一次了,可以不需要定义
                //channel.queueDeclare("queue1", false, true, false, null);
                // 同一时刻,服务器只会推送一条消息给消费者
                //channel.basicQos(1);
                // 6: 定义接受消息的回调
                Channel finalChannel = channel;
                finalChannel.basicQos(1);
                finalChannel.basicConsume("queue1", true, new DeliverCallback() {
                    @Override
                    public void handle(String s, Delivery delivery) throws IOException {
                        try{
                            System.out.println("Work2-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                            Thread.sleep(200);
                        }catch(Exception ex){
                            ex.printStackTrace();
                        }
                    }
                }, new CancelCallback() {
                    @Override
                    public void handle(String s) throws IOException {
                    }
                });
                System.out.println("Work2-开始接受消息");
                System.in.read();
            } catch (Exception ex) {
                ex.printStackTrace();
                System.out.println("发送消息出现异常...");
            } finally {
                // 7: 释放连接关闭通道
                if (channel != null && channel.isOpen()) {
                    try {
                        channel.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
                if (connection != null && connection.isOpen()) {
                    try {
                        connection.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }
    
  • 公平分发(Fair Dispatch)代码实现

    //生产者
    public class Producer {
        public static void main(String[] args) {
            // 1: 创建连接工厂
            ConnectionFactory connectionFactory = new ConnectionFactory();
            // 2: 设置连接属性
            connectionFactory.setHost("192.168.10.100");
            connectionFactory.setPort(5672);
            connectionFactory.setVirtualHost("/");
            connectionFactory.setUsername("admin");
            connectionFactory.setPassword("admin");
            Connection connection = null;
            Channel channel = null;
            try {
                // 3: 从连接工厂中获取连接
                connection = connectionFactory.newConnection("生产者");
                // 4: 从连接中获取通道channel
                channel = connection.createChannel();
                // 6: 准备发送消息的内容
                //==================end topic模式=====================
                for (int i = 1; i <= 20; i++) {
                    //消息的内容
                    String msg = "学相伴:" + i;
                    // 7: 发送消息给中间件rabbitmq-server
                    // @params1: 交换机exchange
                    // @params2: 队列名称/routingkey
                    // @params3: 属性配置
                    // @params4: 发送消息的内容
                    channel.basicPublish("", "queue1", null, msg.getBytes());
                }
                System.out.println("消息发送成功!");
            } catch (Exception ex) {
                ex.printStackTrace();
                System.out.println("发送消息出现异常...");
            } finally {
                // 7: 释放连接关闭通道
                if (channel != null && channel.isOpen()) {
                    try {
                        channel.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
                if (connection != null) {
                    try {
                        connection.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }
    
    //消费者-Work1
    public class Work1 {
        public static void main(String[] args) {
            // 1: 创建连接工厂
            ConnectionFactory connectionFactory = new ConnectionFactory();
            // 2: 设置连接属性
            connectionFactory.setHost("47.104.141.27");
            connectionFactory.setPort(5672);
            connectionFactory.setVirtualHost("/");
            connectionFactory.setUsername("admin");
            connectionFactory.setPassword("admin");
            Connection connection = null;
            Channel channel = null;
            try {
                // 3: 从连接工厂中获取连接
                connection = connectionFactory.newConnection("消费者-Work1");
                // 4: 从连接中获取通道channel
                channel = connection.createChannel();
                // 5: 申明队列queue存储消息
                /*
                 *  如果队列不存在,则会创建
                 *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
                 *
                 *  @params1: queue 队列的名称
                 *  @params2: durable 队列是否持久化
                 *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
                 *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
                 *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
                 * */
                // 这里如果queue已经被创建过一次了,可以不需要定义
    //            channel.queueDeclare("queue1", false, false, false, null);
                // 同一时刻,服务器只会推送一条消息给消费者
                // 6: 定义接受消息的回调
                Channel finalChannel = channel;
                finalChannel.basicQos(1);
                finalChannel.basicConsume("queue1", false, new DeliverCallback() {
                    @Override
                    public void handle(String s, Delivery delivery) throws IOException {
                        try{
                            System.out.println("Work1-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                            Thread.sleep(2000);
                            finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
                        }catch(Exception ex){
                            ex.printStackTrace();
                        }
                    }
                }, new CancelCallback() {
                    @Override
                    public void handle(String s) throws IOException {
                    }
                });
                System.out.println("Work1-开始接受消息");
                System.in.read();
            } catch (Exception ex) {
                ex.printStackTrace();
                System.out.println("发送消息出现异常...");
            } finally {
                // 7: 释放连接关闭通道
                if (channel != null && channel.isOpen()) {
                    try {
                        channel.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
                if (connection != null && connection.isOpen()) {
                    try {
                        connection.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }
    
    //消费者-Wokr2
    public class Work2 {
        public static void main(String[] args) {
            // 1: 创建连接工厂
            ConnectionFactory connectionFactory = new ConnectionFactory();
            // 2: 设置连接属性
            connectionFactory.setHost("47.104.141.27");
            connectionFactory.setPort(5672);
            connectionFactory.setVirtualHost("/");
            connectionFactory.setUsername("admin");
            connectionFactory.setPassword("admin");
            Connection connection = null;
            Channel channel = null;
            try {
                // 3: 从连接工厂中获取连接
                connection = connectionFactory.newConnection("消费者-Work2");
                // 4: 从连接中获取通道channel
                channel = connection.createChannel();
                // 5: 申明队列queue存储消息
                /*
                 *  如果队列不存在,则会创建
                 *  Rabbitmq不允许创建两个相同的队列名称,否则会报错。
                 *
                 *  @params1: queue 队列的名称
                 *  @params2: durable 队列是否持久化
                 *  @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
                 *  @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
                 *  @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
                 * */
                // 这里如果queue已经被创建过一次了,可以不需要定义
                //channel.queueDeclare("queue1", false, true, false, null);
                // 同一时刻,服务器只会推送一条消息给消费者
                //channel.basicQos(1);
                // 6: 定义接受消息的回调
                Channel finalChannel = channel;
                finalChannel.basicQos(1);
                finalChannel.basicConsume("queue1", false, new DeliverCallback() {
                    @Override
                    public void handle(String s, Delivery delivery) throws IOException {
                        try{
                            System.out.println("Work2-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
                            Thread.sleep(200);
                            finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
                        }catch(Exception ex){
                            ex.printStackTrace();
                        }
                    }
                }, new CancelCallback() {
                    @Override
                    public void handle(String s) throws IOException {
                    }
                });
                System.out.println("Work2-开始接受消息");
                System.in.read();
            } catch (Exception ex) {
                ex.printStackTrace();
                System.out.println("发送消息出现异常...");
            } finally {
                // 7: 释放连接关闭通道
                if (channel != null && channel.isOpen()) {
                    try {
                        channel.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
                if (connection != null && connection.isOpen()) {
                    try {
                        connection.close();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
    }
    

3.5 小结

  • 当队列里消息较多时,我们通常会开启多个消费者处理消息;公平分发和轮询分发都是我们经常使用的模式。
  • 轮询分发的主要思想是“按均分配”,不考虑消费者的处理能力,所有消费者均分;这种情况下,处理能力弱的服务器,一直都在处理消息,而处理能力强的服务器,在处理完消息后,处于空闲状态;
  • 公平分发的主要思想是”能者多劳”,按需分配,能力强的干的多。

4. 消息应答

4.1 概念

  • 消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。

  • 为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了

4.2 自动应答(谨慎使用)

  • 消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失 了,当然另一方面这种模消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用

4.3 消息应答的方法

  1. Channel.basicAck(用于肯定确认): RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
  2. Channel.basicNack(用于否定确认)
  3. Channel.basicReject(用于否定确认):与 Channel.basicNack相比少一个参数,不处理该消息了直接拒绝,可以将其丢弃了

4.4 Multiple的解释

  • 手动应答的好处是可以批量应答并且减少网络拥堵

RabbitMQ入门及笔记_第6张图片

  • multiple 的 true 和 false 代表不同意思:
    • true 代表批量应答 channel 上未应答的消息:比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是8 那么此时5-8 的这些还未应答的消息都会被确认收到消息应答
    • false 同上面相比:只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答

4.5 消息自动重新入队

  • 如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。

4.6 消息手动应答

  • 默认消息采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答

RabbitMQ入门及笔记_第7张图片

5. 过期时间TTL

5.1 概述

  • 过期时间TTL表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置TTL。目前有两种方法可以设置:
    1. 第一种方法是通过队列属性设置队列中所有消息都有相同的过期时间
    2. 第二种方法是对消息进行单独设置,每条消息TTL可以不同。
  • 如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为dead message被投递到死信队列, 消费者将无法再收到该消息。

5.2 两种方法的区别

  • 如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃

5.3 两只方法的实现

5.3.1 设置队列TTL
//设置队列TTL
public class Producer {
    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("192.168.10.100");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            Map<String,Object> args2 = new HashMap<>();
            //设置参数:过期时间5000ms
            args2.put("x-message-ttl",5000);
            channel.queueDeclare("ttl.queue", true, false, false, args2);
            // 6: 准备发送消息的内容
            String message = "你好,ttl";
            Map<String, Object> headers = new HashMap<String, Object>();
            headers.put("x", "1");
            headers.put("y", "1");
            AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
                    .deliveryMode(2) // 传送方式
                    .priority(1)
                    .contentEncoding("UTF-8") // 编码方式
                    .expiration("3000") // 过期时间
                    .headers(headers).build(); //自定义属性
            // 7: 发送消息给中间件rabbitmq-server
            // @params1: 交换机exchange
            // @params2: 队列名称/routing
            // @params3: 属性配置
            // @params4: 发送消息的内容
            for (int i = 0; i <100 ; i++) {
                channel.basicPublish("", "ttl.queue", basicProperties, message.getBytes());
                System.out.println("消息发送成功!");
                Thread.sleep(1000);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}
//测试
public class Consumer {
    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("47.104.141.27");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("消费者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            // 这里如果queue已经被创建过一次了,可以不需要定义
            //channel.queueDeclare("queue1", false, false, false, null);
            // 6: 定义接受消息的回调
            Channel finalChannel = channel;
            finalChannel.basicConsume("ttl.queue", true, new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println(properties);
                    System.out.println("获取的消息是:" + new String(body,"UTF-8"));
                }
            });
            System.out.println("开始接受消息");
            System.in.read();
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}
  • 注意:参数 x-message-ttl 的值 必须是非负 32 位整数 (0 <= n <= 2^32-1) ,以毫秒为单位表示 TTL 的值。这样,值 6000 表示存在于 队列 中的当前 消息 将最多只存活 6 秒钟。

RabbitMQ入门及笔记_第8张图片

5.3.2 设置消息TTL

消息的过期时间;只需要在发送消息(可以发送到任何队列,不管该队列是否属于某个交换机)的时候设置过期时间即可。在测试类中编写如下方法发送消息并设置过期时间到队列:

public class MessageTTLProducer {
    public static void main(String[] args) {
        // 1: 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2: 设置连接属性
        connectionFactory.setHost("47.104.141.27");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3: 从连接工厂中获取连接
            connection = connectionFactory.newConnection("生产者");
            // 4: 从连接中获取通道channel
            channel = connection.createChannel();
            // 5: 申明队列queue存储消息
            channel.queueDeclare("ttl.queue2", true, false, false, null);
            // 6: 准备发送消息的内容
            String message = "你好,消息TTL";
            Map<String, Object> headers = new HashMap<String, Object>();
            headers.put("x", "1");
            headers.put("y", "1");
            AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
                    .deliveryMode(2) // 传送方式
                    .priority(1)
                    .contentEncoding("UTF-8") // 编码方式
                    .expiration("5000") // 过期时间
                    .headers(headers).build(); //自定义属性
            // 7: 发送消息给中间件rabbitmq-server
            for (int i = 0; i <10 ; i++) {
                channel.basicPublish("", "ttl.queue2", basicProperties, message.getBytes());
                System.out.println("消息发送成功!");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("发送消息出现异常...");
        } finally {
            // 7: 释放连接关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

RabbitMQ入门及笔记_第9张图片

  • 注意:expiration 字段以微秒为单位表示 TTL 值。且与 x-message-ttl 具有相同的约束条件。因为 expiration 字段必须为字符串类型,broker 将只会接受以字符串形式表达的数字。当同时指定了 queue 和 message 的 TTL 值,则两者中较小的那个才会起作用。

6. 发布确认

6.1 发布确认原理

  • 生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
  • confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

6.2 发布确认的策略

6.2.1 开启发布确认的方法
  • 发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布确认,都需要在 channel 上调用该方法。

    Channel channel = connection.createChannel();
    channel.confirmSelect();
    
6.2.2 单个确认发布
  • 这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,**waitForConfirmsOrDie(long)**这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。

  • 这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了用来说,稍微高一点的值将是最佳的。

  • 案例:

    public static void publishMessageIndividually() throws Exception {
    	try (Channel channel = RabbitMqUtils.getChannel()) {
            String queueName = UUID.randomUUID().toString();
    		channel.queueDeclare(queueName, false, false, false, null);
    		//开启发布确认
     		channel.confirmSelect();
    		long begin = System.currentTimeMillis();
    		for (int i = 0; i < MESSAGE_COUNT; i++) {
                String message = i + "";
    			channel.basicPublish("", queueName, null, message.getBytes());
    			//服务端返回 false 或超时时间内未返回,生产者可以消息重发
     			boolean flag = channel.waitForConfirms();
    			if(flag){
    				System.out.println("消息发送成功");
    			} 
            }
    		long end = System.currentTimeMillis();
    		System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms");
    	} 
    }
    
6.2.3 批量确认发布
  • 上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。

  • 案例

    public static void publishMessageBatch() throws Exception {
    	try (Channel channel = RabbitMqUtils.getChannel()) {
            String queueName = UUID.randomUUID().toString();
    		channel.queueDeclare(queueName, false, false, false, null);
    		//开启发布确认
    		channel.confirmSelect();
    		//批量确认消息大小
     		int batchSize = 100;
    		//未确认消息个数
     		int outstandingMessageCount = 0;
    		long begin = System.currentTimeMillis();
    		for (int i = 0; i < MESSAGE_COUNT; i++) {
                String message = i + "";
    			channel.basicPublish("", queueName, null, message.getBytes());
    			outstandingMessageCount++;
    			if (outstandingMessageCount == batchSize) {
                    channel.waitForConfirms();
    				outstandingMessageCount = 0; 
                } 
            }
    		//为了确保还有剩余没有确认消息 再次确认
     		if (outstandingMessageCount > 0) {
                channel.waitForConfirms();
    		}
    		long end = System.currentTimeMillis();
    		System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) +"ms");
    }
    
6.2.4 异步确认发布
  • 异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。

  • 案例

    public static void publishMessageAsync() throws Exception {
    	try (Channel channel = RabbitMqUtils.getChannel()) {
            String queueName = UUID.randomUUID().toString();
    		channel.queueDeclare(queueName, false, false, false, null);
    		//开启发布确认
    		channel.confirmSelect();
    		/**
    		 * 线程安全有序的一个哈希表,适用于高并发的情况
     		 * 1.轻松的将序号与消息进行关联
    		 * 2.轻松批量删除条目 只要给到序列号
     		 * 3.支持并发访问
     		 */
    		ConcurrentSkipListMap<Long, String> outstandingConfirms = new
    		ConcurrentSkipListMap<>();
    		/**
    		 * 确认收到消息的一个回调
     		 * 1.消息序列号
    		 * 2.true 可以确认小于等于当前序列号的消息
     		 * false 确认当前序列号消息
     		 */
    		ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
    			if (multiple) {
    			//返回的是小于等于当前序列号的未确认消息 是一个 map
    			ConcurrentNavigableMap<Long, String> confirmed =
    			outstandingConfirms.headMap(sequenceNumber, true);
    			//清除该部分未确认消息
     			confirmed.clear();
    			}else{
    			//只清除当前序列号的消息
     				outstandingConfirms.remove(sequenceNumber);
    			}
    		};
    		ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
                String message = outstandingConfirms.get(sequenceNumber);
    			System.out.println("发布的消息"+message+"未被确认,序列号"+sequenceNumber);
    		};
    		/**
    		 * 添加一个异步确认的监听器
     		 * 1.确认收到消息的回调
     		 * 2.未收到消息的回调
     		 */
    		channel.addConfirmListener(ackCallback, null);
    		long begin = System.currentTimeMillis();
    		for (int i = 0; i < MESSAGE_COUNT; i++) {
                String message = "消息" + i;
    			/**
    			 * channel.getNextPublishSeqNo()获取下一个消息的序列号
    			 * 通过序列号与消息体进行一个关联
    			 * 全部都是未确认的消息体
     			 */
    			outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
    			channel.basicPublish("", queueName, null, message.getBytes());
    		} 
    
    		long end = System.currentTimeMillis();
    		System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) 	+ "ms");
        }
    }
    
6.2.5 如何处理异步未确认消息
  • 最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。
6.2.6 以上3种发布确认速度对比
  • 单独发布消息:同步等待确认,简单,但吞吐量非常有限。
  • 批量发布消息:批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。
  • 异步处理:最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些

7. 死信队列

7.1 死信的概念

  • DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。

7.2 死信的来源

  • 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.
  • 消息TTL过期
  • 队列达到最大长度(队列满了,无法再添加数据到 mq 中)

7.3 死信的实现原理

  • DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。
  • 要想使用死信队列,只需要在定义队列的时候设置队列参数x-dead-letter-exchange指定交换机即可。

RabbitMQ入门及笔记_第10张图片

7.4 在rabbitMQ管理界面中结果

  • 未过期:

    RabbitMQ入门及笔记_第11张图片

  • 已过期:

    RabbitMQ入门及笔记_第12张图片

8. 延迟队列

8.1 延迟队列概念

  • 延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列

8.2 延迟队列使用场景

  1. 订单在十分钟之内未支付则自动取消
  2. 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  3. 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  4. 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  5. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
  • 这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

RabbitMQ入门及笔记_第13张图片

8.3 Rabbitmq 插件实现延迟队列

  • 如果不能实现在消息粒度上的 TTL,并使其在设置的TTL 时间及时死亡,就无法设计成一个通用的延时队列。那如何解决呢,接下来我们就去解决该问题

  • 在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载

    rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。

    进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ

    /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
    rabbitmq-plugins enable rabbitmq_delayed_message_exchange
    

    RabbitMQ入门及笔记_第14张图片

8.4 总结

  • 延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。

  • 当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景

9. springboot整合RabbitMQ案例

9.1 fanout模式

  1. 目标:使用springboot完成rabbitmq的消费模式-Fanout

    RabbitMQ入门及笔记_第15张图片

  2. 实现步骤:

    1. 创建生产者工程:sspringboot-rabbitmq-fanout-producer
    2. 创建消费者工程:springboot-rabbitmq-fanout-consumer
    3. 引入spring-boot-rabbitmq的依赖
    4. 进行消息的分发和测试
    5. 查看和观察web控制台的状况
  3. 具体实现

    1. 创建生产者工程:sspringboot-rabbitmq-fanout-producer
    2. 在pom.xml中引入依赖
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-amqpartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    1. 在application.yml进行配置
    # 服务端口
    server:
      port: 8080
    # 配置rabbitmq服务
    spring:
      rabbitmq:
        username: admin
        password: admin
        virtual-host: /
        host: 192.168.10.100
        port: 5672
    
    1. 定义订单的生产者
    @Component
    public class OrderService {
        
        @Autowired
        private RabbitTemplate rabbitTemplate;
        // 1: 定义交换机
        private String exchangeName = "fanout_order_exchange";
        // 2: 路由key
        private String routeKey = "";
        public void makeOrder(Long userId, Long productId, int num) {
            // 1: 模拟用户下单
            String orderNumer = UUID.randomUUID().toString();
            // 2: 根据商品id productId 去查询商品的库存
            // int numstore = productSerivce.getProductNum(productId);
            // 3:判断库存是否充足
            // if(num >  numstore ){ return  "商品库存不足..."; }
            // 4: 下单逻辑
            // orderService.saveOrder(order);
            // 5: 下单成功要扣减库存
            // 6: 下单完成以后
            System.out.println("用户 " + userId + ",订单编号是:" + orderNumer);
            // 发送订单信息给RabbitMQ fanout
            rabbitTemplate.convertAndSend(exchangeName, routeKey, orderNumer);
        }
    }
    
    1. 绑定关系
    @Configuration
    public class DirectRabbitConfig {
        //队列 起名:TestDirectQueue
        @Bean
        public Queue emailQueue() {
            //一般设置一下队列的持久化就好,其余两个就是默认false
            return new Queue("email.fanout.queue", true);
        }
        @Bean
        public Queue smsQueue() {
            return new Queue("sms.fanout.queue", true);
        }
        @Bean
        public Queue weixinQueue() {
            return new Queue("weixin.fanout.queue", true);
        }
        
        //Direct交换机 起名:TestDirectExchange
        @Bean
        public DirectExchange fanoutOrderExchange() {
            return new DirectExchange("fanout_order_exchange", true, false);
        }
        
        //绑定  将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
        @Bean
        public Binding bindingDirect1() {
            return BindingBuilder.bind(weixinQueue()).to(fanoutOrderExchange()).with("");
        }
        @Bean
        public Binding bindingDirect2() {
            return BindingBuilder.bind(smsQueue()).to(fanoutOrderExchange()).with("");
        }
        @Bean
        public Binding bindingDirect3() {
            return BindingBuilder.bind(emailQueue()).to(fanoutOrderExchange()).with("");
        }
    }
    
    1. 进行测试
    @SpringBootTest
    class SpringbootRabbitmqFanoutProducerApplicationTests {
       @Autowired
       OrderService orderService;
       @Test
       public void contextLoads() throws Exception {
           for (int i = 0; i < 10; i++) {
               Thread.sleep(1000);
               Long userId = 100L + i;
               Long productId = 10001L + i;
               int num = 10;
               orderService.makeOrder(userId, productId, num);
           }
       }
    }
    
    1. 定义消费者
      • 创建方式和pom依赖与生产者相同,注意:application.yml进行配置时server服务端口号要与生产者不相同,可写为8081
    //消费者-邮件服务
    // bindings其实就是用来确定队列和交换机绑定关系
    @RabbitListener(bindings =@QueueBinding(
            // email.fanout.queue 是队列名字,这个名字你可以自定随便定义。
            value = @Queue(value = "email.fanout.queue",autoDelete = "false"),
            // order.fanout 交换机的名字 必须和生产者保持一致
            exchange = @Exchange(value = "fanout_order_exchange",
                    // 这里是确定的rabbitmq模式是:fanout 是以广播模式 、 发布订阅模式
                    type = ExchangeTypes.FANOUT)
    ))
    @Component
    public class EmailService {
        // @RabbitHandler 代表此方法是一个消息接收的方法。该不要有返回值
        @RabbitHandler
        public void messagerevice(String message){
            // 此处省略发邮件的逻辑
            System.out.println("email-------------->" + message);
        }
    }
    
    //消费者-短信服务
    @RabbitListener(bindings =@QueueBinding(
            value = @Queue(value = "sms.fanout.queue",autoDelete = "false"),
            exchange = @Exchange(value = "fanout_order_exchange",
            					type = ExchangeTypes.FANOUT)
    ))
    @Component
    public class SMSService {
        @RabbitHandler
        public void messagerevice(String message){
            System.out.println("sms-------------->" + message);
        }
    }
    
    //消费者-微信服务
    @RabbitListener(bindings =@QueueBinding(
            value = @Queue(value = "weixin.fanout.queue",autoDelete = "false"),
            exchange = @Exchange(value = "fanout_order_exchange",
            					 type = ExchangeTypes.FANOUT))
    )
    @Component
    public class WeixinService {
        @RabbitHandler
        public void messagerevice(String message){
            System.out.println("weixin-------------->" + message);
        }
    }
    
    1. 启动服务

    RabbitMQ入门及笔记_第16张图片

9.2 其他

direct模式与topic模式与fanout模式类似,把交换机的类型分别变为direct-exchange和topic-exchange,同时设置队列的路由key及模糊匹配等。

10. RabbitMQ持久化

10.1 概念

  • 刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当 RabbitMQ 服务停掉以后消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。
  • 持久化就把信息写入到磁盘的过程。

10.2 RabbitMQ持久化消息

RabbitMQ入门及笔记_第17张图片

  • 把消息默认放在内存中是为了加快传输和消费的速度,存入磁盘是保证消息数据的持久化。

10.3 RabbitMQ非持久化消息

  • 非持久消息:是指当内存不够用的时候,会把消息和数据转移到磁盘,但是重启以后非持久化队列消息就丢失。

10.4 RabbitMQ持久化分类

  • RabbitMQ的持久化队列分为:
    1. 队列持久化
    2. 消息持久化
    3. 交换机持久化
面试题:所谓的持久化,就是消息是否存盘。非持久化会存盘吗?
不论是持久化的消息还是非持久化的消息都可以写入到磁盘中,只不过非持久的是等内存不足的情况下才会被写入到磁盘中,会随着重启服务而丢失。

10.5 RabbitMQ队列持久化的代码实现

  • 队列的持久化是定义队列时的durable参数来实现的,Durable为true时,队列才会持久化。
// 参数1:名字  
// 参数2:是否持久化,
// 参数3:独d占的queue, 
// 参数4:不使用时是否自动删除,
// 参数5:其他参数
channel.queueDeclare(queueName,true,false,false,null);
  • 其中参数2:设置为true,就代表的是持久化的含义。即durable=true。持久化的队列在web控制台中有一个D 的标记

    RabbitMQ入门及笔记_第18张图片

10.6 RabbitMQ消息持久化

  • 消息持久化是通过消息的属性deliveryMode来设置是否持久化,在发送消息时通过basicPublish的参数传入。

    // 参数1:交换机的名字
    // 参数2:队列或者路由key
    // 参数3:是否进行消息持久化
    // 参数4:发送消息的内容
    channel.basicPublish(exchangeName, routingKey1, 		      MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
    
  • 注意:将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是

    这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没

    有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了

10.7 RabbitMQ交换机持久化

  • 和队列一样,交换机也需要在定义的时候设置持久化的标识,否则在rabbit-server服务重启以后将丢失。

    // 参数1:交换机的名字
    // 参数2:交换机的类型,topic/direct/fanout/headers
    // 参数3:是否持久化
    channel.exchangeDeclare(exchangeName,exchangeType,true);
    

11. RabbitMQ其他知识点

11.1 幂等性

  1. 概念

    • 用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等
  2. 消息重复消费

    • 消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
  3. 解决思路

    • MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费
      者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。
  4. 消费端的幂等性保障

    • 在海量订单生成的业务高峰期,生产端有可能就会重复发送了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:

      • 唯一ID+指纹码机制,利用数据库主键去重

        指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基
        本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。

      • 利用 redis 的原子性去实现

        利用 redis 执行setnx命令,天然具有幂等性。从而实现不重复消费

11.2 优先级队列

  1. 使用场景

    • 在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能,但是,tmall商家对我们来说,肯定是要分大客户和小客户,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。
  2. 如何添加

    • 控制台页面添加

      RabbitMQ入门及笔记_第19张图片

    • 队列中代码添加优先级

      Map<String, Object> params = new HashMap();
      params.put("x-max-priority", 10);
      channel.queueDeclare("hello", true, false, false, params);
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Azu11SWe-1658301409474)(C:\Users\10642\AppData\Roaming\Typora\typora-user-images\image-20220720111228643.png)]

    • 消息中代码添加优先级

      AMQP.BasicProperties properties = new
      AMQP.BasicProperties().builder().priority(5).build();
      
    • 注意事项:要让队列实现优先级需要做的事情有:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费,因为这样才有机会对消息进行排序

11.3 惰性队列

  1. 使用场景

    • RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
    • 默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,
      但是效果始终不太理想,尤其是在消息量特别大的时候。
  2. 两种模式:

    • 队列具备两种模式:default 和 lazy,默认的为default 模式,在3.6.0 之前的版本无需做任何变更。lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为"default"和"lazy"。
  3. 惰性队列声明的演示:

    Map<String, Object> args = new HashMap<String, Object>();
    args.put("x-queue-mode", "lazy");
    //将惰性队列的声明通过参数的方式
    channel.queueDeclare("myqueue", false, false, false, args);
    
  4. 内存开销对比

    RabbitMQ入门及笔记_第20张图片

    在发送一百万条消息,每条消息大概占1KB的情况下,普通队列占用内存是1.2GB,而惰性队列仅仅占用 1.5MB

12. RabbitMQ的集群

12.1 RabbitMQ集群的概述

  • RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。这使得RabbitMQ本身不需像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA方案和保存集群的元数据。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的。
  • 在实际使用过程中多采取多机多实例部署方式

12.2 集群的搭建

  • 配置的前提是你的rabbitmq可以运行起来,如运行下面两种指令能看到相关信息且运行不报错

    ps aux|grep rabbitmq
    rabbitmqctl status
    
  • 确保RabbitMQ可以运行的,确保完成之后,把单机版的RabbitMQ服务停止,后台看不到RabbitMQ的进程为止

12.3 单机多实例搭建

  • **场景:**假设有两个rabbitmq节点,分别为rabbit-1, rabbit-2,rabbit-1作为主节点,rabbit-2作为从节点。
  • 启动命令:RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit-1 rabbitmq-server -detached
  • 结束命令:rabbitmqctl -n rabbit-1 stop
12.3.1 启动第一个节点rabbit-1
sudo RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit-1 rabbitmq-server start &
...............省略...................
  ##########  Logs: /var/log/rabbitmq/rabbit-1.log
  ######  ##        /var/log/rabbitmq/rabbit-1-sasl.log
  ##########
              Starting broker...
 completed with 3 plugins.
12.3.2 启动第二个节点rabbit-2
  • 注意:web管理插件端口占用,所以还要指定其web插件占用的端口号
sudo RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME=rabbit-2 rabbitmq-server start &
..............省略..................
  ##########  Logs: /var/log/rabbitmq/rabbit-2.log
  ######  ##        /var/log/rabbitmq/rabbit-2-sasl.log
  ##########
              Starting broker...
 completed with 3 plugins.
12.3.3 验证启动
ps aux|grep rabbitmq
12.3.4 rabbit-1操作作为主节点
#停止应用
> sudo rabbitmqctl -n rabbit-1 stop_app
#目的是清除节点上的历史数据(如果不清除,无法将节点加入到集群)
> sudo rabbitmqctl -n rabbit-1 reset
#启动应用
> sudo rabbitmqctl -n rabbit-1 start_app
12.3.5 rabbit2操作为从节点
# 停止应用
> sudo rabbitmqctl -n rabbit-2 stop_app
# 目的是清除节点上的历史数据(如果不清除,无法将节点加入到集群)
> sudo rabbitmqctl -n rabbit-2 reset
# 将rabbit2节点加入到rabbit1(主节点)集群当中【Server-node服务器的主机名】
> sudo rabbitmqctl -n rabbit-2 join_cluster rabbit-1@'Server-node'
# 启动应用
> sudo rabbitmqctl -n rabbit-2 start_app
12.3.6 验证集群状态
> sudo rabbitmqctl cluster_status -n rabbit-1
12.3.7 Web监控

RabbitMQ入门及笔记_第21张图片

  • 注意在访问的时候:web结面的管理需要给15672 node-1 和15673的node-2 设置用户名和密码。如下:
rabbitmqctl -n rabbit-1 add_user admin admin
rabbitmqctl -n rabbit-1 set_user_tags admin administrator
rabbitmqctl -n rabbit-1 set_permissions -p / admin ".*" ".*" ".*"

rabbitmqctl -n rabbit-2 add_user admin admin
rabbitmqctl -n rabbit-2 set_user_tags admin administrator
rabbitmqctl -n rabbit-2 set_permissions -p / admin ".*" ".*" ".*"

12.4 多机部署的方式

  • 如果采用多机部署方式,需读取其中一个节点的cookie, 并复制到其他节点(节点之间通过cookie确定相互是否可通信)。cookie存放在/var/lib/rabbitmq/.erlang.cookie。
    例如:主机名分别为rabbit-1、rabbit-2
    1. 逐个启动各节点
    2. 配置各节点的hosts文件( vim /etc/hosts)
      ip1:rabbit-1
      ip2:rabbit-2
    3. 其它步骤雷同单机部署方式

12.5 Springboot整合rabbitmq集群配置

  • springboot整合rabbitmq集群创建方式这里省略
  1. 引入starter

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.2.6.RELEASEversion>
        <relativePath/> 
    parent>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-amqpartifactId>
    dependency>
    
  2. 详细配置如下

    rabbitmq:
        addresses: 127.0.0.1:6605,127.0.0.1:6606,127.0.0.1:6705 #指定client连接到的server的地址,多个以逗号分隔(优先取addresses,然后再取host)
    #    port:
        ##集群配置 addresses之间用逗号隔开
        # addresses: ip:port,ip:port
        password: admin
        username: 123456
        virtual-host: / # 连接到rabbitMQ的vhost
        requested-heartbeat: #指定心跳超时,单位秒,0为不指定;默认60s
        publisher-confirms: #是否启用 发布确认
        publisher-reurns: # 是否启用发布返回
        connection-timeout: #连接超时,单位毫秒,0表示无穷大,不超时
        cache:
          channel.size: # 缓存中保持的channel数量
          channel.checkout-timeout: # 当缓存数量被设置时,从缓存中获取一个channel的超时时间,单位毫秒;如果为0,则总是创建一个新channel
          connection.size: # 缓存的连接数,只有是CONNECTION模式时生效
          connection.mode: # 连接工厂缓存模式:CHANNEL 和 CONNECTION
        listener:
          simple.auto-startup: # 是否启动时自动启动容器
          simple.acknowledge-mode: # 表示消息确认方式,其有三种配置方式,分别是none、manual和auto;默认auto
          simple.concurrency: # 最小的消费者数量
          simple.max-concurrency: # 最大的消费者数量
          simple.prefetch: # 指定一个请求能处理多少个消息,如果有事务的话,必须大于等于transaction数量.
          simple.transaction-size: # 指定一个事务处理的消息数量,最好是小于等于prefetch的数量.
          simple.default-requeue-rejected: # 决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)
          simple.idle-event-interval: # 多少长时间发布空闲容器时间,单位毫秒
          simple.retry.enabled: # 监听重试是否可用
          simple.retry.max-attempts: # 最大重试次数
          simple.retry.initial-interval: # 第一次和第二次尝试发布或传递消息之间的间隔
          simple.retry.multiplier: # 应用于上一重试间隔的乘数
          simple.retry.max-interval: # 最大重试时间间隔
          simple.retry.stateless: # 重试是有状态or无状态
        template:
          mandatory: # 启用强制信息;默认false
          receive-timeout: # receive() 操作的超时时间
          reply-timeout: # sendAndReceive() 操作的超时时间
          retry.enabled: # 发送重试是否可用
          retry.max-attempts: # 最大重试次数
          retry.initial-interval: # 第一次和第二次尝试发布或传递消息之间的间隔
          retry.multiplier: # 应用于上一重试间隔的乘数
          retry.max-interval: #最大重试时间间隔
    
    • 对于发送方而言,需要做以下配置:

      1. 配置CachingConnectionFactory
      2. 配置Exchange/Queue/Binding
      3. 配置RabbitAdmin创建上一步的Exchange/Queue/Binding
      4. 配置RabbitTemplate用于发送消息,RabbitTemplate通过CachingConnectionFactory获取到Connection,然后想指定Exchange发送
    • 对于消费方而言,需要做以下配置:

      1. 配置CachingConnectionFactory
      2. 配置Exchange/Queue/Binding
      3. 配置RabbitAdmin创建上一步的Exchange/Queue/Binding
      4. 配置RabbitListenerContainerFactory
      5. 配置@RabbitListener/@RabbitHandler用于接收消息
    • 在默认情况下主要的配置如下:

      RabbitMQ入门及笔记_第22张图片
  3. Spring AMQP的主要对象

    RabbitMQ入门及笔记_第23张图片

  4. 使用:通过配置类加载的方式:

    @Configuration
    public class RabbitConfig {
        private static final Logger logger = LoggerFactory.getLogger(RabbitConfig.class);
        public static final String RECEIVEDLXEXCHANGE="spring-ex";
        public static final String RECEIVEDLXQUEUE="spring-qu1";
        public static final String RECEIVEDLXROUTINGKEY="aa";
        public static final String DIRECTEXCHANGE="spring-ex";
        public static final String MDMQUEUE="mdmQueue";
        public static final String TOPICEXCHANGE="spring-top";
        @Value("${spring.rabbitmq.addresses}")
        private String hosts;
        @Value("${spring.rabbitmq.username}")
        private String userName;
        @Value("${spring.rabbitmq.password}")
        private String password;
        @Value("${spring.rabbitmq.virtual-host}")
        private String virtualHost;
     /*   @Value("${rabbit.channelCacheSize}")
        private int channelCacheSize;*/
    //    @Value("${rabbit.port}")
    //    private int port;
    /*    @Autowired
        private ConfirmCallBackListener confirmCallBackListener;
        @Autowired
        private ReturnCallBackListener returnCallBackListener;*/
        @Bean
        public ConnectionFactory connectionFactory(){
            CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory();
            cachingConnectionFactory.setAddresses(hosts);
            cachingConnectionFactory.setUsername(userName);
            cachingConnectionFactory.setPassword(password);
    //        cachingConnectionFactory.setChannelCacheSize(channelCacheSize);
            //cachingConnectionFactory.setPort(port);
            cachingConnectionFactory.setVirtualHost(virtualHost);
            //设置连接工厂缓存模式:
            cachingConnectionFactory.setCacheMode(CachingConnectionFactory.CacheMode.CONNECTION);
            //缓存连接数
            cachingConnectionFactory.setConnectionCacheSize(3);
            //设置连接限制
            cachingConnectionFactory.setConnectionLimit(6);
            logger.info("连接工厂设置完成,连接地址{}"+hosts);
            logger.info("连接工厂设置完成,连接用户{}"+userName);
            return cachingConnectionFactory;
        }
        @Bean
        public RabbitAdmin rabbitAdmin(){
            RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory());
            rabbitAdmin.setAutoStartup(true);
            rabbitAdmin.setIgnoreDeclarationExceptions(true);
            rabbitAdmin.declareBinding(bindingMdmQueue());
            //声明topic交换器
            rabbitAdmin.declareExchange(directExchange());
            logger.info("管理员设置完成");
            return rabbitAdmin;
        }
        @Bean
        public RabbitListenerContainerFactory listenerContainerFactory() {
            SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
            factory.setConnectionFactory(connectionFactory());
            factory.setMessageConverter(new Jackson2JsonMessageConverter());
            //最小消费者数量
            factory.setConcurrentConsumers(10);
            //最大消费者数量
            factory.setMaxConcurrentConsumers(10);
            //一个请求最大处理的消息数量
            factory.setPrefetchCount(10);
            //
            factory.setChannelTransacted(true);
            //默认不排队
            factory.setDefaultRequeueRejected(true);
            //手动确认接收到了消息
            factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
            logger.info("监听者设置完成");
            return factory;
        }
        @Bean
        public DirectExchange directExchange(){
            return new DirectExchange(DIRECTEXCHANGE,true,false);
        }
        @Bean
        public Queue mdmQueue(){
            Map arguments = new HashMap<>();
            // 绑定该队列到私信交换机
            arguments.put("x-dead-letter-exchange",RECEIVEDLXEXCHANGE);
            arguments.put("x-dead-letter-routing-key",RECEIVEDLXROUTINGKEY);
            logger.info("队列交换机绑定完成");
            return new Queue(RECEIVEDLXQUEUE,true,false,false,arguments);
        }
        @Bean
        Binding bindingMdmQueue() {
            return BindingBuilder.bind(mdmQueue()).to(directExchange()).with("");
        }
        @Bean
        public RabbitTemplate rabbitTemplate(){
            RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
            rabbitTemplate.setMandatory(true);
            //发布确认
    //        rabbitTemplate.setConfirmCallback(confirmCallBackListener);
            // 启用发布返回
    //        rabbitTemplate.setReturnCallback(returnCallBackListener);
            logger.info("连接模板设置完成");
            return rabbitTemplate;
        }
      /*  @Bean
        public TopicExchange topicExchange(){
            return new TopicExchange(TOPICEXCHANGE,true,false);
        }*/
      /*
    *//**
         * @return DirectExchange
         *//*
        @Bean
        public DirectExchange dlxExchange() {
            return new DirectExchange(RECEIVEDLXEXCHANGE,true,false);
        }
    *//*
    *
         * @return Queue
    *//*
        @Bean
        public Queue dlxQueue() {
            return new Queue(RECEIVEDLXQUEUE,true);
        }
    *//*
         * @return Binding
         *//*
        @Bean
        public Binding binding() {
            return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(RECEIVEDLXROUTINGKEY);
        }*/
    }
    

13. 分布式事务

13.1 分布式事务的方式

13.1.1两阶段提交(2PC)需要数据库产商的支持,java组件有atomikos等。
  • 两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
  1. 准备阶段:

    协调者询问参与者事务是否执行成功,参与者发回事务执行结果。

    RabbitMQ入门及笔记_第24张图片

  2. 提交阶段

    • 如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
    • 需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。

    RabbitMQ入门及笔记_第25张图片

  3. 存在的问题

    • 同步阻塞 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
    • 单点问题 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
    • 数据不一致 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
    • 太过保守 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
13.1.2 补偿事务(TCC)
  • 比如:严选,阿里,蚂蚁金服

  • TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

    • Try 阶段主要是对业务系统做检测及资源预留
    • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 - - - Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
    • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
  • 优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些

  • 缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。

13.1.3 本地消息表(异步确保)
  • 比如:支付宝、微信支付主动查询支付状态,对账单的形式

  • 本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。

    • 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
    • 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
    • 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XADwvHMU-1658301409476)(https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/03/06/kuangstudy316cc2e4-e23a-4874-a8a9-0ed14ba71989.png)]

  • 优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性。

  • 缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

13.1.4 MQ 事务消息 异步场景,通用性较强,拓展性较高
  • 有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 Kafka 不支持。
    以阿里的 RabbitMQ 中间件为例,其思路大致为:

    • 第一阶段Prepared消息,会拿到消息的地址。 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。

    • 也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RabbitMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RabbitMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dqu8fPnx-1658301409476)(https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/03/06/kuangstudye6732a40-44e5-491d-8e3d-7070fc943151.png)\

  • 优点: 实现了最终一致性,不需要依赖本地数据库事务。

  • 缺点: 实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源。

13.4.5 总结

通过本文我们总结并对比了几种分布式分解方案的优缺点,分布式事务本身是一个技术难题,是没有一种完美的方案应对所有场景的,具体还是要根据业务场景去抉择吧。阿里RocketMQ去实现的分布式事务,现在也有除了很多分布式事务的协调器,比如LCN等,大家可以多去尝试。

14. RabbitMQ集群监控

在广大的互联网行业中RabbitMQ几乎都会有集群,那么对于集群的监控就成了企业生态中必不可少的一环。接下来我们来将讲解主要的4种监控。

14.1 管理界面监控

  • 管理界面监控需要我们开启对应的插件(rabbitmq-plugins enable rabbitmq_management)
    然后访问http://ip:15672

RabbitMQ入门及笔记_第26张图片

  • 在管理控制台我们就可以直观的看到集群中的每一个节点是否正常,如果为红色则表示节点挂掉了,同时可以很方便的查看到各个节点的内存、磁盘等相关的信息,使用起来也是非常方便的。但是遗憾的该功能做的比较简陋,没有告警等一些列的个性化设置,同时如果想把他接入到公司其他的监控系统统一管理也是很难做到的,所以扩展性不强,一般在小型企业的小集群中使用。

14.2 tracing日志监控

  • 对于企业级的应用开发来讲,我们通常都会比较关注我们的消息,甚至很多的场景把消息的可靠性放在第一位,但是我们的MQ集群难免会出现消息异常丢失或者客户端无法发送消息等异常情况,此时为了帮助开发人员快速的定位问题,我们就可以对消息的投递和消费过程进行监控,而tracing日志监控插件帮我们很好的实现了该功能
    消息中心的消息追踪需要使用Trace实现,Trace是Rabbitmq用于记录每一次发送的消息,方便使用Rabbitmq的开发者调试、排错。可通过插件形式提供可视化界面。Trace启动后会自动创建系统Exchange:amq.rabbitmq.trace ,每个队列会自动绑定该Exchange,绑定后发送到队列的消息都会记录到Trace日志。
14.2.1 消息追踪启用与查看

以下是trace的相关命令和使用(要使用需要先rabbitmq启用插件,再打开开关才能使用):

RabbitMQ入门及笔记_第27张图片

安装插件并开启 trace_on 之后,会发现多个 exchange:amq.rabbitmq.trace ,类型为:topic。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yym0RAWx-1658301409477)(https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/03/06/kuangstudyd39998d5-9bc6-4acb-a379-08a576623b40.png)]

14.2.2 日志追踪
  1. 发送消息

    rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息--01。");
    
  2. 查看trace

    RabbitMQ入门及笔记_第28张图片

  3. 点击Tracing查看Trace log files

    RabbitMQ入门及笔记_第29张图片

  4. 点击xuexiangban-trace.log确认消息轨迹正确性

14.3 Zabbix 监控RabbitMQ

Zabbix是一个基于WEB界面提供分布式系统监视以及网络监视功能的企业级开源解决方案,他也可以帮助我们搭建一个MQ集群的监控系统,同时提供预警等功能,但是由于其搭建配置要求比较高一般都是由运维人员负责搭建

15. 关于RabbitMQ的面试题

面试题1:为什么使用rabbitmq?什么时候用?怎么用?
一开始写项目,架构比较单一,使用的是单体结构,把所有的业务都堆积在一个项目里。随着不断的学习,项目慢慢变为分布式架构,把系统进行拆分,在拆分的过程中,考虑到不同的系统之间需要进行沟通和协同,然后就接触到了消息队列,并且选择了rabbitmq。对于rabbitmq的感受,最核心的一点是它是异步的,多线程机制,分发,使网站的性能得到成倍的提升,处理数据更加高效,更加稳健,进而可以做到削峰。而且将系统拆分之后,就可以解耦。
面试题:2:Rabbitmq为什么需要信道,为什么不是TCP直接通信
1.TCP的创建和销毁,开销大,创建要三次握手,销毁要4次分手。
2.如果不用信道,那应用程序就会TCP连接到Rabbit服务器,高峰时每秒成千上万连接就会造成资源的巨大浪费,而且底层操作系统每秒处理tcp连接数也是有限制的,必定造成性能瓶颈。
3.信道的原理是一条线程一条信道,多条线程多条信道同用一条TCP连接,一条TCP连接可以容纳无限的信道,即使每秒成千上万的请求也不会成为性能瓶颈。
面试题3:所谓的持久化,就是消息是否存盘。那非持久化会存盘吗?
不论是持久化的消息还是非持久化的消息都可以写入到磁盘中,只不过非持久的是等内存不足的情况下才会被写入到磁盘中,会随着重启服务而丢失。
面试题4:可以存在没有交换机的队列吗?
不可能,如果没有指定交换机,那队列会使用一个默认的交换机
面试题5:queue队列到底在消费者中创建还是生产者中创建?
1.一般建议是在rabbitmq操作面板创建。这是一种稳妥的做法。
2.按照常理来说,确实应该消费者这边创建是最好,消息的消费是在这边。这样你承受一个后果,可能我生产者生产消息可能会丢失消息。
3.在生产者创建队列也是可以,这样稳妥的方法,消息是不会出现丢失。
4.如果你生产者和消费都创建的队列,谁先启动谁先创建,后面启动就覆盖前面的
面试题6:开发过程中用注解去绑定关系,还是用配置文件去绑定关系?
两种方式都能完成绑定关系,由于业务需要, 有的时候, 基于注解形式的代码, 不能满足业务需求

你可能感兴趣的:(中间件,rabbitmq,分布式,java)