Day74--RabbitMQ


title: Day74–RabbitMQ
date: 2021-06-02 16:35:24
author: Liu_zimo


MQ基本概念

MQ概述

MQ全称Message Queue(消息队列),是在消息的传输过程中保存消息的容器多用于分布式系统之间进行通信

  • A系统(生产者) → MQ(中间件) → B系统(消费者)
  • MQ,消息队列,存储消息的中间件
  • 分布式系统通信两种方式:直接远程调用和借助第三方完成间接通信
  • 发送方称为生产者,接收方称为消费者

MQ的优势和劣势

  • 优势:
    • 应用解耦:提高系统容错性和可维护性
    • 异步提速:提升用户体验和系统吞吐量
    • 削峰填谷:提高系统稳定性
  • 劣势:
    • 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差。如何保证MQ的高可用?
    • 系统复杂度提高:如何保证消息没有被重复消费?怎么处理消息丢失情况?那么保证消息传递的顺序性?
    • 一致性问题:B、C系统处理成功,D系统处理失败。如何保证消息数据处理的一致性?
小结
  1. 既然MQ有优势也有劣势,那么使用MQ需要满足什么条件呢?
    • 生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。
    • 容许短暂的不一致性。
    • 确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本。

常见的MQ产品

目前业界有很多的MQ产品,例如RabbitMQ、 RocketMQ、ActiveMQ、 Kafka、ZeroMQ、MetaMq等,也有直接使用Redis 充当消息队列的案例,而这些消息队列产品,各有侧重,在实际选型时,需要结合自身需求及MQ产品特征,综合考虑。

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApacheAlibabaApache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义自定义协议,社区封装了http协议支持
客户端支持语言官方支持Erlang,Java,Ruby等,社区产出多种API,几乎支持所有语言Java,C,C++,Python,PHP,Perl,.net等Java,C++(不成熟)官方支持Java,社区产出多种API,如PHP,Python等
单机吞吐量万级(其次)万级(最差)十万级(最好)十万级(次之)
消息延迟微妙级毫秒级毫秒级毫秒以内
功能特性并发能力强,性能极其好,延时低,社区活跃,管理界面丰富老牌产品,成熟度高,文档较多MQ功能比较完备,扩展性佳只支持主要的MQ功能,毕竟是为大数据领域准备的。

RabbitMQ简介

AMQP,即 Advanced Message Queuing Protocol (高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP规范发布。类比HTTP

AMQP原理

2007年,Rabbit 技术公司基于AMQP标准开发的RabbitMQ1.0发布。RabbitMQ采用Erlang语言开发。Erlang 语言由Ericson 设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛。

RabbitMQ基础架构图

  • RabbitMQ中的相关概念:
    • 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的开销
    • 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的分发依据
  • RabbitMQ提供了6种工作模式:简单模式、work queues、Publish/Subscribe发布与订阅模式、Routing路由模式、Topics主题模式、RPC远程调用模式(远程调用,不太算MQ;暂不作介绍)

RabbitMQ六种工作模式

JMS

  • JMS 即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件的API
  • JMS是JavaEE规范中的一种,类比JDBC
  • 很多消息中间件都实现了JMS规范,例如:ActiveMQ。RabbitMQ官方没有提供JMS的实现包,但是开源社区有

小结

  1. RabbitMQ是基于AMQP协议使用Erlang语言开发的一款消息队列产品
  2. RabbitMQ提供了6种工作模式
  3. AMQP是协议,类比HTTP
  4. JMS是API规范接口,类比JDBC

RabbitMQ的安装和配置

  • RabbitMQ官方地址:http://www.rabbitmq.com/

Ubuntu安装RabbitMQ

  1. 安装erlang

    由于rabbitMq需要erlang语言的支持,在安装rabbitMq之前需要安装erlang

    sudo apt-get install erlang-nox

  2. RabbitMQ

    安装前先更新源:sudo apt-get update

    安装:sudo apt-get install rabbitmq-server

  3. 启动、停止、重启、状态rabbitMq命令

    • 启动:sudo rabbitmq-server start
    • 停止:sudo rabbitmq-server stop
    • 重启:sudo rabbitmq-server restart
    • 状态:sudo rabbitmqctl status
  4. RabbitMQ GUID使用

    • 安装了Rabbitmq后,默认也安装了该管理工具,执行命令即可启动
    1. 开启管理界面:sudo rabbitmq-plugins enable rabbitmq_management(先定位到rabbitmq安装目录)
    2. 修改默认配置信息(打开guest账户密码等):vim /usr/lib/rabbitmq/lib/rabbitmq_server-x.x.x/ebin/rabbit.app
    3. 重启服务:sudo service rabbitmq-server restart
    4. 浏览器访问http://localhost:15672/
      • 若访问失败,则关闭防火墙:sudo ufw disable

RabbitMQ的快速入门

入门程序

  • 需求:使用简单模式完成消息传递

  • 步骤:

    1. 创建工程(生成者、消费者)
    2. 分别添加依赖
    3. 编写生产者发送消息
    4. 编写消费者接收消息
  • pom.xml依赖

<dependencies>
    <!--rabbitmq Java客户端-->
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.6.0</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>
  • 生产者
package com.zimo.producer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 生产者 - 简单模式
 *      功能:发送消息
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:05
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        
        // 4.创建Channel
        Channel channel = connection.createChannel();
        
        // 5.创建队列Queue
        /**
         * queueDeclare()
         * @param1 queue: 队列名称
         * @param2 durable: 是否持久化
         * @param3 exclusive: 是否独占,只能有一个消费者监听这个队列,当Connection关闭时,是否删除队列
         * @param4 autoDelete: 是否自动删除,当没有Consumer时,会自动删除掉
         * @param5 arguments: 参数
         */
        // 如果没有hello_world的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("hello_world", true,false,false,null);
        
        // 6.发送消息
        /**
         * basicPublish
         * @param1 exchange: 交换机名称,简单模式下交换机会使用默认的""
         * @param2 routingKey: 路由名称
         * @param3 properties: 配置信息
         * @param4 body[]: 发送的消息数据
         */
        String body = "hello rabbitmq~~";
        channel.basicPublish("","hello_world",null,body.getBytes());
        
        // 7.释放资源
        channel.close();
        connection.close();
    }
}
  • 消费者
