RabbitMQ

116 阅读13分钟

RabbitMQ

RabbitMQ的安装部署

CentOS7安装RabbitMQ单机版(3.8.4)

gper.club/articles/7e…

Docker安装RabbitMQ集群

gper.club/articles/7e…

​ RabbitMQ默认端口:5672;UI端口:15672;集群端口:25672;

image-20210731164406070

1. MQ的作用

  • 异步
  • 解耦
  • 削峰

image-20210731160753156

2. RabbitMQ的工作模型

RabbitMQ是用Erlang语言编写的

特性:

高可靠

灵活的路由

支持多客户端

集群与扩展性

高可用队列

权限管理

插件系统

与Spring集成 Spring AMQP

工作模型

Broker: 主机;VHost: 虚拟主机;Exchange: 交换机;Binding: 绑定;Queue: 队列;Channel: 信道

image-20210731160312131

利用VHost虚拟主机可以实现多租户的权限隔离,使用时互相之间不影响。

3. Exchange消息路由

  • Direct 直连
image-20210731161005673
  • Topic 主题
    • * 代表一个单词
    • # 代表0个或多个单词
    • .隔开的代表一个单词,a.bc.def:a bc def三个单词
image-20210731161040394
  • Fanout 广播
image-20210731161114945

4. 基本使用 Java API

​ 安装RabbitMQ(需要先安装Erlang):略

​ Java使用RabbitMQ

4.1 引入依赖

<dependencies>
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.6.0</version>
    </dependency>
</dependencies>

4.2 消费者

package com.gupaoedu.simple;

import com.rabbitmq.client.*;
import java.io.IOException;

public class MyConsumer {
    private final static String EXCHANGE_NAME = "SIMPLE_EXCHANGE";
    private final static String QUEUE_NAME = "SIMPLE_QUEUE";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        // 连接IP
        factory.setHost("127.0.0.1");
        // 默认监听端口
        factory.setPort(5672);
        // 虚拟机 VHost
        factory.setVirtualHost("/");

        // 设置访问的用户 默认guest
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        // 声明交换机(创建交换机,已存在不会报错)
        // String exchange 交换机名称
      	// String type 类型
      	// boolean durable 是否持久化
      	// boolean autoDelete 是否自动删除
      	// Map<String, Object> arguments 其他参数
        channel.exchangeDeclare(EXCHANGE_NAME,"direct",false, false, null);

        // 声明队列
        // String queue 队列名称
      	// boolean durable 是否持久化
      	// boolean exclusive 是否排外的
      	// boolean autoDelete 是否自动删除
      	// Map<String, Object> arguments 其他参数
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" Waiting for message....");

        // 绑定队列和交换机(绑定键gupao.best)
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "gupao.best");

        // 创建消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("Received message : '" + msg + "'");
                System.out.println("consumerTag : " + consumerTag );
                System.out.println("deliveryTag : " + envelope.getDeliveryTag() );
            }
        };

        // 开始获取消息
        // String queue, boolean autoAck, Consumer callback
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

4.3 生产者

package com.gupaoedu.simple;

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

public class MyProducer {
    private final static String EXCHANGE_NAME = "SIMPLE_EXCHANGE";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        // 连接IP
        factory.setHost("127.0.0.1");
        // 连接端口
        factory.setPort(5672);
        // 虚拟机 VHost
        factory.setVirtualHost("/");
        // 用户 默认guest
        factory.setUsername("guest");
        factory.setPassword("guest");

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        String msg = "Hello world, Rabbit MQ";

        // 发送消息
        // String exchange, String routingKey, BasicProperties props, byte[] body
        channel.basicPublish("QUEUE_NAME", "gupao.best", null, msg.getBytes());

        channel.close();
        conn.close();
    }
}

4.4 参数详解(待补充)

5. 死信 Dead Letter 队列

  • 死信交换机 DLX
  • 死信队列 DLQ

5.1 TTL:time to live

x-message-ttl:队列可设置消息过期时间(对队列内所有消息生效)

.expiration("10000"):对每条消息设置过期时间(只对当前消息生效)

两种方式设置消息过期时间的方式都使用了,将以较小的数值为准

package com.gupaoedu.ttl;

import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.util.HashMap;
import java.util.Map;

public class TTLProducer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        String msg = "Hello world, Rabbit MQ, DLX MSG";

        // 通过队列属性设置消息过期时间
        Map<String, Object> argss = new HashMap<String, Object>();
        argss.put("x-message-ttl",6000);

        // 声明队列(默认交换机AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare("TEST_TTL_QUEUE", false, false, false, argss);

        // 对每条消息设置过期时间
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2) // 持久化消息
                .contentEncoding("UTF-8")
                .expiration("10000") // TTL
                .build();

        // 此处两种方式设置消息过期时间的方式都使用了,将以较小的数值为准

        // 发送消息  未指定交换机会发到默认的交换机上
        channel.basicPublish("", "TEST_DLX_QUEUE", properties, msg.getBytes());

        channel.close();
        conn.close();
    }
}

5.2 死信交换机和死信队列

创建队列的时候设置参数:"x-dead-letter-exchange" = "DLX_EXCHANGE"

如果设置了死信队列,触发以下条件会进入死信队列

  • 消息超时未被消费(TTL)
  • 消息被消费者拒绝并且被设置不重回队列
  • 消息超过队列最大数量或最大存储空间(x-max-length、x-max-length-bytes)