package com.zimo.consumer;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 消费者 - 简单模式
 *      功能:接收消息 - 监听
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:26
 */
public class Consumer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        // 4.创建Channel
        Channel channel = connection.createChannel();
        // 5.创建队列Queue
        channel.queueDeclare("hello_world", true,false,false,null);
        // 6.接收消息
        /**
         * basicConsume
         * @param1 queue: 队列名称
         * @param2 autoAck: 是否自动确认
         * @param3 callback: 回调对象
         */
        channel.basicConsume("hello_world", true, new DefaultConsumer(channel){
            /**
             * @param consumerTag:标识
             * @param envelope:获取信息(交换机、路由key)
             * @param properties:配置信息
             * @param body:数据
             * @throws IOException
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 收到消息,自动执行该方法
                System.out.println("consumerTag:" + consumerTag);
                System.out.println("Exchange:" + envelope.getExchange());
                System.out.println("RoutingKey:" + envelope.getRoutingKey());
                System.out.println("properties:" + properties);
                System.out.println("body:" + new String(body));
            }
        });
        // 注:消费者监听信息,不要关闭资源
    }
}

RabbitMQ的工作模式

简单工作模式 - hello world

  • 模式说明:一个生产者发送消息到一个队列中,对应一个消费者进行监听消费

工作队列模式 - Work Queues

与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息

  • 模式说明:一个生产者对应多个消费者(竞争关系

  • 应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度

  • 生产者

package com.zimo.producer.workqueues;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 生产者 - 工作队列模式
 *      功能:发送消息
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:05
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        // 4.创建Channel
        Channel channel = connection.createChannel();
        // 5.创建队列Queue
        // 如果没有hello_world的队列,则会创建该队列,如果有则不会创建
        channel.queueDeclare("work_queues", true,false,false,null);
        // 6.发送消息
        for (int i = 0; i < 10; i++) {
            String body = i + ": hello rabbitmq~~";
            channel.basicPublish("","work_queues",null,body.getBytes());
        }
        // 7.释放资源
        channel.close();
        connection.close();
    }
}
  • 消费者(将会有多个ConsumerN启动监听消费)
package com.zimo.consumer.workqueues;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 消费者 - 工作队列模式
 *      功能:接收消息 - 监听
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:26
 */
public class Consumer1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        // 4.创建Channel
        Channel channel = connection.createChannel();
        // 5.创建队列Queue
        channel.queueDeclare("work_queues", true,false,false,null);
        // 6.接收消息
        channel.basicConsume("work_queues", true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("body:" + new String(body));
            }
        });
        // 注:消费者监听信息,不要关闭资源
    }
}

订阅模式 - Pub/Sub

一个生产者将消息发送到交换机,由交换机进行分发不同的消息队列,再由对应的队列消费者监听消费。在订阅模型中,多了一个Exchange角色,而且过程略有变化

  • 模式说明:

    • P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)

    • C:消费者,消息的接收者,会一直等待消息到来

    • Queue:消息队列,接收消息、缓存消息

    • Exchange:交换机 (X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:

      1. Fanout:广播,将消息交给所有绑定到交换机的队列
      2. Direct:定向,把消息交给符合指定routing key的队列
      3. Topic:通配符,把消息交给符合routing pattern(路由模式)的队列
      • 交换机只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
Fanout广播模式
  • 生产者
package com.zimo.producer.pub_sub;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 生产者 - 订阅模式
 *      功能:发送消息
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:05
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        // 4.创建Channel
        Channel channel = connection.createChannel();
        // 5.创建交换机
        /**
         * exchangeDeclare()
         * @param1 exchange: 交换机名称
         * @param2 type: 交换机类型(BuiltinExchangeType):DIRECT[定向]、FANOUT[广播]、TOPIC[通配]、HEADERS[参数匹配]
         * @param3 durable: 是否持久化
         * @param4 autoDelete: 是否自动删除
         * @param5 internal: 内部使用,一般都是false
         * @param5 arguments: 参数
         */
        String exchangeName = "test_fanout";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,true,false,false,null);
        // 6.创建两个队列Queue
        String queue1 = "queue1";
        String queue2 = "queue2";
        channel.queueDeclare(queue1,true,false,false,null);
        channel.queueDeclare(queue2,true,false,false,null);
        // 7.绑定队列和交换机
        /**
         * queueBind()
         * @param1 queue: 队列名称
         * @param2 exchange: 交换机名称
         * @param3 routingKey: 路由键,绑定规则。如果交换机类型为fanout,则设置为""
         */
        channel.queueBind(queue1, exchangeName,"");
        channel.queueBind(queue2, exchangeName,"");
        // 8.发送消息
        for (int i = 0; i < 5; i++) {
            String body = "Debug【" + i + "】 hello rabbitmq~~";
            channel.basicPublish(exchangeName,"",null,body.getBytes());
        }
        // 7.释放资源
        channel.close();
        connection.close();
    }
}
  • 消费者
package com.zimo.consumer.pub_sub;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 消费者 - 订阅模式
 *      功能:接收消息 - 监听
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:26
 */
public class Consumer1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        // 4.创建Channel
        Channel channel = connection.createChannel();
        // 队列声明一次就可以了,没有的会自己创建
        String queue1 = "queue1";
        // 5.接收消息
        channel.basicConsume(queue1, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println(queue1 + new String(body));
            }
        });
        // 注:消费者监听信息,不要关闭资源
    }
}

路由模式 - Routing

  • 模式说明:

    • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
    • 消息的发送方在向Exchange发送消息时,也必须指定消息的RoutingKey
    • Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的Routing key完全一致,才会接收到消息
  • 生产者

package com.zimo.producer.routing;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 生产者 - 路由模式
 *      功能:发送消息
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:05
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        // 4.创建Channel
        Channel channel = connection.createChannel();
        // 5.创建交换机
        String exchangeName = "test_direct";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,true,false,false,null);
        // 6.创建两个队列Queue
        String queue1 = "queue1";
        String queue2 = "queue2";
        channel.queueDeclare(queue1,true,false,false,null);
        channel.queueDeclare(queue2,true,false,false,null);
        // 7.绑定队列和交换机
        channel.queueBind(queue1, exchangeName,"error");

        channel.queueBind(queue2, exchangeName,"info");
        channel.queueBind(queue2, exchangeName,"error");
        channel.queueBind(queue2, exchangeName,"warning");
        // 8.发送消息
        for (int i = 0; i < 5; i++) {
            String log = "";
            int num = (int) (Math.random() * 3 + 1);
            switch (num){
                case 1:
                    log = "error";
                    break;
                case 2:
                    log = "info";
                    break;
                case 3:
                    log= "warning";
                    break;
            }
            String body = log + "【" + i + "】 hello rabbitmq~~";
            channel.basicPublish(exchangeName, log,null,body.getBytes());
        }
        // 7.释放资源
        channel.close();
        connection.close();
    }
}
  • 消费者
package com.zimo.consumer.routing;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 消费者 - 路由模式
 *      功能:接收消息 - 监听
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:26
 */
public class Consumer1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        // 4.创建Channel
        Channel channel = connection.createChannel();
        // 队列声明一次就可以了,没有的会自己创建
        String queue1 = "queue1";
        // 5.接收消息
        channel.basicConsume(queue1, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("日志存储到数据库");
                System.out.println(queue1 + new String(body));
            }
        });
        // 注:消费者监听信息,不要关闭资源
    }
}
-----------------------------------------------------------------------------------
public class Consumer2 {
    public static void main(String[] args) throws IOException, TimeoutException {
      	...
        // 队列声明一次就可以了,没有的会自己创建
        String queue2 = "queue2";
        // 5.接收消息
        channel.basicConsume(queue2, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("日志控制台打印");
                System.out.println(queue2 + new String(body));
            }
        });
        // 注:消费者监听信息,不要关闭资源
    }
}

通配符模式 - Topics

Topic主题模式可以实现Pub/Sub发布与订阅模式和Routing路由模式的功能,只是Topic在配置routing key的时候可以使用通配符,显得更加灵活

  • 模式说明:

    • *通配符代表通配一个单词
    • #通配一个或多个单词
  • 生产者

package com.zimo.producer.topics;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 生产者 - 通配模式
 *      功能:发送消息
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:05
 */
public class Producer {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        // 4.创建Channel
        Channel channel = connection.createChannel();
        // 5.创建交换机
        String exchangeName = "test_topic";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,true,false,false,null);
        // 6.创建两个队列Queue
        String queue1 = "queue1";
        String queue2 = "queue2";
        channel.queueDeclare(queue1,true,false,false,null);
        channel.queueDeclare(queue2,true,false,false,null);
        // 7.绑定队列和交换机 routin key 系统的名称.日志的及别
        // 需求:所有error级别日志入库,所有order系统的日志入库
        channel.queueBind(queue1, exchangeName,"#.error");
        channel.queueBind(queue1, exchangeName,"order.*");

        channel.queueBind(queue2, exchangeName,"*.*");
        // 8.发送消息
        for (int i = 0; i < 5; i++) {
            String log = "";
            int num = (int) (Math.random() * 4 + 1);
            switch (num){
                case 1:
                    log = "1.error";
                    break;
                case 2:
                    log = "2.info";
                    break;
                case 3:
                    log= "3.warning";
                    break;
                default:
                    log = "order.log";
            }
            String body = log + "【" + i + "】 hello rabbitmq~~";
            channel.basicPublish(exchangeName, log,null,body.getBytes());
        }
        // 7.释放资源
        channel.close();
        connection.close();
    }
}
  • 消费者
package com.zimo.consumer.topics;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
 * 消费者 - 通配模式
 *      功能:接收消息 - 监听
 * @author Liu_zimo
 * @version v0.1 by 2021/6/3 18:26
 */
public class Consumer1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2.设置参数
        factory.setHost("192.168.1.133");   // 设置ip地址,默认值:localhost
        factory.setPort(5672);              // 设置端口,默认值:5672
        factory.setVirtualHost("/zimo");    // 设置虚拟机,默认值:/
        factory.setUsername("zimo");        // 设置用户名,默认值:guest
        factory.setPassword("123456");      // 设置密码,默认值:guest
        // 3.获取连接 Connection
        Connection connection = factory.newConnection();
        // 4.创建Channel
        Channel channel = connection.createChannel();
        // 队列声明一次就可以了,没有的会自己创建
        String queue1 = "queue1";
        // 5.接收消息
        channel.basicConsume(queue1, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("日志存储到数据库");
                System.out.println(queue1 + " - " + new String(body));
            }
        });
        // 注:消费者监听信息,不要关闭资源
    }
}

小结

  1. 简单模式HelloWorld
    一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)。
  2. 工作队列模式 Work Queue
    一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。
  3. 发布订阅模式Publish/subscribe
    需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。
  4. 路由模式Routing
    需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定 routing key,当发送消息到交换机后,交换机会根据routing key 将消息发送到对应的队列。
  5. 通配符模式Topic
    需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的 routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列。