消息流转过程:

image-20210731170321796
package com.gupaoedu.dlx;

import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 消息消费者,由于消费的代码被注释掉了,
 * 10秒钟后,消息会从正常队列 TEST_DLX_QUEUE 到达死信交换机 DLX_EXCHANGE ,然后路由到死信队列DLX_QUEUE
 *
 */
public class DlxConsumer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        // 指定队列的死信交换机
        Map<String,Object> arguments = new HashMap<String,Object>();
        arguments.put("x-dead-letter-exchange","DLX_EXCHANGE");
        // arguments.put("x-expires","9000"); // 设置队列的TTL
        // arguments.put("x-max-length", 4); // 如果设置了队列的最大长度,超过长度时,先入队的消息会被发送到DLX

        // 声明队列(默认交换机AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare("TEST_DLX_QUEUE", false, false, false, arguments);

        // 声明死信交换机
        channel.exchangeDeclare("DLX_EXCHANGE","topic", false, false, false, null);
        // 声明死信队列
        channel.queueDeclare("DLX_QUEUE", false, false, false, null);
        // 绑定,此处 Dead letter routing key 设置为 #
        channel.queueBind("DLX_QUEUE","DLX_EXCHANGE","#");
        System.out.println(" Waiting for message....");

        // 创建消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("Received message : '" + msg + "'");
            }
        };

        // 开始获取消息
        // String queue, boolean autoAck, Consumer callback
        // channel.basicConsume("TEST_DLX_QUEUE", true, consumer);
    }
}
package com.gupaoedu.dlx;

import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.impl.AMQBasicProperties;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: qingshan
 * 消息生产者,通过TTL测试死信队列
 */
public class DlxProducer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        String msg = "Hello world, Rabbit MQ, DLX MSG";

        // 设置属性,消息10秒钟过期
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2) // 持久化消息
                .contentEncoding("UTF-8")
                .expiration("10000") // TTL
                .build();

        // 发送消息  未指定交换机会发到默认的交换机上
        channel.basicPublish("", "TEST_DLX_QUEUE", properties, msg.getBytes());

        channel.close();
        conn.close();
    }
}

6. 延迟队列(插件)

需要用到rabbitmq_delayed_message_exchange插件来实现消息的延迟投递

6.1 安装插件

基于延时插件实现的延时队列(需连接到Linux系统的服务),需要安装延时队列插件: www.rabbitmq.com/community-p… github.com/rabbitmq/ra…

$ cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.12/plugins

$ wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/3.9.0/rabbitmq_delayed_message_exchange-3.9.0.ez

$ rabbitmq-plugins enable rabbitmq_delayed_message_exchange 

6.2 使用

使用x-delayed-message类型声明一个Exchange

使用自定义标头x-delay发布消息,以毫秒为单位表示消息的延迟时间。消息将在x-delay毫秒后传递到相应的队列

package com.gupaoedu.dlx;

import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 *  使用延时插件实现的消息投递-消费者
 *  必须要在服务端安装rabbitmq-delayed-message-exchange插件,安装步骤见README.MD
 *  先启动消费者
 */
public class DelayPluginConsumer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://guest:guest@127.0.0.1:5672");
        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        // 声明x-delayed-message类型的exchange
        Map<String, Object> argss = new HashMap<String, Object>();
        argss.put("x-delayed-type", "direct");
        channel.exchangeDeclare("DELAY_EXCHANGE", "x-delayed-message", false,
                false, argss);

        // 声明队列
        channel.queueDeclare("DELAY_QUEUE", false,false,false,null);

        // 绑定交换机与队列
        channel.queueBind("DELAY_QUEUE", "DELAY_EXCHANGE", "DELAY_KEY");

        // 创建消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                System.out.println("收到消息:[" + msg + "]\n接收时间:" +sf.format(new Date()));
            }
        };

        // 开始获取消息
        // String queue, boolean autoAck, Consumer callback
        channel.basicConsume("DELAY_QUEUE", true, consumer);
    }
}
package com.gupaoedu.dlx;

import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 *  使用延时插件实现的消息投递-生产者
 *  必须要在服务端安装rabbitmq-delayed-message-exchange插件,安装步骤见README.MD
 *  先启动消费者
 */
public class DelayPluginProducer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://guest:guest@127.0.0.1:5672");

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        // 延时投递,比如延时1分钟
        Date now = new Date();
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, +1);// 1分钟后投递
        Date delayTime = calendar.getTime();

        // 定时投递,把这个值替换delayTime即可
        // Date exactDealyTime = new Date("2019/01/14,22:30:00");

        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        String msg = "发送时间:" + sf.format(now) + ",需要投递时间:" + sf.format(delayTime);

        // 延迟的间隔时间,目标时刻减去当前时刻
        Map<String, Object> headers = new HashMap<String, Object>();
        headers.put("x-delay", delayTime.getTime() - now.getTime());

        AMQP.BasicProperties.Builder props = new AMQP.BasicProperties.Builder()
                .headers(headers);
        channel.basicPublish("DELAY_EXCHANGE", "DELAY_KEY", props.build(),
                msg.getBytes());

        channel.close();
        conn.close();
    }
}

7. 流量控制