Spring整合RabbitMQ

  • 需求:使用Spring整合RabbitMQ

  • 步骤:

    • 生产者:
      1. 创建生产者工程
      2. 添加依赖
      3. 配置整合
      4. 编写代码发送消息
    • 消费者
      1. 创建生产者工程
      2. 添加依赖
      3. 配置整合
      4. 编写消息监听器
  • pom.xml依赖引入

<dependencies>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-context</artifactId>        <version>5.1.7.RELEASE</version>    </dependency>    <dependency>        <groupId>org.springframework.amqp</groupId>        <artifactId>spring-rabbit</artifactId>        <version>2.1.8.RELEASE</version>    </dependency>    <dependency>        <groupId>junit</groupId>        <artifactId>junit</artifactId>        <version>4.13.2</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-test</artifactId>        <version>5.1.7.RELEASE</version>    </dependency></dependencies><build>    <plugins>        <plugin>            <groupId>org.apache.maven.plugins</groupId>            <artifactId>maven-compiler-plugin</artifactId>            <version>3.8.0</version>            <configuration>                <source>1.8</source>                <target>1.8</target>            </configuration>        </plugin>    </plugins></build>
  • rabbitmq配置抽取
rabbitmq.host=192.168.1.133rabbitmq.port=5672rabbitmq.username=zimorabbitmq.password=123456rabbitmq.virtural-host=/zimo

生产者

  • spring配置整合
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>
    <!--定义rabbitmq connectionFactory-->
    <rabbit:connection-factory id="factory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}"
                               password="${rabbitmq.password}" virtual-host="${rabbitmq.virtural-host}"/>
    <!--定义管理交换机、队列-->
    <rabbit:admin connection-factory="factory"/>
    <!--定义持久化队列,不存在则自动创建;不绑定到交换机则绑定到默认交换机,默认交换机类型为direct,名字为:"",路由键为队列的名称-->
    <rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true" />
    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~  广播﹔所有队列都能收到消息  ~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <!--定义广播交换机中的持久化队列,不存在则自动创建-->
    <rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/>
    <!--定义广播交换机中的持久化队列,不存在则自动创建-->
    <rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/>
    <!--定义广播类型交换机;并绑定上述两个队列-->
    <rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding queue="spring_fanout_queue_1" />
            <rabbit:binding queue="spring_fanout_queue_2" />
        </rabbit:bindings>
    </rabbit:fanout-exchange>
    <!-- ~~~~~~~~~~~~~~~~~~~~~~~~  通配符﹔*匹配一个单词,#匹配多个单词  ~~~~~~~~~~~~~~~~~~~~~~~~ -->
    <!--定义广播交换机中的持久化队列,不存在则自动创建-->
    <rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare= "true"/>
    <!--定义广播交换机中的持久化队列,不存在则自动创建-->
    <rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/>
    <!--定义广播交换机中的持久化队列,不存在则自动创建-->
    <rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true" />
    <rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding pattern="zimo.*" queue= "spring_topic_queue_star" />
            <rabbit:binding pattern="zimo.#" queue="spring_topic_queue_well" />
            <rabbit:binding pattern="test.#" queue="spring_topic_queue_well2" />
        </rabbit:bindings>
    </rabbit:topic-exchange>
    <!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="factory"/>
</beans>
  • 测试类
package com.zimo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
 * spring整合rabbitmq
 * @author Liu_zimo
 * @version v0.1 by 2021/6/4 15:09
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")
public class ProducerTest {
    // 1.注入RabbitTemplate
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void test_HelloWorld(){
        // 2.发送消息
        rabbitTemplate.convertAndSend("spring_queue", "hello world spring rabbitmq...");
    }
    @Test
    public void test_Fanout(){
        // 2.发送消息
        rabbitTemplate.convertAndSend("spring_fanout_exchange", "","fanout spring rabbitmq...");
    }
    @Test
    public void test_Topics(){
        // 2.发送消息
        rabbitTemplate.convertAndSend("spring_topic_exchange", "zimo.aa.bb","topics spring rabbitmq...");
    }
}

消费者

  • spring配置整合
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:context="http://www.springframework.org/schema/context"       xmlns:rabbit="http://www.springframework.org/schema/rabbit"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd        http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">    <!--加载配置文件-->    <context:property-placeholder location="classpath:rabbitmq.properties"/>    <!--定义rabbitmq connectionFactory-->    <rabbit:connection-factory id="factory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}"                               password="${rabbitmq.password}" virtual-host="${rabbitmq.virtural-host}"/>    <bean id="springQueueListener" class="com.zimo.rabbitmq.SpringQueueListener"/>    <rabbit:listener-container connection-factory="factory" auto-declare="true">        <rabbit:listener ref="springQueueListener" queue-names="spring_queue" />    </rabbit:listener-container></beans>
  • 消费者监听类
package com.zimo.rabbitmq;import org.springframework.amqp.core.Message;import org.springframework.amqp.core.MessageListener;/** * 简单模式 - 消费者监听类 * @author Liu_zimo * @version v0.1 by 2021/6/4 16:02 */public class SpringQueueListener implements MessageListener {    @Override    public void onMessage(Message message) {        System.out.println(new String(message.getBody()));    }}
  • 测试类
package com.zimo;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;/** * @author Liu_zimo * @version v0.1 by 2021/6/4 16:06 */@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml")public class ConsumerTest {    @Test    public void test_HellloWorld(){        while (true){                    }    }}

Spring整合RabbitMQ配置详解

  • rabbit:connection-factory:产生connection连接的
  • rabbit:queue:创建队列
    • id:bean的名称
    • name:queue的名称
    • durable:是否持久化
    • auto-declare:如果不存在自动创建
    • auto-delete:自动删除。最后一个消费者和该队列断开后,自动删除队列
    • exclusive:是否独占连接
  • rabbit:fanout-exchange:创建广播交换机
  • rabbit:topic-exchange:创建通配交换机
  • rabbit:direct-exchange:创建类型交换机
    • id:bean的名称
    • name:交换机的名称
  • rabbit:binding:交换机绑定
    • queue:绑定队列的名称
    • key:路由Key的名称
  • rabbit:template:定义rabbitTemplate对象,方便发送消息
  • rabbit:listener-container:定义一个监听器
    • connection-factory:connection连接工厂
    • auto-declare:如果不存在自动创建
  • rabbit:listener:定义监听消费者
    • ref:监听消费者实现MessageListener的Bean对象
    • queue-names:监听队列名称

SpringBoot整合RabbitMQ

生产者

  1. 创建生产者SpringBoot工程
  2. 引入依赖坐标
  3. 编写yml配置,基本信息配置
  4. 定义交换机,队列以及绑定关系的配置类
  5. 注入RabbitTemplate,调用方法,完成消息发送
  • 坐标依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • yml配置
# 配置RabbitMQ的基本信息spring:  rabbitmq:    host: 192.168.1.133    port: 5672    username: guest    password: guest    virtual-host: /
  • 定义配置类
package com.zimo.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * 配置类
 * @author Liu_zimo
 * @version v0.1 by 2021/6/4 18:32
 */
@Configuration
public class RabbitMQConfig {
    public static final String EXCHANGE_NAME = "boot_topic_exchange";
    public static final String QUEUE_NAME = "boot_queue";
    // 1.交换机
    @Bean("bootExchange")
    public Exchange bootExchange(){
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }
    // 2.Queue队列
    @Bean("bootQueue")
    public Queue bootQueue(){
        return QueueBuilder.durable(QUEUE_NAME).build();
    }
    // 3.队列和交换机绑定关系:绑定交换机、队列、设置routingkey
    @Bean
    public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }
}

消费者

  1. 创建生产者SpringBoot工程
  2. 引入依赖坐标
  3. 编写yml配置,基本信息配置
  4. 定义监听类,使用@RabbitListener注解完成队列监听
package com.zimo.consumer.listener;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
 * 监听消息队列
 * @author Liu_zimo
 * @version v0.1 by 2021/6/7 13:40
 */
@Component
public class RabbitMQListener {
    public static final String QUEUE_NAME = "boot_queue";
    @RabbitListener(queues = QUEUE_NAME)
    public void ListenerQueue(Message message){
        System.out.println(new String(message.getBody()));
    }
}

小结

  • SpringBoot提供了快速整合RabbitMQ的方式
  • 基本信息再yml中配置,队列交互机以及绑定关系在配置类中使用Bean的方式配置
  • 生产端直接注入RabbitTemplate完成消息发送
  • 消费端直接使用@RabbitListener完成消息接收

RabbitMQ高级特性

消息可靠性投递

在使用RabbitMQ的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ为我们提供了两种方式用来控制消息的投递可靠性模式。

  • confirm确认模式
    • 设置ConnectionFactory的publisher-confirms="true"开启确认模式
    • 使用rabbitTemplate.setConfirmCallback设置回调函数,当消息发送到exchange后回调confirm方法
      在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理
  • return退回模式
    • 设置ConnectionFactory的publisher-returns="true"开启回退模式
    • 使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage

RabbitMQ整个消息投递的路径为:Producer —> RabbitMQ Broker —> exchange —> queue —> consumer

  • 消息从producer到exchange则会返回一个confirmCallback
  • 消息从exchange到queue投递失败则会返回一个returnCallback

我们将利用这两个callback控制消息的可靠性投递

  • 在RabbitMQ中也提供了事务机制,但是性能较差。使用channel下列方法,完成事务控制:
    • txSelect(),用于将当前channel设置成transaction模式
    • txCommit(),用于提交事务
    • txRollback(),用于回滚事务
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>
    <!--定义rabbitmq connectionFactory-->
    <rabbit:connection-factory id="factory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}"
                               password="${rabbitmq.password}" virtual-host="${rabbitmq.virtural-host}"
                               publisher-confirms="true" publisher-returns="true"/>
    <!--定义管理交换机、队列-->
    <rabbit:admin connection-factory="factory"/>

    <!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="factory"/>

    <!-- 消息可靠性投递(生产端) -->
    <rabbit:queue id="test_queue_confirm" name="test_queue_confirm"></rabbit:queue>
    <rabbit:direct-exchange name="test_exchange_confirm">
        <rabbit:bindings>
            <rabbit:binding queue="test_queue_confirm" key="confirm"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>
</beans>
  • 测试案例
package com.zimo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
 * @author Liu_zimo
 * @version v0.1 by 2021/6/4 15:09
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")
public class ProducerTest {
    // 1.注入RabbitTemplate
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 确认模式:
     *      1.确认模式开启:ConnectionFactory开启publisher-confirms=“true”
     *      2.在RabbitTemplate设置ConfirmCallBack回调函数
     *      3.发送消息
     */
    @Test
    public void testConfirm(){
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * @param correlationData 相关配置信息
             * @param ack exchange交换机 是否成功收到了消息。true成功 false失败
             * @param cause 失败原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if (ack){
                    System.out.println("msg send success");
                }else {
                    System.out.println("error:" + cause);
                }
            }
        });
        rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message send");
    }
    /**
     * 回退模式:当消息发送给Exchange后,Exchange路由到Queue失败是才会执行ReturncallBack
     *      1.确认模式开启:ConnectionFactory开启publisher-returns="true"
     *      2.在RabbitTemplate设置ReturnCallBack回调函数
     *      3.设置Exchange处理消息模式:
     *          3.1 如果消息没有路由到Queue,则丢弃消息(默认)
     *          3.2 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
     */
    @Test
    public void testReturn(){
        // 设置交换机处理失败消息的模式
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * @param message 消息对象
             * @param replyCode 错误码
             * @param replyType 错误信息
             * @param exchange 交换机
             * @param routingKey 路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyType, String exchange, String routingKey) {
                System.out.println(new String(message.getBody()) + ":" + replyCode + "-" + replyType + "-" + exchange + "-" + routingKey);
            }
        });
        rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message send");
    }
}