7.1 服务端流控(删除先入队或拒收)

  • 设置队列容量(会删除先入队的消息
    • x-max-length 最大数量
    • x-max-length-bytes 最大存储空间
  • 内存控制(达到设定值不再接收消息
    • RabbitMQ配置文件中设置:vm_memory_high_watermark = 0.4 (40%)
  • 磁盘控制(达到设定值不再接收消息
    • disk_free_limit.relative = 0.3 (30%)
    • disk_free_limit.absolute = 2GB

7.2 消费端流控

prefetch count:非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前,不进行消费新的消息。消费者未回复的消息数量,即消费者正在处理的数量

代码示例:

package com.gupaoedu.limit;

import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * 消息消费者,测试消费端限流,先启动
 */
public class LimitConsumer {
    private final static String QUEUE_NAME = "TEST_LIMIT_QUEUE";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        final Channel channel = conn.createChannel();

        // 声明队列(默认交换机AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println("Consumer1  Waiting for message....");

        // 创建消费者,并接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");

                System.out.println("Consumer1 Received message : '" + msg + "'" );

                channel.basicAck(envelope.getDeliveryTag(), true); // 手动应答

            }
        };

        // 非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前,不进行消费新的消息。
        channel.basicQos(2); // prefetch count
        channel.basicConsume(QUEUE_NAME, false, consumer); // false:非自动应答(手动应答)
    }
}

多消费者

  • 多个消费者监听同一个队列,RabbitMQ会进行消息的轮询分发(每条消息只会发送给一个消费者)

如果某些消费者任务繁重,来不及消费那么多消息,而某些其他消费者由于某些原因很快处理完了所分配到的消息,进而空闲,这样就会造成整体应用吞吐量的下降

  • 可通过 channel.basicQos(x); 限制信道上消费者所能保持的最大未确认消息的数(通过消费端限流来平衡)

8. Spring AMQP

8.1 引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.gupaoedu</groupId>
    <artifactId>spring-rabbitmq</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <!-- spring版本号 -->
        <spring.version>4.3.14.RELEASE</spring.version>
        <!-- log4j日志文件管理包版本 -->
        <slf4j.version>1.6.6</slf4j.version>
        <log4j.version>1.2.12</log4j.version>
        <!-- junit版本号 -->
        <junit.version>4.10</junit.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!--rabbitmq依赖 -->
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>1.3.5.RELEASE</version>
        </dependency>
        <!-- 日志文件管理包 -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <!-- log end -->

        <!--单元测试依赖 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

8.2 配置applicationContext.xml

<?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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <import resource="classpath*:rabbitMQ.xml" />

    <!-- 扫描指定package下所有带有如 @Controller,@Service,@Resource 并把所注释的注册为Spring Beans -->
    <context:component-scan base-package="com.gupaoedu.*" />

    <!-- 激活annotation功能 -->
    <context:annotation-config />

    <!-- 激活annotation功能 -->
    <context:spring-configured />
</beans>

log4j.properties

log4j.rootLogger=INFO,consoleAppender,fileAppender
log4j.category.ETTAppLogger=DEBUG, ettAppLogFile
log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
log4j.appender.consoleAppender.Threshold=TRACE
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.consoleAppender.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss SSS} ->[%t]--[%-5p]--[%c{1}]--%m%n
log4j.appender.fileAppender=org.apache.log4j.DailyRollingFileAppender
log4j.appender.fileAppender.File=F:/dev_logs/rabbitmq/debug1.log
log4j.appender.fileAppender.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.fileAppender.Threshold=TRACE
log4j.appender.fileAppender.Encoding=BIG5
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.fileAppender.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss SSS}-->[%t]--[%-5p]--[%c{1}]--%m%n
log4j.appender.ettAppLogFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.ettAppLogFile.File=F:/dev_logs/rabbitmq/ettdebug.log
log4j.appender.ettAppLogFile.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.ettAppLogFile.Threshold=DEBUG
log4j.appender.ettAppLogFile.layout=org.apache.log4j.PatternLayout
log4j.appender.ettAppLogFile.layout.ConversionPattern=%-d{yyyy-MM-dd HH\:mm\:ss SSS}-->[%t]--[%-5p]--[%c{1}]--%m%n

8.3 配置rabbitMQ.xml