Consumer ACK

ack指Acknowledge,确认。表示消费端收到消息后的确认方式

  • 有三种确认方式:
    • 自动确认:acknowledge="none"
    • 手动确认:acknowledge="manual"
    • 根据异常情况确认:acknowledge="auto",(这种方式使用麻烦)

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应message从RabbitMQ的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>
    <!--定义rabbitmq connectionFactory-->
    <rabbit:connection-factory id="factory" host="${rabbitmq.host}" port="${rabbitmq.port}" username="${rabbitmq.username}"
                               password="${rabbitmq.password}" virtual-host="${rabbitmq.virtural-host}"/>

    <context:component-scan base-package="com.zimo.listener"/>

    <rabbit:listener-container connection-factory="factory" auto-declare="true" acknowledge="manual">
        <rabbit:listener ref="ackListener" queue-names="test_queue_confirm" />
    </rabbit:listener-container>
</beans>
  • 监听处理类
package com.zimo.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
 * Consumer ACK机制
 *      1. 设置手动签收acknowledge="manual"
 *      2. 让监听器类实现ChannelAwareMessageListener接口
 *      3. 消息成功处理,则调用channel的basicAck()签收 ,
 *          如果失败调用channel的basicNack()拒收,broker重新发送给consumer
 * @author Liu_zimo
 * @version v0.1 by 2021/6/7 15:15
 */
@Component
public class AckListener implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 1.接收转换消息
            System.out.println(new String(message.getBody()));
            // 2.处理业务
            System.out.println("doing");
            // 3.手动签收
            channel.basicAck(deliveryTag,true); // 参数:标记、是否签收多条
        }catch (Exception e){
            // 4.拒签
            // 第三个参数:重回队列,设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
            channel.basicNack(deliveryTag, true, true);
            // channel.basicReject(deliveryTag, true); // 处理单条消息
        }
    }
}
  • rabbit:listener-container标签中设置acknowledge属性,设置ack方式
    • none:自动确认
    • manual:手动确认
  • 如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag,false);方法确认签收消息
  • 如果出现异常,则在catch中调用basicNack或basicReject,拒绝消息,让MQ重新发送消息

消息可靠性总结

  1. 持久化
    • exchange要持久化
    • queue要持久化
    • message要持久化
  2. 生产方确认Confirm
  3. 消费方确认Ack
  4. Broker高可用

消费端限流

  • <rabbit:listener-container>中配置prefetch属性设置消费端一次拉去多少消息
  • 消费端的确认模式一定为手动确认acknowledge="manual"
<rabbit:listener-container connection-factory="factory" acknowledge="manual" prefetch="1">
    <rabbit:listener ref="QOSListener" queue-names="test_queue_confirm" />
</rabbit:listener-container>
  • 测试代码
package com.zimo.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
 * Consumer限流机制
 *      1.确保ack机制为手动确认
 *      2.listener-container配置属性prefetch="1",每次从mq拉取一条数据消费,直到手动确认才继续拉取下一条消息
 *
 * @author Liu_zimo
 * @version v0.1 by 2021/6/7 16:03
 */
@Component
public class QOSListener implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        // 获取消息
        System.out.println(new String(message.getBody()));
        // 处理业务
        System.out.println("处理业务");
        // 手动签收
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }
}

TTL

  • TTL全称Time To Live(存活时间/过期时间)
  • 当消息到达存活时间后,还没有被消费,会被自动清除
  • RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间
<!--TTL-->
<rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
    <!--设置queue参数-->
    <rabbit:queue-arguments>
        <!--设置x-message-ttl队列的过期时间-->
        <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"></entry>
    </rabbit:queue-arguments>
</rabbit:queue>
<rabbit:topic-exchange name="test_exchange_ttl">
    <rabbit:bindings>
        <rabbit:binding pattern="ttl.#" queue="test_queue_ttl"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic-exchange>
/**
 * TTL:过期时间
 *  1.队列统一过期
 *  2.单独消息过期
 *  如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准
 *  队列过期后,会将队列所有消息全部移除
 *  消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉》
 */
@Test
public void testTTL(){
    // for (int i = 0; i < 10; i++) {
    //     rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.aa","hehe...ttl");
    // }
    // 消息单独过期
    rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.confirm", "message ttl", new MessagePostProcessor(){
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            // 1.设置message的信息
            message.getMessageProperties().setExpiration("5000");   // 消息的过期时间
            // 2.返回该消息
            return message;
        }
    });
    for (int i = 0; i < 10; i++) {
        rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.aa","hehe...ttl");
    }
}
  • 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期
  • 设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时)会单独判断这一消息是否过期
  • 如果两者都进行了设置,以时间短的为准。

死信队列

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX

死信队列原理图

  • 消息成为死信的三种情况:
    1. 队列消息长度到达限制
    2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false
    3. 原队列存在消息过期设置,消息到达超时时间未被消费
队列绑定死信交换机
  • 给队列设置参数:x-dead-letter-exchangex-dead-letter-routing-key
    • x-dead-letter-exchange:死信队列交换机名称
    • x-dead-letter-routing-key:死信队列绑定的routingKey