<?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:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
     http://www.springframework.org/schema/rabbit
     http://www.springframework.org/schema/rabbit/spring-rabbit-1.2.xsd">

    <!--配置connection-factory,指定连接rabbit server参数 -->
    <rabbit:connection-factory id="connectionFactory" virtual-host="/" username="guest" password="guest" host="127.0.0.1" port="5672" />

    <!--通过指定下面的admin信息,当前producer中的exchange和queue会在rabbitmq服务器上自动生成 -->
    <rabbit:admin id="connectAdmin" connection-factory="connectionFactory" />

    <!--############分隔线############-->
  
    <!--定义queue -->
    <rabbit:queue name="MY_FIRST_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />

    <!--定义direct exchange,绑定MY_FIRST_QUEUE -->
    <rabbit:direct-exchange name="MY_DIRECT_EXCHANGE" durable="true" auto-delete="false" declared-by="connectAdmin">
        <rabbit:bindings>
            <rabbit:binding queue="MY_FIRST_QUEUE" key="FirstKey">
            </rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!--定义rabbit template用于数据的接收和发送 -->
    <rabbit:template id="amqpTemplate" connection-factory="connectionFactory" exchange="MY_DIRECT_EXCHANGE" />

    <!--消息接收者 -->
    <bean id="messageReceiver" class="com.gupaoedu.consumer.FirstConsumer"></bean>

    <!--queue listener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象 -->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener queues="MY_FIRST_QUEUE" ref="messageReceiver" />
    </rabbit:listener-container>

    <!--定义queue -->
    <rabbit:queue name="MY_SECOND_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />

    <!-- 将已经定义的Exchange绑定到MY_SECOND_QUEUE,注意关键词是key -->
    <rabbit:direct-exchange name="MY_DIRECT_EXCHANGE" durable="true" auto-delete="false" declared-by="connectAdmin">
        <rabbit:bindings>
            <rabbit:binding queue="MY_SECOND_QUEUE" key="SecondKey"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!-- 消息接收者 -->
    <bean id="receiverSecond" class="com.gupaoedu.consumer.SecondConsumer"></bean>

    <!-- queue litener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象 -->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener queues="MY_SECOND_QUEUE" ref="receiverSecond" />
    </rabbit:listener-container>

    <!--############分隔线############-->

    <!--定义queue -->
    <rabbit:queue name="MY_THIRD_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />

    <!-- 定义topic exchange,绑定MY_THIRD_QUEUE,注意关键词是pattern -->
    <rabbit:topic-exchange name="MY_TOPIC_EXCHANGE" durable="true" auto-delete="false" declared-by="connectAdmin">
        <rabbit:bindings>
            <rabbit:binding queue="MY_THIRD_QUEUE" pattern="#.Third.#"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!--定义rabbit template用于数据的接收和发送 -->
    <rabbit:template id="amqpTemplate2" connection-factory="connectionFactory" exchange="MY_TOPIC_EXCHANGE" />

    <!-- 消息接收者 -->
    <bean id="receiverThird" class="com.gupaoedu.consumer.ThirdConsumer"></bean>

    <!-- queue litener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象 -->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener queues="MY_THIRD_QUEUE" ref="receiverThird" />
    </rabbit:listener-container>

    <!--############分隔线############-->
  
    <!--定义queue -->
    <rabbit:queue name="MY_FOURTH_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />

    <!-- 定义fanout exchange,绑定MY_FIRST_QUEUE 和 MY_FOURTH_QUEUE -->
    <rabbit:fanout-exchange name="MY_FANOUT_EXCHANGE" auto-delete="false" durable="true" declared-by="connectAdmin" >
        <rabbit:bindings>
            <rabbit:binding queue="MY_FIRST_QUEUE"></rabbit:binding>
            <rabbit:binding queue="MY_FOURTH_QUEUE"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:fanout-exchange>

    <!-- 消息接收者 -->
    <bean id="receiverFourth" class="com.gupaoedu.consumer.FourthConsumer"></bean>

    <!-- queue litener 观察 监听模式 当有消息到达时会通知监听在对应的队列上的监听对象 -->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener queues="MY_FOURTH_QUEUE" ref="receiverFourth" />
    </rabbit:listener-container>
</beans>

<rabbit:connection-factory

<rabbit:admin

<rabbit:queue

<rabbit:direct-exchange

<rabbit:bindings

<rabbit:template

<rabbit:listener-container

image-20210731211911774

效果图示

image-20210731211126084

8.4 消费者

FirstConsumer

package com.gupaoedu.consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;

public class FirstConsumer implements MessageListener {
    private Logger logger = LoggerFactory.getLogger(FirstConsumer.class);

    public void onMessage(Message message) {
        logger.info("The first consumer received message : " + message.getBody());
    }
}

SecondConsumer

public class SecondConsumer implements MessageListener {
    private Logger logger = LoggerFactory.getLogger(SecondConsumer.class);

    public void onMessage(Message message) {
        logger.info("The second consumer received message : " + message);
    }
}

ThirdConsumer

public class ThirdConsumer implements MessageListener {
    private Logger logger = LoggerFactory.getLogger(ThirdConsumer.class);

    public void onMessage(Message message) {
        logger.info("The third cosumer received message : " + message);
    }
}

FourthConsumer

public class FourthConsumer implements MessageListener {
    private Logger logger = LoggerFactory.getLogger(FourthConsumer.class);

    public void onMessage(Message message) {
        logger.info("The fourth consumer received message : " + message);
    }
}

8.5 生产者

package com.gupaoedu.producer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

/**
 * 消息生产者
 */
@Service
public class MessageProducer {
    private Logger logger = LoggerFactory.getLogger(MessageProducer.class);

    @Autowired
    @Qualifier("amqpTemplate")
    private AmqpTemplate amqpTemplate;

    @Autowired
    @Qualifier("amqpTemplate2")
    private AmqpTemplate amqpTemplate2;