<!--
    死信队列:
        1.声明正常的队列和交换机
        2.声明死信队列和死信交换机
        3.正常队列绑定死信交换机:绑定两个参数
            - x-dead-letter-exchange:死信交换机名称
            - x-dead-letter-routing-key:发送给死信交换机的routingkey
-->
<rabbit:queue name="test_queue" id="test_queue">
    <rabbit:queue-arguments>
        <entry key="x-dead-letter-exchange" value="dlx_exchange" />
        <entry key="x-dead-letter-routing-key" value="dlx.error" />
        <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
        <entry key="x-max-length" value="10" value-type="java.lang.Integer"/>
    </rabbit:queue-arguments>
</rabbit:queue>
<rabbit:topic-exchange name="test_exchange">
    <rabbit:bindings>
        <rabbit:binding pattern="test.dlx.#" queue="test_queue"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic-exchange>

<rabbit:queue name="dlx_queue" id="dlx_queue"></rabbit:queue>
<rabbit:topic-exchange name="dlx_exchange">
    <rabbit:bindings>
        <rabbit:binding pattern="dlx.#" queue="dlx_queue"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic-exchange>
  • 消费者
# 监听绑定了死信队列的正常队列
<rabbit:listener ref="dlxListener" queue-names="test_queue" />
package com.zimo.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
 * 死信队列拒收消息测试
 * @author Liu_zimo
 * @version v0.1 by 2021/6/7 16:03
 */
@Component
public class DlxListener implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        // 拒绝签收
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), true, false);
    }
}

  • 测试用例
/**
 * 发送测试死信消息:
 *  1.过期时间
 *  2.长度限制
 *  3.拒收消息
 */
@Test
public void testDLX(){
    // 1.测试过期时间
    rabbitTemplate.convertAndSend("test_exchange", "test.dlx.hehe", "this is msg...");
    // 2.测试长度限制
    for (int i = 0; i < 15; i++) {
        rabbitTemplate.convertAndSend("test_exchange", "test.dlx.hehe", "this is msg...");
    }
    // 3.测试消息拒收
    rabbitTemplate.convertAndSend("test_exchange", "test.dlx.hehe", "this is msg...");
}
小结
  1. 死信交换机和死信队列和普通的没有区别
  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
  3. 消息成为死信的三种情况:
    • 队列消息长度到达限制;
    • 消费者拒接消费消息,并且不重回队列
    • 原队列存在消息过期设置,消息到达超时时间未被消费

延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费,在RabbitMQ中并未提供延迟队列功能。

但是可以使用:TTL+死信队列组合实现延迟队列的效果。

  • 需求:
    1. 下单后,30分钟未支付,取消订单,回滚库存
    2. 新用户注册成功7天后,发送短信问候
  • 实现方式:
    1. 定时器
    2. 延迟队列

延迟队列原理

  • 生产者
<!--
    延迟队列:
        1.声明正常的队列和交换机
        2.声明死信队列和死信交换机
        3.绑定,设置正常队列过期时间为30分钟
-->
<rabbit:queue id="order_queue" name="order_queue">
    <rabbit:queue-arguments>
        <entry key="x-deal-letter-exchange" value="order_exchange_dlx" />
        <entry key="x-deal-letter-routing-key" value="order.dlx.cencel" />
        <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
    </rabbit:queue-arguments>
</rabbit:queue>
<rabbit:topic-exchange name="order_exchange">
    <rabbit:bindings>
        <rabbit:binding pattern="order.#" queue="order_queue"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic-exchange>
<rabbit:queue id="order_queue_dlx" name="order_queue_dlx"></rabbit:queue>
<rabbit:topic-exchange name="order_exchange_dlx">
    <rabbit:bindings>
        <rabbit:binding pattern="order.dlx.#" queue="order_queue_dlx"></rabbit:binding>
    </rabbit:bindings>
</rabbit:topic-exchange>
@Test
public void testDelay(){
    // 1.发送订单消息
    rabbitTemplate.convertAndSend("order_exchange", "order.msg", "订单信息:id=1,time=18点20分");
}
  • 消费者
<rabbit:listener-container connection-factory="factory" auto-declare="true" acknowledge="manual" prefetch="1">
    <!-- 延迟队列监听死信队列 -->
    <rabbit:listener ref="orderListener" queue-names="order_queue_dlx" />
</rabbit:listener-container>
package com.zimo.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
 * 订单消费者
 * @author Liu_zimo
 * @version v0.1 by 2021/6/7 16:03
 */
@Component
public class OrderListener implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            // 获取消息
            System.out.println(new String(message.getBody()));
            // 处理业务
            System.out.println("根据id查询订单状态...");
            System.out.println("判断状态是否为支付成功");
            System.out.println("取消未支付订单,回滚库存");
            // 签收
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
        }catch (Exception e){
            // 拒绝签收
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), true, false);
        }
    }
}
小结
  1. 延迟队列指消息进入队列后,可以被延迟一定时间,再进行消费
  2. RabbitMQ没有提供延迟队列功能,但是可以使用:TTL+ DLX来实现延迟队列效果

日志与监控

RabbitMQ默认日志存放路径:/var/log/rabbitmq/rabbit@xxx.log

  • 查看队列:rabbitmqctl list_queue
  • 查看exchanges:rabbitmqctl list_exchanges
  • 查看用户:rabbitmqctl list_users
  • 查看连接:rabbitmqctl list_connections
  • 查看消费者信息:rabbitmqctl list_consumers
  • 查看环境变量:rabbitmqctl environment
  • 杳看未被确认的队列:rabbitmqctl list_queues name messages_unacknowledged
  • 查看单个队列的内存使用:rabbitmqctl list_queues name memory
  • 查看准备就绪的队列:rabbitmqctl list_queues name messages_ready

消息可靠性分析与追踪

  • 在RabbitMQ中可以使用Firehose和rabbitmq_tracing插件功能来实现消息追踪,开发环境下调试使用,真正线上生产环境时慎用,会影响性能
消息追踪 - Firehose

Firehose的机制是将生产者投递给rabbitmq的消息,rabbitmq投递给消费者的消息按照指定的格式发送到默认的exchange上。这个默认的exchange的名称为amq.rabbitmq.trace。它是一个topic类型的exchange。发送到这个exchange上的消息的routing key为 publish.exchangename和deliver.queuename。其中exchangename和queuename为实际exchange和queue的名称,分别对应生产者投递到exchange的消息,和消费者从queue上获取的消息。

打开trace会影响消息写入功能,适当打开后请关闭。

  • rabbitmqctl trace_on:开启Firehose命令
  • rabbitmqctl trace_off:关闭Firehose命令
消息追踪 - rabbitmq_tracing

rabbitmq tracing和Firehose在实现上如出一辙,只不过rabbitmq _tracing的方式比Firehose多了层GUI的包装,更容易使用和管理。

  • 启用插件:rabbitmq-plugins enable rabbitmq_tracing

管理

RabbitMQ应用问题

消息可靠性保障

需求:100%确保消息发送成功

消息补偿机制

消息补偿机制

消息幂等性处理

幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。

乐观锁解决方案

消息幂等性处理

RabbitMQ集群搭建

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

集群方案

RabbitMQ高可用集群

单机多实例部署

由于某些因素的限制,有时候你不得不在一台机器上去搭建一个rabbitmq集群,这个有点类似zookeeper的单机版。真实生成环境还是要配成多机集群的。有关怎么配置多机集群的可以参考其他的资料,这里主要论述如何在单机中配置多个rabbitmq实例。

  • 确保MQ正常运行,没有问题
    • 查看状态:rabbitmqctl status
    • 停止服务:service rabbitmq-server stop
    • 启动第一个节点:RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit1 rabbitmq-server start
    • 启动第二个节点:RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port, 15673}]" RABBITMQ_NODENAME=rabbit2 rabbitmq-server start
      web管理插件(后台管理系统)端口占用,所以还要指定其web插件占用的端口号
      • 停止服务:rabbitmqctl -n rabbit1/rabbit2 stop
  • rabbit1操作作为主节点:
    • 停止rabbit1节点:rabbitmqct1 -n rabbit1 stop_app
    • 重置rabbit1节点:rabbitmqctl -n rabbit1 reset
    • 启动rabbit1节点rabbitmqctl -n rabbit1 start_app
  • rabbit2操作作为从节点:
    • 停止rabbit2节点:rabbitmqct1 -n rabbit2 stop_app
    • 重置rabbit2节点:rabbitmqctl -n rabbit2 reset
    • 加入rabbit1节点集群:rabbitmqctl -n rabbit2 join_cluster rabbit1@'主机名'
    • 启动rabbit2节点rabbitmqctl -n rabbit2 start_app
  • 查看集群状态:rabbitmqctl cluster_status -n rabbit1
RabbitMQ镜像集群配置
  • 要复制队列内容到集群里的每个节点,必须要创建镜像队列
  • rabbitmqctl set_policy my_ha "^"'{"ha-mode":"all"}'
  • 图形化界面设置Admin - Policies - add/ update a policy
    • Name:策略名称
    • Pattern:匹配规则,^是所有
    • Definition:使用ha-mode模式中的all,也就是同步所有匹配的队列
负载均衡 - HAProxy

HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单—进程模型,此模型支持非常大的并发连接数。

  • 安装HAProxy

    1. 下载依赖包:gcc、vim、wget、haproxy
    2. 解压haproxy
    3. 进入目录、编译、安装
    4. 赋权
    5. 创建haproxy.cfg配置文件
  • 配置HAProxy

    listen rabbitmq_cluster
    	bind 0.0.0.0:5672	//代理对外提供服务的端口
    	...
    	blance roundrobin
    		server node1 127.0.0.1:5673 check inter 5000 rise 2 fall 2	// 绑定的服务端口 重试2次 失败2次 就不再重试
    		server node2 127.0.0.1:5674 check inter 5000 rise 2 fall 2	// 绑定的服务端口
    		
    listen stats	// ha管理控制台端口
    	bind 192.168.1.133:8100
    	...
    
  • 启动HAProxy负载

    • 启动并指定配置文件:安装路径/sbin/haproxy -f /etc/haproxy/haproxy.cfg
    • 查看haproxy进程状态:ps -ef | grep haproxy
    • 访问如下地址对mq节点进行监控:http://192.168.1.133:8100/rabbitmq-stats
    • 代码中访问mq集群地址,则变为访问haproxy地址:5672
public static void main(String[] args) throws IOException, TimeoutException {
    // 1.创建连接工厂
    ConnectionFactory factory = new ConnectionFactory();
    // 2.设置参数
    factory.setHost("192.168.1.133");   // 设置HAProxy的ip地址
    factory.setPort(5672);              // 设置HAProxy端口
    // 3.获取连接 Connection
    Connection connection = factory.newConnection();
    // 4.创建Channel
    Channel channel = connection.createChannel();
    // 5.创建队列Queue
    // 如果没有hello_world的队列,则会创建该队列,如果有则不会创建
    channel.queueDeclare("work_queues", true,false,false,null);
    // 6.发送消息
    for (int i = 0; i < 10; i++) {
        String body = i + ": hello rabbitmq~~";
        channel.basicPublish("","work_queues",null,body.getBytes());
    }
    // 7.释放资源
    channel.close();
    connection.close();
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳子陌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值