    /**
     * 演示三种交换机的使用
     *
     * @param message
     */
    public void sendMessage(Object message) {
        logger.info("Send message:" + message);

        // amqpTemplate 默认交换机 MY_DIRECT_EXCHANGE
        // amqpTemplate2 默认交换机 MY_TOPIC_EXCHANGE

        // Exchange 为 direct 模式,直接指定routingKey
        amqpTemplate.convertAndSend("FirstKey", "[Direct,FirstKey] "+message);
        amqpTemplate.convertAndSend("SecondKey", "[Direct,SecondKey] "+message);

        // Exchange模式为topic,通过topic匹配关心该主题的队列
        amqpTemplate2.convertAndSend("msg.Third.send","[Topic,msg.Third.send] "+message);

        // 广播消息,与Exchange绑定的所有队列都会收到消息,routingKey为空
        amqpTemplate2.convertAndSend("MY_FANOUT_EXCHANGE",null,"[Fanout] "+message);
    }
}

8.6 测试类

package com.gupaoedu.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.gupaoedu.producer.MessageProducer;

public class RabbitTest {
    private ApplicationContext context = null;

    @Test
    public void sendMessage() {
        context = new ClassPathXmlApplicationContext("applicationContext.xml");
        MessageProducer messageProducer = (MessageProducer) context.getBean("messageProducer");
        int k = 100;
        while (k > 0) {
            messageProducer.sendMessage("第" + k + "次发送的消息");
            k--;
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

9. Spring Boot 集成 RabbitMQ

  • 对象定义:创建交换机、队列、绑定
  • 监听关系:消费者类监听队列
  • 发送消息:注入模板发送消息

案例:

image-20210731212230367

9.1 消费者工程

  1. 依赖和配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.gupaoedu</groupId>
	<artifactId>springboot-rabbit-consumer</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>springboot-rabbit-consumer</name>
	<description>Demo project for RabbitMQ Consumer</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.6.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

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

		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.9.5</version>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.21</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

application.properties

server.port=9072
// 手动ack
spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.cache.channel.size=

gupaomq.properties

com.gupaoedu.directexchange=GP_DIRECT_EXCHANGE
com.gupaoedu.topicexchange=GP_TOPIC_EXCHANGE
com.gupaoedu.fanoutexchange=GP_FANOUT_EXCHANGE
com.gupaoedu.firstqueue=GP_FIRST_QUEUE
com.gupaoedu.secondqueue=GP_SECOND_QUEUE
com.gupaoedu.thirdqueue=GP_THIRD_QUEUE
com.gupaoedu.fourthqueue=GP_FOURTH_QUEUE
package com.gupaoedu.entity;

import java.io.Serializable;

public class Merchant implements Serializable {
    int id; // 商户编号
    String name; // 商户名称
    String address; // 商户地址
    String accountNo; // 商户账号
    String accountName; // 户名
    String state; // 状态 1 激活 2 关闭
    String stateStr; // 状态中文

    public Merchant() {
    }

    public Merchant(int id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public String getAccountName() {
        return accountName;
    }

    public void setAccountName(String accountName) {
        this.accountName = accountName;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String getStateStr() {
        if(null == state){
            return "";
        }else if("1".equals(state.toString())){
            return "激活";
        }else if ("0".equals(state.toString())){
            return "关闭";
        }else{
            return "未知";
        }
    }

    public void setStateStr(String stateStr) {
        this.stateStr = state;
    }

    @Override
    public String toString() {
        return "Merchant{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                ", accountNo='" + accountNo + '\'' +
                ", accountName='" + accountName + '\'' +
                ", state='" + state + '\'' +
                ", stateStr='" + stateStr + '\'' +
                '}';
    }
}
  1. RabbitMQ配置类 RabbitConfig
package com.gupaoedu.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;


@Configuration
@PropertySource("classpath:gupaomq.properties")
public class RabbitConfig {
    @Value("${com.gupaoedu.firstqueue}")
    private String firstQueue;

    @Value("${com.gupaoedu.secondqueue}")
    private String secondQueue;

    @Value("${com.gupaoedu.thirdqueue}")
    private String thirdQueue;

    @Value("${com.gupaoedu.fourthqueue}")
    private String fourthQueue;

    @Value("${com.gupaoedu.directexchange}")
    private String directExchange;

    @Value("${com.gupaoedu.topicexchange}")
    private String topicExchange;

    @Value("${com.gupaoedu.fanoutexchange}")
    private String fanoutExchange;

    // 创建四个队列
    @Bean("vipFirstQueue")
    public Queue getFirstQueue(){
        return new Queue(firstQueue);
    }

    @Bean("vipSecondQueue")
    public Queue getSecondQueue(){
        return new Queue(secondQueue);
    }

    @Bean("vipThirdQueue")
    public  Queue getThirdQueue(){
        return  new Queue(thirdQueue);
    }

    @Bean("vipFourthQueue")
    public  Queue getFourthQueue(){
        return  new Queue(fourthQueue);
    }

    // 创建三个交换机
    @Bean("vipDirectExchange")
    public DirectExchange getDirectExchange(){
        return new DirectExchange(directExchange);
    }

    @Bean("vipTopicExchange")
    public TopicExchange getTopicExchange(){
        return new TopicExchange(topicExchange);
    }

    @Bean("vipFanoutExchange")
    public FanoutExchange getFanoutExchange(){
        return new FanoutExchange(fanoutExchange);
    }

    // 定义四个绑定关系
    @Bean
    public Binding bindFirst(@Qualifier("vipFirstQueue") Queue queue, @Qualifier("vipDirectExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("gupao.best");
    }

    @Bean
    public Binding bindSecond(@Qualifier("vipSecondQueue") Queue queue, @Qualifier("vipTopicExchange") TopicExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("*.gupao.*");
    }

    @Bean
    public Binding bindThird(@Qualifier("vipThirdQueue") Queue queue, @Qualifier("vipFanoutExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

    @Bean
    public Binding bindFourth(@Qualifier("vipFourthQueue") Queue queue, @Qualifier("vipFanoutExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

    /**
     * 在消费端转换JSON消息
     * 监听类都要加上containerFactory属性
     * @param connectionFactory
     * @return
     */
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setAutoStartup(true);
        return factory;
    }
}
  1. 消费者
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.firstqueue}", containerFactory="rabbitListenerContainerFactory")
public class FirstConsumer {

    @RabbitHandler
    public void process(@Payload Merchant merchant){

        System.out.println("First Queue received msg : " + merchant.getName());
        
    }

}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.secondqueue}", containerFactory="rabbitListenerContainerFactory")
public class SecondConsumer {
    @RabbitHandler
    public void process(String msgContent,Channel channel, Message message) throws IOException {
        System.out.println("Second Queue received msg : " + msgContent );

        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.thirdqueue}", containerFactory="rabbitListenerContainerFactory")
public class ThirdConsumer {
    @RabbitHandler
    public void process(String msg){
        System.out.println("Third Queue received msg : " + msg);
    }
}
@Component
@PropertySource("classpath:gupaomq.properties")
@RabbitListener(queues = "${com.gupaoedu.fourthqueue}", containerFactory="rabbitListenerContainerFactory")
public class FourthConsumer {
    @RabbitHandler
    public void process(String message) throws IOException {

        System.out.println("Fourth Queue received msg : " + message);
    }
}
  1. 启动类
@SpringBootApplication
public class RabbitConsumerApp {
   public static void main(String[] args) {
      SpringApplication.run(RabbitConsumerApp.class, args);
   }
}

9.2 生产者工程

实战demo见工程springboot-project:springboot-rabbit-producer

Spring AMQP默认使用的消息转换器是SimpleMessageConverter。

Jackson2JsonMessageConverter 用于将消息转换为JSON后发送。

com.gupaoedu.directexchange=GP_DIRECT_EXCHANGE
com.gupaoedu.topicexchange=GP_TOPIC_EXCHANGE
com.gupaoedu.fanoutexchange=GP_FANOUT_EXCHANGE
com.gupaoedu.directroutingkey=gupao.best
com.gupaoedu.topicroutingkey1=shanghai.gupao.teacher
com.gupaoedu.topicroutingkey2=changsha.gupao.student
@Configuration
public class RabbitConfig {
    /**
     * 所有的消息发送都会转换成JSON格式发到交换机
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTemplate gupaoTemplate(final ConnectionFactory connectionFactory) {
        final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        return rabbitTemplate;
    }
}
package com.gupaoedu.producer;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gupaoedu.entity.Merchant;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:gupaomq.properties")
public class RabbitSender {

    @Value("${com.gupaoedu.directexchange}")
    private String directExchange;

    @Value("${com.gupaoedu.topicexchange}")
    private String topicExchange;

    @Value("${com.gupaoedu.fanoutexchange}")
    private String fanoutExchange;

    @Value("${com.gupaoedu.directroutingkey}")
    private String directRoutingKey;

    @Value("${com.gupaoedu.topicroutingkey1}")
    private String topicRoutingKey1;

    @Value("${com.gupaoedu.topicroutingkey2}")
    private String topicRoutingKey2;


    // 自定义的模板gupaoTemplate,所有的消息都会转换成JSON发送
    @Autowired
    AmqpTemplate gupaoTemplate;

    public void send() throws JsonProcessingException {
        Merchant merchant =  new Merchant(1001,"a direct msg : 中原镖局","汉中省解放路266号");
        gupaoTemplate.convertAndSend(directExchange,directRoutingKey, merchant);

        gupaoTemplate.convertAndSend(topicExchange,topicRoutingKey1, "a topic msg : shanghai.gupao.teacher");
        gupaoTemplate.convertAndSend(topicExchange,topicRoutingKey2, "a topic msg : changsha.gupao.student");

        // 发送JSON字符串
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(merchant);
        System.out.println(json);
        gupaoTemplate.convertAndSend(fanoutExchange,"", json);
    }

}

测试类

package com.gupaoedu;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.gupaoedu.producer.RabbitSender;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * 记得一定要先启动消费者,否则交换机和队列以及绑定关系都不会创建
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class AppTests {

    @Autowired
    RabbitSender rabbitSender;

   @Test
   public void contextLoads() throws JsonProcessingException {
       // 先启动消费者 consumer,否则交换机、队列、绑定都不存在
        rabbitSender.send();
   }

}

10. 消息可靠性投递

  • 生产者发送消息给Broker 的可靠性
  • Exchange路由给Queue 的可靠性
  • 队列存储消息 的可靠性
  • 消费者消费消息 的可靠性(Queue到Consumer)

image-20210731214629402

10.1 服务端确认(事务消息)

1. Transaction 事务模式

image-20210731223449376

// 将channel设置成事务模式
channel.txSelect(); 
// 发送消息。String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("", QUEUE_NAME, null, (msg).getBytes());
// ...
// 无问题 提交事务 
channel.txCommit(); 
// try/catch 有异常 事务回滚 
channel.txRollback();

2. Confirm 确认模式

image-20210731223516332

普通模式

// 开启发送方确认模式:将channel设置为Confirm模式 
channel.confirmSelect();
// 发送消息
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
// ①普通Confirm,发送一条,确认一条
if (channel.waitForConfirms()) {
		// 消息发送成功 
}

批量确认模式

try {
    channel.confirmSelect();
    for (int i = 0; i < 1000; i++) {
        // 发送消息
        // String exchange, String routingKey, BasicProperties props, byte[] body
        channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
    }
    // 批量确认结果,ACK如果是Multiple=True,代表ACK里面的Delivery-Tag之前的消息都被确认了
    // 比如5条消息可能只收到1个ACK,也可能收到2个(抓包才看得到)
    // 直到所有信息都发布,只要有一个未被Broker确认就会IOException
    channel.waitForConfirmsOrDie();
    System.out.println("消息发送完毕,批量确认成功");
} catch (Exception e) {
    // 发生异常,可能需要对所有消息进行重发
    e.printStackTrace();
}

异步确认模式

// 用来维护未确认消息的deliveryTag
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());

// 这里不会打印所有响应的ACK;ACK可能有多个,有可能一次确认多条,也有可能一次确认一条
// 异步监听确认和未确认的消息
// 如果要重复运行,先停掉之前的生产者,清空队列
channel.addConfirmListener(new ConfirmListener() {
  	// 未确认的消息
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        // 如果true表示批量执行了deliveryTag这个值以前(小于deliveryTag的)的所有消息,如果为false的话表示单条确认
      	System.out.println("Broker未确认消息,标识:" + deliveryTag);
        if (multiple) {
            // headSet表示后面参数之前的所有元素,全部删除
            confirmSet.headSet(deliveryTag + 1L).clear();
        } else {
            // 只移除一个元素
            confirmSet.remove(deliveryTag);
        }
        // TODO 这里添加重发的方法
    }
  	// 已经确认的消息
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        // 如果true表示批量执行了deliveryTag这个值以前(小于deliveryTag的)的所有消息,如果为false的话表示单条确认
        System.out.println(String.format("Broker已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
        if (multiple) {
            // headSet表示后面参数之前的所有元素,全部删除
            confirmSet.headSet(deliveryTag + 1L).clear();
        } else {
            // 只移除一个元素
            confirmSet.remove(deliveryTag);
        }
        System.out.println("未确认的消息:"+confirmSet);
    }
});

// 开启发送方确认模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
    long nextSeqNo = channel.getNextPublishSeqNo();
    // 发送消息
    // String exchange, String routingKey, BasicProperties props, byte[] body
    channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
    confirmSet.add(nextSeqNo);
}
System.out.println("所有消息:"+confirmSet);

10.2 路由保证

默认情况下无法被路由的消息将会被丢弃。有两种方式来挽救:

  • 配置mandatory=true + 回发监听器ReturnListener

    • 消息无法路由时将回发至ReturnListener
  • 指定交换机的备份交换机

    • 创建交换机的时候设置alternate-exchange参数
import com.gupaoedu.util.ResourceUtil;

/**
 * 当消息无法匹配到队列时,会发回给生产者
 */
public class ReturnListenerProducer {
    public static void main(String[] args) throws Exception{
        ConnectionFactory factory = new ConnectionFactory();
      	// IP、端口、用户名、密码、虚拟主机
		// 默认使用 / 的vhost,如果修改vhost,加在端口后即可,如 /gphost
        factory.setUri("amqp://guest:guest@127.0.0.1:5672");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.addReturnListener(new ReturnListener() {
            public void handleReturn(int replyCode,
                                     String replyText,
                                     String exchange,
                                     String routingKey,
                                     AMQP.BasicProperties properties,
                                     byte[] body)
                    throws IOException {
                System.out.println("=========监听器收到了无法路由,被返回的消息=========");
                System.out.println("replyText:"+replyText);
                System.out.println("exchange:"+exchange);
                System.out.println("routingKey:"+routingKey);
                System.out.println("message:"+new String(body));
            }
        });

      	// 2 消息持久化
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).contentEncoding("UTF-8").build();

        // 声明备份交换机
        channel.exchangeDeclare("ALTERNATE_EXCHANGE","topic", false, false, false, null);
        channel.queueDeclare("ALTERNATE_QUEUE", false, false, false, null);
        channel.queueBind("ALTERNATE_QUEUE","ALTERNATE_EXCHANGE","#");
        
        // 在声明交换机的时候指定备份交换机
        Map<String,Object> arguments = new HashMap<String,Object>();
        arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
        channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);

        // 发送到了默认的交换机上,由于没有任何队列使用这个关键字跟交换机绑定,所以会被退回
        // 第三个参数是设置的mandatory,如果mandatory是false,消息也会被直接丢弃
        channel.basicPublish("","gupaodirect",true, properties,"只为更好的你".getBytes());

        TimeUnit.SECONDS.sleep(10);

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

10.3 消息存储

  • 队列持久化 durable = true

  • 交换机持久化 durable = true

  • 消息持久化

    • 上面的例子中2代码持久化,其他数字代表非持久化。AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).contentEncoding("UTF-8").build();
  • 集群(防止单点故障)

10.4 消费者确认

  • 默认的是自动应答(NONE):一接收到消息就会ack
    • spring.rabbitmq.listener.direct.acknowledge-mode=manual spring.rabbitmq.listener.simple.acknowledge-mode=manual
    • NONE自动ack、MANUAL手动ack、AUTO不发生异常的情况下自动ack
  • channel.basicConsume(QUEUE_NAME, false, consumer); // false 关闭自动应答
    • channel.basicAck(); // 手工应答
    • channel.basicReject(); // 单条拒绝
    • channel.basicNack(); // 批量拒绝

应答最好放到finally中,确保被执行,否则队列中的消息会一直存在

package com.gupaoedu.ack;

import com.gupaoedu.util.ResourceUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * 消息消费者,用于测试消费者手工应答和重回队列
 */
public class AckConsumer {
    private final static String QUEUE_NAME = "TEST_ACK_QUEUE";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        final Channel channel = conn.createChannel();

        // 声明队列(默认交换机AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" Waiting for message....");

        // 创建消费者,并接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("Received message : '" + msg + "'");

                if (msg.contains("拒收")){
                    // 拒绝消息
                    // 第二个参数 requeue:是否重回队列,true:是;false:直接丢弃,相当于告诉队列可以直接删除掉
                    // TODO 如果只有这一个消费者,requeue为true的时候会造成消息重复消费【死循环】
                    channel.basicReject(envelope.getDeliveryTag(), false);
                } else if (msg.contains("异常")){
                    // 批量拒绝
                  	// 第二个参数 multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息
                    // 第三个参数 requeue:是否重回队列
                    // TODO 如果只有这一个消费者,requeue为true的时候会造成消息重复消费【死循环】
                    channel.basicNack(envelope.getDeliveryTag(), true, false);
                } else {
                    // 手工应答
                    // 如果不应答,队列中的消息会一直存在,重新连接的时候会重复消费
                    // ... 业务逻辑的处理 ...
                    channel.basicAck(envelope.getDeliveryTag(), true);
                }
            }
        };

        // 开始获取消息,注意这里开启了手工应答
        // String queue, boolean autoAck, Consumer callback
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

10.5 其他

1. 消费者回调

  • 消费者调用生产者提供的API:通知生产者已处理完数据
  • 给生产者发一条消息(回执)

2. 补偿机制(重发)

  • 重发消息(消息重试机制)
    • 时间间隔
    • 重发次数
    • 消息落库,保证重发消息一致

3. 最终一致性

  • 对账(定时(比如每晚)进行批处理对账)

4. 消息的幂等性

  • 全局唯一的业务ID/流水号

5. 消息的顺序性

  • 一个消费者消费一个队列时才能保证顺序性
    • image-20210830173645046

11. 集群和高可用

集群的目的:

  • 高可用
  • 负载均衡

RabbitMQ的节点类型

  • disc(默认):磁盘节点。数据只存储到磁盘上面,是稳定、持久的
    • 至少需要1个disc,所以单节点一定是disc
    • 用来做稳定存储的节点
  • RAM:内存节点。数据同时存储在内存和磁盘上
    • 读写在内存中操作,持久化在磁盘中操作。读写效率比较高
    • 可以做对外提供服务的节点

11.1 普通集群

节点间只同步元数据,不同步队列内容

同一个集群要保证cookie一致:/var/lib/rabbitmq/.erlang.cookie

image-20210731234948021

11.2 镜像队列

1、节点之间同步所有数据,多个消费者连接到不同节点会消费到一样的数据

2、镜像队列的设计只保证了HA,并不能显著提升性能,反而因为增加节点消耗了更多硬件资源

image-20210801000402811

11.3 高可用HA

HAProxy+Keepalived搭建RabbitMQ高可用集群:gper.club/articles/7e…

VRRP:虚拟路由器冗余协议

image-20210801000100429

11.4 Docker安装RabbitMQ集群

gper.club/articles/7e…

12. 实践经验总结

  • 资源管理

    • 交换机、队列资源谁使用谁创建(消费者)
    • 创建方式可以在代码中,或者在管理界面中统一创建(就像数据库的表一样)
  • 配置文件和命名规范

    • xxx_mq.properties:统一存放交换机名字、队列名字等
    • 虚拟机后缀:_VHOST
    • 交换机后缀:_EXCHANGE
    • 队列后缀:_QUEUE
    • 体现数据来源和去向:SALE_TO_PRODUCT_EXCHANGE(销售系统跟产品系统通信用的)
  • 调用封装

    • 自定义封装,简化参数

    • 自定义封装,方便切换成其他的MQ产品

    •   GpSendMsg(Object msg){ 
          	JmsTemplate.send(destination,msg); 
        }
        GpSendMsg(Object msg){ 
          	RabbitTemplate.send(exchange,routingKey,msg); 
        }
      
  • 消息追溯与重发

    • 如果要实现消息的可追溯:入库-消息表
    • 如果要实现消息的重发:入库-消息表,加上发送状态字段
  • 生产环境运维监控

    • zabbix
    • Grafana
  • 日志追踪,消息的流入流出(影响性能,不建议做)

    • Firehose
    • Tracing GUI
  • 连接数过多(如何减少连接数)

    • 设计的时候兼容单条和多条数据的通信,多条消息合并发送减少连接次数
    • image-20210830180150719

13. RabbitMQ面试题

RabbitMQ面试题

大纲

RabbitMQ-2020大纲