【RabbitMQ】应用

目 录

  • [一. 7种工作模式介绍](#一. 7种工作模式介绍)
    • [1.1 Simple(简单模式)](#1.1 Simple(简单模式))
    • [1.2 Work Queue(工作队列)](#1.2 Work Queue(工作队列))
    • [1.3 Publish/Subscribe(发布/订阅)](#1.3 Publish/Subscribe(发布/订阅))
    • [1.4 Routing(路由模式)](#1.4 Routing(路由模式))
    • [1.5 Topics(通配符模式)](#1.5 Topics(通配符模式))
    • [1.6 RPC(RPC通信)](#1.6 RPC(RPC通信))
    • [1.7 Publisher Confirms(发布确认)](#1.7 Publisher Confirms(发布确认))
  • [二. 工作模式的使用案例](#二. 工作模式的使用案例)
    • [2.1 简单模式](#2.1 简单模式)
    • [2.2 Work Queues(工作队列)](#2.2 Work Queues(工作队列))
    • [2.3 Publish/Subscribe(发布/订阅)](#2.3 Publish/Subscribe(发布/订阅))
    • [2.4 Routing(路由模式)](#2.4 Routing(路由模式))
    • [2.5 Topics(通配符模式)](#2.5 Topics(通配符模式))
    • [2.6 RPC(RPC通信)](#2.6 RPC(RPC通信))
    • [2.7 Publisher Confirms(发布确认)](#2.7 Publisher Confirms(发布确认))
      • [2.7.1 Publishing Messages Individually(单独确认)](#2.7.1 Publishing Messages Individually(单独确认))
      • [2.7.2 Publishing Messages in Batches(批量确认)](#2.7.2 Publishing Messages in Batches(批量确认))
      • [2.7.3 Handling Publisher Confirms Asynchronously(异步确认)](#2.7.3 Handling Publisher Confirms Asynchronously(异步确认))
  • [三. Spring Boot 整合 RabbitMQ](#三. Spring Boot 整合 RabbitMQ)
    • [3.1 工作队列模式](#3.1 工作队列模式)
    • [3.2 Publish/Subscribe(发布订阅模式)](#3.2 Publish/Subscribe(发布订阅模式))
    • [3.3 Routing (路由模式)](#3.3 Routing (路由模式))
    • [3.4 Topics(通配符模式)](#3.4 Topics(通配符模式))
  • [四.基于 SpringBoot+RabbitMQ 完成应用通信](#四.基于 SpringBoot+RabbitMQ 完成应用通信)
    • [4.1 创建项目](#4.1 创建项目)
    • [4.2 订单系统(生产者)](#4.2 订单系统(生产者))
    • [4.3 物流系统(消费者)](#4.3 物流系统(消费者))
    • [4.4 启动服务, 观察结果](#4.4 启动服务, 观察结果)
    • [4.5 发送消息格式为对象](#4.5 发送消息格式为对象)

RabbitMQ 共提供了7种工作模式, 进行消息传递, 我们入门程序的案例, 其实就是⼀个简单模式

官方文档:点击跳转

一. 7种工作模式介绍

1.1 Simple(简单模式)

  • P: 生产者, 也就是要发送消息的程序
  • C: 消费者,消息的接收者
  • Queue: 消息队列, 图中黄色背景部分. 类似⼀个邮箱, 可以缓存消息; 生产者向其中投递消息, 消费者从其中取出消息.

特点: ⼀个生产者P,⼀个消费者C, 消息只能被消费⼀次. 也称为点对点(Point-to-Point)模式

适用场景: 消息只能被单个消费者处理

1.2 Work Queue(工作队列)

⼀个生产者P,多个消费者C1,C2. 在多个消息的情况下, Work Queue 会将消息分派给不同的消费者, 每个消费者都会接收到不同的消息.

特点: 消息不会重复, 分配给不同的消费者.

适用场景: 集群环境中做异步处理

比如12306 短信通知服务, 订票成功后, 订单消息会发送到 RabbitMQ, 短信服务从 RabbitMQ 中获取订单信息, 并发送通知信息(在短信服务之间进行任务分配)

1.3 Publish/Subscribe(发布/订阅)

图中 X 表示交换机, 在订阅模型中,多了⼀个 Exchange 角色, 过程略有变化

概念介绍

Exchange: 交换机 (X).

作用: 生产者将消息发送到 Exchange , 由交换机将消息按⼀定规则路由到⼀个或多个队列中(上图中生产者将消息投递到队列中, 实际上这个在 RabbitMQ 中不会发生. )

RabbitMQ 交换机有四种类型: fanout,direct, topic, headers, 不同类型有着不同的路由策略. AMQP 协议里还有另外两种类型, System 和自定义, 此处不再描述.

  1. Fanout : 广播,将消息交给所有绑定到交换机的队列 (Publish/Subscribe模式)
  2. Direct : 定向,把消息交给符合指定 routing key 的队列 (Routing模式)
  3. Topic : 通配符,把消息交给符合 routing pattern (路由模式) 的队列 (Topics模式)
  4. headers 类型的交换器不依赖于路由键的匹配规则来路由消息, 而是根据发送的消息内容中的 headers 属性进行匹配. headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在

Exchange(交换机)只负责转发消息, 不具备存储消息的能力, 因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息就会丢失

  • RoutingKey : 路由键.生产者将消息发给交换器时, 指定的⼀个字符串, 用来告诉交换机应该如何处理这个消息.
  • Binding Key : 绑定. RabbitMQ 中通过 Binding (绑定)将交换器与队列关联起来, 在绑定的时候⼀般会指定⼀个 Binding Key, 这样 RabbitMQ 就知道如何正确地将消息路由到队列了

比如下图: 如果在发送消息时, 设置了 RoutingKey 为 orange, 消息就会路由到 Q1

当消息的 Routing key 与队列绑定的 Bindingkey 相匹配时,消息才会被路由到这个队列

BindingKey 其实也属于路由键中的⼀种, 官⽅解释为 : the routingkey to use for the binding.可以翻译为:在绑定的时候使用的路由键. 大多数时候,包括官方文档和 RabbitMQJava API 中都把 BindingKey 和 RoutingKey 看作 RoutingKey, 为了避免混淆,可以这么理解:

  1. 在使用绑定的时候,需要的路由键是 BindingKey.

  2. 在发送消息的时候,需要的路由键是 RoutingKey

    本文后续也可能把两者合称为 Routing Key, 根据使用场景来区分

Publish/Subscribe模式

⼀个生产者P, 多个消费者 C1, C2, X 代表交换机消息复制多份,每个消费者接收相同的消息生产者发送⼀条消息,经过交换机转发到多个不同的队列,多个不同的队列就有多个不同的消费者适合场景: 消息需要被多个消费者同时接收的场景. 如: 实时通知或者广播消息

比如中国气象局发布 "天气预报" 的消息送入交换机, 新浪,百度, 搜狐, 网易等门户网站接入消息, 通过队列绑定到该交换机, 自动获取气象局推送的气象数据

1.4 Routing(路由模式)

路由模式是发布订阅模式的变种, 在发布订阅基础上, 增加路由 key

发布订阅模式是无条件的将所有消息分发给所有消费者, 路由模式是 Exchange 根据 RoutingKey 的规则,将数据筛选后发给对应的消费者队列

适合场景: 需要根据特定规则分发消息的场景.

比如系统打印日志, 日志等级分为 error, warning, info,debug, 就可以通过这种模式,把不同的日志发送到不同的队列, 最终输出到不同的文件

1.5 Topics(通配符模式)

路由模式的升级版, 在 routingKey 的基础上,增加了通配符的功能, 使之更加灵活.

Topics 和 Routing 的基本原理相同,即:生产者将消息发给交换机,交换机根据 RoutingKey 将消息转发给与 RoutingKey 匹配的队列. 类似于正则表达式的方式来定义 Routingkey 的模式.

不同之处是:routingKey 的匹配方式不同,Routing 模式是相等匹配,topics 模式是通配符匹配.

适合场景: 需要灵活匹配和过滤消息的场景

1.6 RPC(RPC通信)

在 RPC 通信的过程中, 没有生产者和消费者, 比较像咱们 RPC 远程调用, 大概就是通过两个队列实现了⼀个可回调的过程.

  1. 客户端发送消息到⼀个指定的队列, 并在消息属性中设置 replyTo 字段, 这个字段指定了⼀个回调队列, 用于接收服务端的响应.
  2. 服务端接收到请求后, 处理请求并发送响应消息到 replyTo 指定的回调队列
  3. 客⼾端在回调队列上等待响应消息. ⼀旦收到响应,客户端会检查消息的 correlationId 属性,以确保它是所期望的响应

1.7 Publisher Confirms(发布确认)

Publisher Confirms 模式是 RabbitMQ 提供的⼀种确保消息可靠发送到 RabbitMQ 服务器的机制。在这种模式下,⽣产者可以等待 RabbitMQ 服务器的确认,以确保消息已经被服务器接收并处理.

  1. 生产者将Channel设置为confirm模式(通过调用 channel.confirmSelect()完成 )后, 发布的每⼀条消息都会获得⼀个唯⼀的 ID, 生产者可以将这些序列号与消息关联起来,以便跟踪消息的状态.
  2. 当消息被 RabbitMQ 服务器接收并处理后,服务器会异步地向生产者发送⼀个确认(ACK)给生产者(包含消息的唯⼀ID),表明消息已经送达.

通过 Publisher Confirms 模式,生产者可以确保消息被 RabbitMQ 服务器成功接收, 从而避免消息丢失的问题.

适用场景: 对数据安全性要求较高的场景. 比如金融交易, 订单处理.

二. 工作模式的使用案例

在前面学习了简单模式的写法, 接下来学习另外几种工作模式的写法

2.1 简单模式

快速入门程序就是简单模式. 此处省略

2.2 Work Queues(工作队列)

简单模式的增强版, 和简单模式的区别就是: 简单模式有⼀个消费者, 工作队列模式支持多个消费者接收消息, 消费者之间是竞争关系, 每个消息只能被⼀个消费者接收

步骤:

  1. 引入依赖
  2. 编写生产者代码
  3. 编写消费者代码

引入依赖

xml 复制代码
<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.7.3</version>
</dependency>

编写生产者代码

工作队列模式和简单模式区别是有多个消费者, 所以生产者消费者代码差异不大

相比简单模式, 生产者的代码基本⼀样, 为了能看到多个消费者竞争的关系, 我们⼀次发送10条消息

我们把发送消息的地方, 改为⼀次发送10条消息

java 复制代码
for (int i = 0; i < 10; i++) {
	String msg = "Hello World" + i;
	channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
}

整体代码:

java 复制代码
public class Constants {
	public static final String HOST = "110.41.51.65";
	public static final Integer PORT = 15673;
	public static final String VIRTUAL_HOST = "bite";
	public static final String USER_NAME = "study";
	public static final String PASSWORD = "study";
	
	public static final String WORK_QUEUE_NAME = "work_queues";
}
java 复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import constant.Constants;

public class WorkRabbitProducer {

	public static void main(String[] args) throws Exception {
		//1. 创建channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		//2. 声明队列
		//如果没有⼀个这样的⼀个队列, 会⾃动创建, 如果有, 则不创建
		channel.queueDeclare(Constants.WORK_QUEUE_NAME, true, false, false, null);
		//3. 发送消息
		for (int i = 0; i < 10; i++) {
			String msg = "Hello World" + i;
			channel.basicPublish("",Constants.WORK_QUEUE_NAME,null,msg.getBytes());
		}
		//4. 释放资源
		channel.close();
		connection.close();
	}
}

编写消费者代码

消费者代码和简单模式⼀样, 只是复制两份. 两个消费者代码可以是⼀样的

java 复制代码
import com.rabbitmq.client.*;
import constant.Constants;

import java.io.IOException;

public class WorkRabbitmqConsumer1 {

	public static void main(String[] args) throws Exception {
		//1. 创建channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		//2. 声明队列
		//如果没有⼀个这样的⼀个队列, 会⾃动创建, 如果有, 则不创建
		channel.queueDeclare(Constants.WORK_QUEUE_NAME, true, false, false, null);
		
		//3. 接收消息, 并消费
		DefaultConsumer consumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
				System.out.println("接收到消息: " + new String(body));
			}
		};
		channel.basicConsume(Constants.WORK_QUEUE_NAME, true, consumer);
	}
}

运行程序, 观察结果

先启动两个消费者运行, 再启动生产者

如果先启动生产者, 再启动消费者, 由于消息较少, 处理较快, 那么第⼀个启动的消费者就会瞬间把10条消息消费掉, 所以我们先启动两个消费者, 再启动生产者

  1. 启动2个消费者
  2. 启动生产者

可以看到两个消费者都打印了消费信息

WorkQueuesConsumer1 打印:

java 复制代码
body:Hello World0
body:Hello World2
body:Hello World4
body:Hello World6
body:Hello World8

WorkQueuesConsumer2 打印:

java 复制代码
body:Hello World1
body:Hello World3
body:Hello World5
body:Hello World7
body:Hello World9

可以看到管理界面上显示两个消费者

2.3 Publish/Subscribe(发布/订阅)

在发布/订阅模型中,多了⼀个 Exchange 角色

Exchange 常见有三种类型, 分别代表不同的路由规则

  • Fanout : 广播,将消息交给所有绑定到交换机的队列 (Publish/Subscribe模式)
  • Direct : 定向,把消息交给符合指定 routing key 的队列 (Routing模式)
  • Topic : 通配符,把消息交给符合 routing pattern(路由模式)的队列 (Topics模式)

也就分别对应不同的工作模式

我们来看看 Publish/Subscribe 模式

步骤:

  1. 引入依赖
  2. 编写生产者代码
  3. 编写消费者代码

引入依赖

xml 复制代码
<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.7.3</version>
</dependency>

编写生产者代码

和前面两个的区别是 :

需要创建交换机, 并且绑定队列和交换机

创建交换机

java 复制代码
/*
	exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
	参数:
	1. exchange:交换机名称
	2. type:交换机类型
		* DIRECT("direct"), 定向,直连,routing
		* FANOUT("fanout"),扇形(⼴播), 每个队列都能收到消息
		* TOPIC("topic"),通配符
		* HEADERS("headers") 参数匹配(⼯作⽤的较少)
	3. durable: 是否持久化. 
		true-持久化,false⾮持久化.
		持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息
	4. autoDelete: ⾃动删除.
		⾃动删除的前提是⾄少有⼀个队列或者交换器与这个交换器绑定, 之后所有与这个交换器绑定的队列或者交换器都与此解绑. 
		⽽不是这种理解: 当与此交换器连接的客⼾端都断开时,RabbitMQ会⾃动删除本交换器.
	5. internal: 内部使⽤, ⼀般falase.
		如果设置为true, 表⽰内部使⽤.
		客⼾端程序⽆法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种⽅式
	6. arguments: 参数
*/

channel.exchangeDeclare(Constants.FANOUT_EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, false, null);

声明两个队列

后面验证是否两个队列都能收到消息

java 复制代码
//如果没有⼀个这样的⼀个队列, 会⾃动创建, 如果有, 则不创建
channel.queueDeclare(Constants.FANOUT_QUEUE_NAME1, true, false, false, null);
channel.queueDeclare(Constants.FANOUT_QUEUE_NAME2, true, false, false, null);

绑定队列和交换机

java 复制代码
/*
	queueBind(String queue, String exchange, String routingKey)
	参数:
	1. queue: 队列名称
	2. exchange: 交换机名称
	3. routingKey: 路由key, 路由规则
		如果交换机类型为fanout,routingkey设置为"",表⽰每个消费者都可以收到全部信息
*/
channel.queueBind(Constants.FANOUT_QUEUE_NAME1,Constants.FANOUT_EXCHANGE_NAME, "");
channel.queueBind(Constants.FANOUT_QUEUE_NAME2,Constants.FANOUT_EXCHANGE_NAME, "");

发送消息

java 复制代码
/**
	* basicPublish(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body)
	* 参数说明:
	* Exchange: 交换机名称
	* routingKey: 如果交换机类型为fanout,routingkey设置为"",表⽰每个消费者都可以收到全部信息
*/
String msg = "hello fanout";
channel.basicPublish(Constants.FANOUT_EXCHANGE_NAME,"",null,msg.getBytes());

完整代码

java 复制代码
public static String FANOUT_EXCHANGE_NAME = "test_fanout";
public static String FANOUT_QUEUE_NAME1 = "fanout_queue1";
public static String FANOUT_QUEUE_NAME2 = "fanout_queue2";
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import constant.Constants;

public class FanoutRabbitProducer {

	public static void main(String[] args) throws Exception {
		//1. 创建channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		//2. 创建交换机
		/*
		exchangeDeclare(String exchange, BuiltinExchangeType type, boolean 
		durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
		参数:
		1. exchange:交换机名称
		2. type:交换机类型
			* DIRECT("direct"), 定向,直连,routing
			* FANOUT("fanout"),扇形(⼴播), 每个队列都能收到消息
			* TOPIC("topic"),通配符
			* HEADERS("headers") 参数匹配(⼯作⽤的较少)
		3. durable: 是否持久化
		4. autoDelete: ⾃动删除
		5. internal: 内部使⽤, ⼀般falase
		6. arguments: 参数
		*/
		
		channel.exchangeDeclare(Constants.FANOUT_EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, false, null);
		//3. 声明队列
		//如果没有⼀个这样的⼀个队列, 会⾃动创建, 如果有, 则不创建
		channel.queueDeclare(Constants.FANOUT_QUEUE_NAME1, true, false, false, null);
		channel.queueDeclare(Constants.FANOUT_QUEUE_NAME2, true, false, false, null);
		//4. 绑定队列和交换机
		/*
			queueBind(String queue, String exchange, String routingKey, 
			Map<String, Object> arguments)
			参数:
			1. queue: 队列名称
			2. exchange: 交换机名称
			3. routingKey: 路由key, 路由规则
			如果交换机类型为fanout,routingkey设置为"",表⽰每个消费者都可以收到全部信息
		*/
		
		channel.queueBind(Constants.FANOUT_QUEUE_NAME1,Constants.FANOUT_EXCHANGE_NAME, "");
		
		channel.queueBind(Constants.FANOUT_QUEUE_NAME2,Constants.FANOUT_EXCHANGE_NAME, "");
		//5. 发送消息
		/**
			* basicPublish(String exchange, String routingKey, 
			AMQP.BasicProperties props, byte[] body)
			* 参数说明:
			* Exchange: 交换机名称
			* routingKey: 如果交换机类型为fanout,routingkey设置为"",表⽰每个消费者都可以收到全部信息
		*/
		String msg = "hello fanout";
		 
		channel.basicPublish(Constants.FANOUT_EXCHANGE_NAME,"",null,msg.getBytes());
		
		//6.释放资源
		channel.close();
		connection.close();
	}
}

编写消费者代码

交换机和队列的绑定关系及声明已经在生产方写完, 所以消费者不需要再写了

去掉声明队列的代码就可以了

  1. 创建 Channel
  2. 接收消息, 并处理

完整代码

消费者1

java 复制代码
import com.rabbitmq.client.*;
import constant.Constants;

import java.io.IOException;

public class FanoutRabbitmqConsumer1 {

	public static void main(String[] args) throws Exception {
		//1. 创建channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		
		//2. 接收消息, 并消费
		DefaultConsumer consumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
				System.out.println("接收到消息: " + new String(body));
			}
		};
		channel.basicConsume(Constants.FANOUT_QUEUE_NAME1, true, consumer);
	}
}

消费者2 把队列名称改⼀下就可以了. 此处省略

运行程序, 观察结果

  1. 运行生产者

a) 可以看到两个队列分别有了⼀条消息

b) Exchange 多了队列绑定关系

  1. 运行消费者

消费者1

java 复制代码
接收到消息: hello fanout

消费者2

java 复制代码
接收到消息: hello fanout

2.4 Routing(路由模式)

队列和交换机的绑定, 不能是任意的绑定了, 而是要指定⼀个BindingKey(RoutingKey的⼀种)

消息的发送方在向 Exchange 发送消息时, 也需要指定消息的 RoutingKey

Exchange 也不再把消息交给每⼀个绑定的 key, 而是根据消息的 RoutingKey 进行判断, 只有队列绑定时的 BindingKey 和发送消息的 RoutingKey 完全⼀致, 才会接收到消息

接下来我们看看 Routing 模式的实现

步骤:

  1. 引入依赖
  2. 编写生产者代码
  3. 编写消费者代码

引入依赖

xml 复制代码
<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.7.3</version>
</dependency>

编写生产者代码

和发布订阅模式的区别是: 交换机类型不同, 绑定队列的 BindingKey 不同

创建交换机, 定义交换机类型为 BuiltinExchangeType.DIRECT

java 复制代码
channel.exchangeDeclare(Constants.DIRECT_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, false, null);

声明队列

java 复制代码
channel.queueDeclare(Constants.DIRECT_QUEUE_NAME1, true, false, false, null);
channel.queueDeclare(Constants.DIRECT_QUEUE_NAME2, true, false, false, null);

绑定交换机和队列

java 复制代码
//队列1绑定orange
channel.queueBind(Constants.DIRECT_QUEUE_NAME1,Constants.DIRECT_EXCHANGE_NAME, "orange");
//队列2绑定black, green
channel.queueBind(Constants.DIRECT_QUEUE_NAME2,Constants.DIRECT_EXCHANGE_NAME, "black");
channel.queueBind(Constants.DIRECT_QUEUE_NAME2,Constants.DIRECT_EXCHANGE_NAME, "green");;

发送消息

java 复制代码
//发送消息时, 指定RoutingKey
String msg = "hello direct, I am orange";
channel.basicPublish(Constants.DIRECT_EXCHANGE_NAME,"orange",null,msg.getBytes());
String msg_black = "hello direct,I am black";
channel.basicPublish(Constants.DIRECT_EXCHANGE_NAME,"black",null,msg_black.getBytes());

String msg_green= "hello direct, I am green";
channel.basicPublish(Constants.DIRECT_EXCHANGE_NAME,"green",null,msg_green.getBytes());

完整代码:

java 复制代码
public static String DIRECT_EXCHANGE_NAME = "test_direct";
public static String DIRECT_QUEUE_NAME1 = "direct_queue1";
public static String DIRECT_QUEUE_NAME2 = "direct_queue2";

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import constant.Constants;

public class DirectRabbitProducer {

	public static void main(String[] args) throws Exception {
		//1. 创建channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		//2. 创建交换机
		channel.exchangeDeclare(Constants.DIRECT_EXCHANGE_NAME, 
		BuiltinExchangeType.FANOUT, true, false, false, null);
		//3. 声明队列
		//如果没有⼀个这样的⼀个队列, 会⾃动创建, 如果有, 则不创建
		channel.queueDeclare(Constants.DIRECT_QUEUE_NAME1, true, false, false, null);
		channel.queueDeclare(Constants.DIRECT_QUEUE_NAME2, true, false, false, null);
		//4. 绑定队列和交换机
		//队列1绑定orange
		channel.queueBind(Constants.DIRECT_QUEUE_NAME1,Constants.DIRECT_EXCHANGE_NAME, "orange");
		//队列2绑定black, green
		
		channel.queueBind(Constants.DIRECT_QUEUE_NAME2,Constants.DIRECT_EXCHANGE_NAME, 
		"black");
		
		channel.queueBind(Constants.DIRECT_QUEUE_NAME2,Constants.DIRECT_EXCHANGE_NAME, "green");
		
		//5. 发送消息
		String msg = "hello direct, I am orange";
		
		channel.basicPublish(Constants.DIRECT_EXCHANGE_NAME,"orange",null,msg.getBytes());
		
		String msg_black = "hello direct,I am black";
		
		channel.basicPublish(Constants.DIRECT_EXCHANGE_NAME,"black",null,msg_black.getBytes());
		
		String msg_green= "hello direct, I am green";
		
		channel.basicPublish(Constants.DIRECT_EXCHANGE_NAME,"green",null,msg_green.getBytes());
		
		//6.释放资源
		channel.close();
		connection.close();
	}
}

编写消费者代码

Routing 模式的消费者代码和 Publish/Subscribe 代码⼀样, 同样复制出来两份

消费者1:DirectRabbitmqConsumer1

消费者2:DirectRabbitmqConsumer2

修改消费的队列名称就可以

完整代码:

java 复制代码
import com.rabbitmq.client.*;
import constant.Constants;

import java.io.IOException;

public class DirectRabbitmqConsumer1 {

	public static void main(String[] args) throws Exception {
		//1. 创建channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		
		//2. 接收消息, 并消费
		DefaultConsumer consumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
				System.out.println("接收到消息: " + new String(body));
			}
		};
		channel.basicConsume(Constants.DIRECT_QUEUE_NAME1, true, consumer);
	}
}

运行程序, 观察结果

  1. 运行生产者

a) 可以看到 direct_queue1 队列中, 路由了⼀条消息. direct_queue2 队列中, 路由了⼀条消息

b) exchange下队列和 Routing Key 的绑定关系

  1. 运行消费者

DirectRabbitmqConsumer1 :

java 复制代码
接收到消息: hello direct, I am orange

DirectRabbitmqConsumer2:

java 复制代码
接收到消息: hello direct,I am black
接收到消息: hello direct, I am green

2.5 Topics(通配符模式)

Topics 和 Routing 模式的区别是:

  1. topics 模式使用的交换机类型为 topic (Routing模式使用的交换机类型为direct)

  2. topic 类型的交换机在匹配规则上进行了扩展, Binding Key支持通配符匹配(direct 类型的交换机路由规则是 BindingKey 和 RoutingKey 完全匹配)

在 topic 类型的交换机在匹配规则上, 有些要求:

  1. RoutingKey 是⼀系列由点( . )分隔的单词, 比如 " stock.usd.nyse ", " nyse.vmw "," quick.orange.rabbit "
  2. BindingKey 和RoutingKey⼀样, 也是点( . )分割的字符串.
  3. Binding Key 中可以存在两种特殊字符串, 用于模糊匹配
    ◦ * 表示⼀个单词
    ◦ # 表示多个单词(0-N个)

比如:

  • Binding Key 为"d.a.b" 会同时路由到 Q1 和Q2
  • Binding Key 为"d.a.f" 会路由到 Q1
  • Binding Key 为"c.e.f" 会路由到 Q2
  • Binding Key 为"d.b.f" 会被丢弃, 或者返回给生产者(需要设置 mandatory 参数)

接下来我们看看 Routing 模式的实现

步骤:

  1. 引入依赖
  2. 编写生产者代码
  3. 编写消费者代码

引入依赖

xml 复制代码
<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.7.3</version>
</dependency>

编写生产者代码

和路由模式, 发布订阅模式的区别是: 交换机类型不同, 绑定队列的 RoutingKey 不同

创建交换机

定义交换机类型为BuiltinExchangeType.TOPIC

java 复制代码
channel.exchangeDeclare(Constants.TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true, false, false, null);

声明队列

java 复制代码
channel.queueDeclare(Constants.TOPIC_QUEUE_NAME1, true, false, false, null);
channel.queueDeclare(Constants.TOPIC_QUEUE_NAME2, true, false, false, null);

绑定交换机和队列

java 复制代码
//队列1绑定error, 仅接收error信息
channel.queueBind(Constants.TOPIC_QUEUE_NAME1,Constants.TOPIC_EXCHANGE_NAME, "*.error");
//队列2绑定info, error: error,info信息都接收
channel.queueBind(Constants.TOPIC_QUEUE_NAME2,Constants.TOPIC_EXCHANGE_NAME, "#.info");
channel.queueBind(Constants.TOPIC_QUEUE_NAME2,Constants.TOPIC_EXCHANGE_NAME, "*.error");

发送消息

java 复制代码
String msg = "hello topic, I'm order.error";
channel.basicPublish(Constants.TOPIC_EXCHANGE_NAME,"order.error",null,msg.getBytes());

String msg_black = "hello topic, I'm order.pay.info";
channel.basicPublish(Constants.TOPIC_EXCHANGE_NAME,"order.pay.info",null,msg_black.getBytes());

String msg_green= "hello topic, I'm pay.error";
channel.basicPublish(Constants.TOPIC_EXCHANGE_NAME,"pay.error",null,msg_green.getBytes());

完整代码:

java 复制代码
public static String TOPIC_EXCHANGE_NAME = "test_topic";
public static String TOPIC_QUEUE_NAME1 = "topic_queue1";
public static String TOPIC_QUEUE_NAME2 = "topic_queue2";

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import constant.Constants;

public class TopicRabbitProducer {

	public static void main(String[] args) throws Exception {
		//1. 创建channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		//2. 创建交换机
		channel.exchangeDeclare(Constants.TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true, false, false, null);
		//3. 声明队列
		//如果没有⼀个这样的⼀个队列, 会⾃动创建, 如果有, 则不创建
		channel.queueDeclare(Constants.TOPIC_QUEUE_NAME1, true, false, false, null);
		channel.queueDeclare(Constants.TOPIC_QUEUE_NAME2, true, false, false, null);
		//4. 绑定队列和交换机
		//队列1绑定error, 仅接收error信息
		
		channel.queueBind(Constants.TOPIC_QUEUE_NAME1,Constants.TOPIC_EXCHANGE_NAME, "*.error");
		//队列2绑定info, error: error,info信息都接收
		
		channel.queueBind(Constants.TOPIC_QUEUE_NAME2,Constants.TOPIC_EXCHANGE_NAME, "#.info");
		
		channel.queueBind(Constants.TOPIC_QUEUE_NAME2,Constants.TOPIC_EXCHANGE_NAME, "*.error");
		
		//5. 发送消息
		String msg = "hello topic, I'm order.error";
		
		channel.basicPublish(Constants.TOPIC_EXCHANGE_NAME,"order.error",null,msg.getBytes());
		
		String msg_black = "hello topic, I'm order.pay.info";
		
		channel.basicPublish(Constants.TOPIC_EXCHANGE_NAME,"order.pay.info",null,msg_black.getBytes());
		
		String msg_green= "hello topic, I'm pay.error";
		 
		channel.basicPublish(Constants.TOPIC_EXCHANGE_NAME,"pay.error",null,msg_green.getBytes());
		
		//6.释放资源
		channel.close();
		connection.close();
	}
}

编写消费者代码

Routing 模式的消费者代码和 Routing 模式代码⼀样, 修改消费的队列名称即可.

同样复制出来两份

消费者1:TopicRabbitmqConsumer1

消费者2: TopicRabbitmqConsumer2

完整代码:

java 复制代码
import com.rabbitmq.client.*;
import constant.Constants;

import java.io.IOException;

public class TopicRabbitmqConsumer1 {

	public static void main(String[] args) throws Exception {
		//1. 创建channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		
		//2. 接收消息, 并消费
		DefaultConsumer consumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
				System.out.println("接收到消息: " + new String(body));
			}
		};
		channel.basicConsume(Constants.TOPIC_QUEUE_NAME1, true, consumer);
	}
}

运行程序, 观察结果

  1. 运行生产者, 可以看到队列的消息数
  1. 运行消费者:

TopicRabbitmqConsumer1 :

java 复制代码
接收到消息: hello topic, I'm order.error
接收到消息: hello topic, I'm pay.error

TopicRabbitmqConsumer2:

java 复制代码
接收到消息: hello topic, I'm order.error
接收到消息: hello topic, I'm order.pay.info
接收到消息: hello topic, I'm pay.error

2.6 RPC(RPC通信)

RPC(Remote Procedure Call), 即远程过程调用. 它是⼀种通过网络从远程计算机上请求服务, 而不需要了解底层网络的技术. 类似于 Http 远程调用.

RabbitMQ 实现 RPC 通信的过程, 大概是通过两个队列实现⼀个可回调的过程.

大概流程如下:

  1. 客户端发送消息到⼀个指定的队列, 并在消息属性中设置 replyTo 字段, 这个字段指定了⼀个回调队列, 服务端处理后, 会把响应结果发送到这个队列.
  2. 服务端接收到请求后, 处理请求并发送响应消息到 replyTo 指定的回调队列
  3. 客户端在回调队列上等待响应消息. ⼀旦收到响应,客户端会检查消息的 correlationId 属性,以确保它是所期望的响应

接下来我们看看RPC模式的实现

步骤:

  1. 引入依赖
  2. 编写客户端
  3. 编写服务端

引入依赖

xml 复制代码
<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.7.3</version>
</dependency>

编写客户端代码

客户端代码主要流程如下:

  1. 声明两个队列, 包含回调队列 replyQueueName, 声明本次请求的唯⼀标志corrId
  2. 将 replyQueueName 和 corrId 配置到要发送的消息队列中
  3. 使用阻塞队列来阻塞当前进程, 监听回调队列中的消息, 把请求放到阻塞队列中
  4. 阻塞队列有消息后, 主线程被唤醒,打印返回内容

声明队列

java 复制代码
//2. 声明队列, 发送消息
channel.queueDeclare(Constants.RPC_REQUEST_QUEUE_NAME, true, false, false, null);

定义回调队列

java 复制代码
// 定义临时队列,并返回⽣成的队列名称
String replyQueueName = channel.queueDeclare().getQueue();

使用内置交换机发送消息

java 复制代码
// 本次请求唯⼀标志
String corrId = UUID.randomUUID().toString();
// ⽣成发送消息的属性
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
	.correlationId(corrId) // 唯⼀标志本次请求
	.replyTo(replyQueueName) // 设置回调队列
	.build();
// 通过内置交换机, 发送消息
String message = "hello rpc...";
channel.basicPublish("", Constants.RPC_REQUEST_QUEUE_NAME, props, 
message.getBytes());

使用阻塞队列, 来存储回调结果

java 复制代码
// 阻塞队列,⽤于存储回调结果
final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
//接收服务端的响应
DefaultConsumer consumer = new DefaultConsumer(channel) {
	@Override
	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
		System.out.println("接收到回调消息:"+ new String(body));
		//如果唯⼀标识正确, 放到阻塞队列中
		if (properties.getCorrelationId().equals(corrId)) {
			response.offer(new String(body, "UTF-8"));
		}
	}
};
channel.basicConsume(replyQueueName, true, consumer);

获取回调结果

java 复制代码
// 获取回调的结果
String result = response.take();
System.out.println(" [RPCClient] Result:" + result);

完整代码

java 复制代码
public static String RPC_REQUEST_QUEUE_NAME = "rpc_request_queue";

import com.rabbitmq.client.*;
import constant.Constants;

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class RPCClient {

	public static void main(String[] args) throws Exception {
		//1. 创建Channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		//2. 声明队列
		channel.queueDeclare(Constants.RPC_REQUEST_QUEUE_NAME, true, false, false, null);
		
		// 唯⼀标志本次请求
		String corrId = UUID.randomUUID().toString();
		// 定义临时队列,并返回⽣成的队列名称
		String replyQueueName = channel.queueDeclare().getQueue();
		// ⽣成发送消息的属性
		AMQP.BasicProperties props = new AMQP.BasicProperties
			.Builder()
			.correlationId(corrId) // 唯⼀标志本次请求
			.replyTo(replyQueueName) // 设置回调队列
			.build();
		// 通过内置交换机, 发送消息
		String message = "hello rpc...";
		channel.basicPublish("", Constants.RPC_REQUEST_QUEUE_NAME, props, 
		message.getBytes());
		
		// 阻塞队列,⽤于存储回调结果
		final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
		//接收服务端的响应
		DefaultConsumer consumer = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
				System.out.println("接收到回调消息:"+ new String(body));
				if (properties.getCorrelationId().equals(corrId)) {
					response.offer(new String(body, "UTF-8"));
				}
			}
		};
		channel.basicConsume(replyQueueName, true, consumer);
		// 获取回调的结果
		String result = response.take();
		System.out.println(" [RPCClient] Result:" + result);
		
		//释放资源
		channel.close();
		connection.close();
	}
}

编写服务端代码

服务端代码主要流程如下:

  1. 接收消息
  2. 根据消息内容进行响应处理, 把应答结果返回到回调队列中

声明队列

java 复制代码
//2. 声明队列, 发送消息
channel.queueDeclare(Constants.RPC_REQUEST_QUEUE_NAME, true, false, false, null);

设置同时最多只能获取⼀个消息

如果不设置 basicQos, RabbitMQ 会使用默认的 QoS 设置, 其 prefetchCount 默认值为 0. 当 prefetchCount 为 0 时,RabbitMQ 会根据内部实现和当前的网络状况等因素,可能会同时发送多条消息给消费者. 这意味着在默认情况下,消费者可能会同时接收到多条消息, 但具体数量不是严格保证的, 可能会有所波动(后面会讲)

在 RPC 模式下,通常期望的是⼀对⼀的消息处理, 即⼀个请求对应⼀个响应. 消费者在处理完⼀个消息并确认之后,才会接收到下⼀条消息.

java 复制代码
// 设置同时最多只能获取⼀个消息
channel.basicQos(1);
System.out.println("Awaiting RPC request");

接收消息, 并做出相应处理

java 复制代码
Consumer consumer = new DefaultConsumer(channel){
	@Override
	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
		AMQP.BasicProperties replyProps = new AMQP.BasicProperties
			.Builder()
			.correlationId(properties.getCorrelationId())
			.build();
		// ⽣成返回
		String message = new String(body);
		String response = "request:"+ message + ", response: 处理成功";
		// 回复消息,通知已经收到请求
		channel.basicPublish( "", properties.getReplyTo(), replyProps, response.getBytes());
		// 对消息进⾏应答
		channel.basicAck(envelope.getDeliveryTag(), false);
	}
};
channel.basicConsume(Constants.RPC_REQUEST_QUEUE_NAME, false, consumer);

RabbitMQ 消息确定机制

在 RabbitMQ中,basicConsume 方法的 autoAck 参数⽤于指定消费者是否应该自动向消息队列确认消息

自动确认(autoAck=true): 消息队列在将消息发送给消费者后, 会立即从内存中删除该消息. 这意味着, 如果消费者处理消息失败,消息将丢失,因为消息队列认为消息已经被成功消费

手动确认(autoAck=false): 消息队列在将消息发送给消费者后,需要消费者显式地调用 basicAck 方法来确认消息. 手动确认提供了更高的可靠性, 确保消息不会被意外丢失, 适用于消息处理重要且需要确保每个消息都被正确处理的场景

完整代码

java 复制代码
import com.rabbitmq.client.*;
import constant.Constants;

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

public class RPCServer {
	public static void main(String[] args) throws IOException, TimeoutException {
		//1. 创建Channel通道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		//2. 声明队列
		channel.queueDeclare(Constants.RPC_REQUEST_QUEUE_NAME, true, false, false, null);
		//3. 接收消息, 并消费
		// 设置同时最多只能获取⼀个消息
		channel.basicQos(1);
		System.out.println("Awaiting RPC request");
		Consumer consumer = new DefaultConsumer(channel){
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
				AMQP.BasicProperties replyProps = new AMQP.BasicProperties
					.Builder()
					.correlationId(properties.getCorrelationId())
					.build();
				// ⽣成返回
				String message = new String(body);
				String response = "request:"+ message + ", response: 处理成功";
				// 回复消息,通知已经收到请求
				channel.basicPublish( "", properties.getReplyTo(), replyProps, 
				response.getBytes());
				// 对消息进⾏应答
				channel.basicAck(envelope.getDeliveryTag(), false);
			}
		};
		channel.basicConsume(Constants.RPC_REQUEST_QUEUE_NAME, false, consumer);
	}
}

运行程序, 观察结果

运行客户端:

amq.gen-iijHJbGabr7E2aK1KQV3Nw 就是回调队列

运行服务端, 观察客户端日志

java 复制代码
接收到回调消息:request:hello rpc..., response: 处理成功
[RPCClient] Result:request:hello rpc..., response: 处理成功

2.7 Publisher Confirms(发布确认)

作为消息中间件, 都会面临消息丢失的问题

消息丢失大概分为三种情况:

  1. 生产者问题. 因为应用程序故障, 网络抖动等各种原因, 生产者没有成功向 broker 发送消息.
  2. 消息中间件自身问题. 生产者成功发送给了 Broker, 但是 Broker 没有把消息保存好, 导致消息丢失.
  3. 消费者问题. Broker 发送消息到消费者, 消费者在消费消息时, 因为没有处理好, 导致 broker 将消费失败的消息从队列中删除了

RabbitMQ 也对上述问题给出了相应的解决方案. 问题2可以通过持久化机制. 问题3可以采用消息应答机制. (后面详细讲)

针对问题1, 可以采用发布确认(Publisher Confirms)机制实现.

发布确认 属于 RabbitMQ 的七大工作模式之⼀.

生产者将信道设置成 confirm (确认)模式, ⼀旦信道进入 confirm 模式, 所有在该信道上面发布的消息都会被指派⼀个唯⼀的ID(从1开始), ⼀旦消息被投递到所有匹配的队列之后, RabbitMQ 就会发送⼀个确认给生产者(包含消息的唯⼀ID), 这就使得生产者知道消息已经正确到达目的队列了, 如果消息和队列是可持久化的, 那么确认消息会在将消息写入磁盘之后发出. broker 回传给生产者的确认消息中

deliveryTag 包含了确认消息的序号, 此外 broker 也可以设置channel.basicAck⽅法中的multiple参数, 表示到这个序号之前的所有消息都已经得到了处理

发送⽅确认机制最大的好处在于它是异步的, 生产者可以同时发布消息和等待信道返回确认消息.

  1. 当消息最终得到确认之后, ⽣产者可以通过回调方法来处理该确认消息.
  2. 如果 RabbitMQ 因为自身内部错误导致消息丢失, 就会发送⼀条nack(Basic.Nack)命令, 生产者同样可以在回调方法中处理该 nack 命令.

使用发送确认机制, 必须要信道设置成 confirm(确认)模式

发布确认是 AMQP 0.9.1 协议的扩展, 默认情况下它不会被启用. 生产者通过 channel.confirmSelect() 将信道设置为 confirm 模式.

java 复制代码
Channel channel = connection.createChannel();
channel.confirmSelect();

发布确认有3种策略, 接下来我们来学习这三种策略

2.7.1 Publishing Messages Individually(单独确认)

代码示例:

java 复制代码
static void publishMessagesIndividually() throws Exception {
	try (Connection connection = createConnection()) {
		//创建channel
		Channel ch = connection.createChannel();
		//开启信道确认模式
		ch.confirmSelect();
		//声明队列
		ch.queueDeclare(PUBLISHER_CONFIRMS_QUEUE_NAME1, true, false, true, null);
		
		long start = System.currentTimeMillis();
		//循环发送消息
		for (int i = 0; i < MESSAGE_COUNT; i++) {
			String body = "消息"+ i;
			//发布消息
			ch.basicPublish("", PUBLISHER_CONFIRMS_QUEUE_NAME1, null, body.getBytes());
			//等待确认消息.只要消息被确认,这个⽅法就会被返回
			//如果超时过期, 则抛出TimeoutException。如果任何消息被nack(丢失), 
			waitForConfirmsOrDie将抛出IOException。
			ch.waitForConfirmsOrDie(5_000);
		}
		long end = System.currentTimeMillis();
		System.out.format("Published %d messages individually in %d ms", MESSAGE_COUNT, end - start);
	}
}

观察运行结果:

java 复制代码
Published 200 messages individually in 6265 ms

可以发现, 发送200条消息, 耗时很长.

观察上面代码, 会发现这种策略是每发送⼀条消息后就调用 channel.waitForConfirmsOrDie 方法,之后等待服务端的确认, 这实际上是⼀种串行同步等待的方式. 尤其对于持久化的消息来说, 需要等待消息确认存储在磁盘之后才会返回(调用 Linux 内核的 fsync 方法).

但是发布确认机制是支持异步的. 可以⼀边发送消息, ⼀边等待消息确认.

由此进行了改进, 接下来看另外两种策略:

  • Publishing Messages in Batches(批量确认) : 每发送⼀批消息后,调用 channel.waitForConfirms 方法, 等待服务器的确认返回.
  • Handling Publisher Confirms Asynchronously(异步确认): 提供⼀个回调方法,服务端确认了⼀条或者多条消息后客户端会回这个方法进行处理

2.7.2 Publishing Messages in Batches(批量确认)

代码示例:

java 复制代码
static void publishMessagesInBatch() throws Exception {
	try (Connection connection = createConnection()) {
		//创建信道
		Channel ch = connection.createChannel();
		//信道设置为confirm模式
		ch.confirmSelect();
		//声明队列
		ch.queueDeclare(PUBLISHER_CONFIRMS_QUEUE_NAME2, true, false, true, null);
		
		int batchSize = 100;
		int outstandingMessageCount = 0;
		
		long start = System.currentTimeMillis();
		for (int i = 0; i < MESSAGE_COUNT; i++) {
			String body = "消息"+ i;
			//发送消息
			ch.basicPublish("", PUBLISHER_CONFIRMS_QUEUE_NAME2, null, body.getBytes());
			outstandingMessageCount++;
			//批量确认消息
			if (outstandingMessageCount == batchSize) {
				ch.waitForConfirmsOrDie(5_000);
				outstandingMessageCount = 0;
			}
		}
		//消息发送完, 还有未确认的消息, 进⾏确认
		if (outstandingMessageCount > 0) {
			ch.waitForConfirmsOrDie(5_000);
		}
		long end = System.currentTimeMillis();
		System.out.format("Published %d messages in batch in %d ms", MESSAGE_COUNT, end - start);
	}
}

观察运行结果:

java 复制代码
Published 200 messages in batch in 128 ms

可以观察到, 性能提⾼了很多.

相比于单独确认策略, 批量确认极大地提升了 confirm 的效率, 缺点是出现Basic.Nack 或者超时时, 我们不清楚具体哪条消息出了问题. 客户端需要将这⼀批次的消息全部重发, 这会带来明显的重复消息数量.当消息经常丢失时,批量确认的性能应该是不升反降的.

2.7.3 Handling Publisher Confirms Asynchronously(异步确认)

异步 confirm 方法的编程实现最为复杂. Channel 接口提供了⼀个方法 addConfirmListener. 这个方法可以添加 ConfirmListener 回调接口.

ConfirmListener 接口中包含两个方法 : handleAck(long deliveryTag, boolean

multiple) 和 handleNack(long deliveryTag, boolean multiple) , 分别对应处理

RabbitMQ 发送给生产者的 ack 和 nack.

deliveryTag 表示发送消息的序号. multiple 表示是否批量确认.

我们需要为每⼀个 Channel 维护⼀个已发送消息的序号集合. 当收到 RabbitMQ 的 confirm 回调时, 从集合中删除对应的消息. 当 Channel 开启 confirm 模式后, channel 上发送消息都会附带⼀个从1开始递增的 deliveryTag 序号. 我们可以使用 SortedSet 的有序性来维护这个已发消息的集合.

  1. 当收到 ack 时, 从序列中删除该消息的序号. 如果为批量确认消息, 表示小于等于当前序号 deliveryTag 的消息都收到了, 则清除对应集合
  2. 当收到 nack 时, 处理逻辑类似, 不过需要结合具体的业务情况, 进行消息重发等操作

代码示例:

java 复制代码
static void handlePublishConfirmsAsynchronously() throws Exception {
	try (Connection connection = createConnection()) {
		Channel ch = connection.createChannel();
		
		ch.queueDeclare(PUBLISHER_CONFIRMS_QUEUE_NAME3, false, false, true, null);
		
		ch.confirmSelect();
		
		//有序集合,元素按照⾃然顺序进⾏排序,存储未confirm消息序号
		SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<>());
		
		ch.addConfirmListener(new ConfirmListener() {
			@Override
			public void handleAck(long deliveryTag, boolean multiple) throws IOException {
				//System.out.println("ack, SeqNo: " + deliveryTag + 
				",multiple:" + multiple);
				//multiple 批量
				//confirmSet.headSet(n)⽅法返回当前集合中⼩于n的集合
				if (multiple) {
					//批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰
					这批序号的消息都已经被ack了
					confirmSet.headSet(deliveryTag+1).clear();
				} else {
					//单条确认:将当前的deliveryTag从集合中移除
					confirmSet.remove(deliveryTag);
			}
		}
		
		@Override
		public void handleNack(long deliveryTag, boolean multiple) throws IOException {
				System.err.format("deliveryTag: %d, multiple: %b%n", deliveryTag, multiple);
				if (multiple) {
					//批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
					confirmSet.headSet(deliveryTag+1).clear();
				} else {
					//单条确认:将当前的deliveryTag从集合中移除
					confirmSet.remove(deliveryTag);
				}
				//如果处理失败, 这⾥需要添加处理消息重发的场景. 此处代码省略
				
			}
		});
		
		//循环发送消息
		long start = System.currentTimeMillis();
		for (int i = 0; i < MESSAGE_COUNT; i++) {
			String message = "消息" + i;
			//得到下次发送消息的序号, 从1开始
			long nextPublishSeqNo = ch.getNextPublishSeqNo();
			//System.out.println("消息序号:"+ nextPublishSeqNo);
			ch.basicPublish("", PUBLISHER_CONFIRMS_QUEUE_NAME3, null, message.getBytes());
			//将序号存⼊集合中
			confirmSet.add(nextPublishSeqNo);
		}
		
		//消息确认完毕
		while (!confirmSet.isEmpty()){
			Thread.sleep(10);
		}
		
		long end = System.currentTimeMillis();
		System.out.format("Published %d messages and handled confirms 
		asynchronously in %d ms%n", MESSAGE_COUNT, end - start);
	}
}

观察运行结果:

java 复制代码
Published 200 messages and handled confirms asynchronously in 92 ms

三种策略对比

完整代码:

java 复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import constant.Constants;

import java.io.IOException;
import java.util.Collections;
import java.util.SortedSet;
import java.util.TreeSet;

public class PublisherConfirms {

	private static final int MESSAGE_COUNT = 500;
	private static final String PUBLISHER_CONFIRMS_QUEUE_NAME1 = "publisher_confirms_queue1";
	private static final String PUBLISHER_CONFIRMS_QUEUE_NAME2 = "publisher_confirms_queue2";
	private static final String PUBLISHER_CONFIRMS_QUEUE_NAME3 = "publisher_confirms_queue3";
	
	static Connection createConnection() throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Constants.HOST);//ip 默认值localhost
		factory.setPort(Constants.PORT); //默认值5672
		factory.setVirtualHost(Constants.VIRTUAL_HOST);//虚拟机名称, 默认 /
		
		factory.setUsername(Constants.USER_NAME);//⽤⼾名,默认guest
		factory.setPassword(Constants.PASSWORD);//密码, 默认guest
		return factory.newConnection();
	}
	
	public static void main(String[] args) throws Exception {
		publishMessagesIndividually();
		publishMessagesInBatch();
		handlePublishConfirmsAsynchronously();
	}
	
	static void publishMessagesIndividually() throws Exception {
		try (Connection connection = createConnection()) {
			//创建channel
			Channel ch = connection.createChannel();
			//开启信道确认模式
			ch.confirmSelect();
			//声明队列
			ch.queueDeclare(PUBLISHER_CONFIRMS_QUEUE_NAME1, true, false, true, null);
			
			long start = System.currentTimeMillis();
			//循环发送消息
			for (int i = 0; i < MESSAGE_COUNT; i++) {
				String body = "消息" + i;
				//发布消息
				ch.basicPublish("", PUBLISHER_CONFIRMS_QUEUE_NAME1, null, body.getBytes());
				//等待确认消息.只要消息被确认,这个⽅法就会被返回
				//如果超时过期, 则抛出TimeoutException. 如果任何消息被nack(丢失), 
				waitForConfirmsOrDie将抛出IOException。
				ch.waitForConfirmsOrDie(5_000);
			}
			long end = System.currentTimeMillis();
			System.out.format("Published %d messages individually in %d ms%n", MESSAGE_COUNT, end - start);
		}
	}
	
	static void publishMessagesInBatch() throws Exception {
		try (Connection connection = createConnection()) {
			//创建信道
			Channel ch = connection.createChannel();
			//信道设置为confirm模式
			ch.confirmSelect();
			//声明队列
			ch.queueDeclare(PUBLISHER_CONFIRMS_QUEUE_NAME2, true, false, true, null);
			
			int batchSize = 100;
			int outstandingMessageCount = 0;
			
			long start = System.currentTimeMillis();
			for (int i = 0; i < MESSAGE_COUNT; i++) {
				String body = "消息" + i;
				//发送消息
				ch.basicPublish("", PUBLISHER_CONFIRMS_QUEUE_NAME2, null, body.getBytes());
				outstandingMessageCount++;
				//批量确认消息
				if (outstandingMessageCount == batchSize) {
					ch.waitForConfirmsOrDie(5_000);
					outstandingMessageCount = 0;
				}
			}
			//消息发送完, 还有未确认的消息, 进⾏确认
			if (outstandingMessageCount > 0) {
				ch.waitForConfirmsOrDie(5_000);
			}
			long end = System.currentTimeMillis();
			System.out.format("Published %d messages in batch in %d ms%n", MESSAGE_COUNT, end - start);
		}
	}
	
	static void handlePublishConfirmsAsynchronously() throws Exception {
		try (Connection connection = createConnection()) {
			Channel ch = connection.createChannel();
			
			ch.queueDeclare(PUBLISHER_CONFIRMS_QUEUE_NAME3, false, false, true, null);
			
			ch.confirmSelect();
			
			//有序集合,元素按照⾃然顺序进⾏排序,存储未confirm消息序号
			SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<>());
			
			ch.addConfirmListener(new ConfirmListener() {
				@Override
				public void handleAck(long deliveryTag, boolean multiple) throws IOException {
				// System.out.println("ack, SeqNo: " + deliveryTag + ",multiple:" + multiple);
				//multiple 批量
				//confirmSet.headSet(n)⽅法返回当前集合中⼩于n的集合
				if (multiple) {
					//批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
					confirmSet.headSet(deliveryTag+1).clear();
				} else {
					//单条确认:将当前的deliveryTag从集合中移除
					confirmSet.remove(deliveryTag);
				}
			}
			
				@Override
				public void handleNack(long deliveryTag, boolean multiple) throws IOException {
					System.err.format("deliveryTag: %d, multiple: %b%n", 
					deliveryTag, multiple);
					if (multiple) {
						//批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
						confirmSet.headSet(deliveryTag+1).clear();
					} else {
						//单条确认:将当前的deliveryTag从集合中移除
						confirmSet.remove(deliveryTag);
					}
					//如果处理失败, 这⾥需要添加处理消息重发的场景. 此处代码省略
				}
			});
			
			//循环发送消息
			long start = System.currentTimeMillis();
			for (int i = 0; i < MESSAGE_COUNT; i++) {
				String message = "消息" + i;
				//得到下次发送消息的序号, 从1开始
				long nextPublishSeqNo = ch.getNextPublishSeqNo();
				// System.out.println("消息序号:"+ nextPublishSeqNo);
				ch.basicPublish("", PUBLISHER_CONFIRMS_QUEUE_NAME3, null, message.getBytes());
				//将序号存⼊集合中
				confirmSet.add(nextPublishSeqNo);
			}
			
			//消息确认完毕
			while (!confirmSet.isEmpty()){
				Thread.sleep(10);
			}
			
			long end = System.currentTimeMillis();
			System.out.format("Published %d messages and handled confirms asynchronously in %d ms%n", MESSAGE_COUNT, end - start);
		}
	}
}

消息数越多, 异步确认的优势越明显

200条消息的结果对比:

java 复制代码
Published 200 messages individually in 6931 ms
Published 200 messages in batch in 137 ms
Published 200 messages and handled confirms asynchronously in 73 ms

500条消息结果对比

java 复制代码
Published 500 messages individually in 15805 ms
Published 500 messages in batch in 246 ms
Published 500 messages and handled confirms asynchronously in 107 ms

三. Spring Boot 整合 RabbitMQ

对于 RabbitMQ 开发, Spring 也提供了⼀些便利. Spring 和 RabbitMQ 的官方文档对此均有介绍

Spring官方:点击跳转

RabbitMQ 官方:点击跳转

下面来看如何基于SpringBoot 进行 RabbitMQ 的开发

3.1 工作队列模式

步骤:

  1. 引入依赖
  2. 编写 yml 配置,基本信息配置
  3. 编写生产者代码
  4. 编写消费者代码

a. 定义监听类, 使用 @RabbitListener 注解完成队列监听

  1. 运行观察结果

引入依赖

xml 复制代码
<!--Spring MVC相关依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<!--RabbitMQ相关依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

也可以通过创建项目时, 加入依赖

添加配置

yaml 复制代码
#配置RabbitMQ的基本信息
spring:
  rabbitmq:
	host: 110.41.51.65
	port: 15673 #默认为5672
	username: study
	password: study
	virtual-host: bite #默认值为 /

或以下配置

yaml 复制代码
#amqp://username:password@Ip:port/virtual-host
spring:
  rabbitmq:
	addresses: amqp://study:study@110.41.51.65:15673/bite

编写生产者代码

为方便测试, 我们通过接口来发送消息

java 复制代码
//work模式队列名称
public static final String WORK_QUEUE = "work_queue";

声明队列

java 复制代码
import com.bite.rabbitmq.constant.Constants;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class RabbitMQConfig {
	//1. ⼯作模式队列
	@Bean("workQueue")
	public Queue workQueue() {
		return QueueBuilder.durable(Constants.WORK_QUEUE).build();
	}
}
@RequestMapping("/producer")
@RestController
public class ProducerController {
	@Autowired
	private RabbitTemplate rabbitTemplate;
	
	@RequestMapping("/work")
	public String work(){
		for (int i = 0; i < 10; i++) {
			//使⽤内置交换机发送消息, routingKey和队列名称保持⼀致
			rabbitTemplate.convertAndSend("", Constants.WORK_QUEUE, "hello spring amqp: work...");
		}
		return "发送成功";
	}
}

编写消费者代码

定义监听类

java 复制代码
import com.bite.rabbitmq.constant.Constants;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class WorkListener {
	@RabbitListener(queues = Constants.WORK_QUEUE)
	public void listenerQueue(Message message){
		System.out.println("listener 1["+Constants.WORK_QUEUE+"]收到消息:" + message);
	}
		
	@RabbitListener(queues = Constants.WORK_QUEUE)
	public void listenerQueue2(Message message){
		System.out.println("listener 2["+Constants.WORK_QUEUE+"]收到消息:" + message);
	}
}

@RabbitListener 是 Spring 框架中用于监听 RabbitMQ 队列的注解, 通过使用这个注解,可以定义⼀个方法, 以便从 RabbitMQ 队列中接收消息. 该注解支持多种参数类型,这些参数类型代表了从 RabbitMQ 接收到的消息和相关信息.

以下是⼀些常用的参数类型:

  1. String :返回消息的内容
  2. Message ( org.springframework.amqp.core.Message ): Spring AMQP的Message 类,返回原始的消息体以及消息的属性, 如消息ID, 内容, 队列信息等.
  3. Channel ( com.rabbitmq.client.Channel ):RabbitMQ 的通道对象, 可以用于进行更高级的操作,如手动确认消息.

运行程序, 观察结果

⽣产者测试(测试时需要把监听先注掉, 不然会立马被消费掉)

点进去可以看到消息的内容

消费者测试, 打印消息内容

java 复制代码
1 listener 1[work_queue]接收到消息:(Body:'hello spring amqp: work...'MessageProperties [headers={}, contentType=text/plain,contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=true, receivedExchange=, receivedRoutingKey=work_queue, deliveryTag=1, consumerTag=amq.ctag-pNYVOTLX7zeFmnD7sL9NXw, consumerQueue=work_queue])
2 listener 2[work_queue]接收到消息:(Body:'hello spring amqp: work...'MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0,receivedDeliveryMode=PERSISTENT, priority=0, redelivered=true, receivedExchange=, receivedRoutingKey=work_queue, deliveryTag=1, consumerTag=amq.ctag-ghlFZQoXLpDBD_BXedIBdQ, consumerQueue=work_queue])
3 ...

3.2 Publish/Subscribe(发布订阅模式)

在发布/订阅模型中,多了⼀个 Exchange 角色.

Exchange 常见有三种类型, 分别代表不同的路由规则

a) Fanout:广播,将消息交给所有绑定到交换机的队列 (Publish/Subscribe模式)

b) Direct:定向,把消息交给符合指定 routing key 的队列 (Routing模式)

c) Topic:通配符,把消息交给符合 routing pattern (路由模式) 的队列 (Topics模式)

步骤:

  1. 引入依赖
  2. 编写生产者代码
  3. 编写消费者代码

引入依赖

xml 复制代码
<!--Spring MVC相关依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<!--RabbitMQ相关依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

编写生产者代码

和简单模式的区别是:需要创建交换机, 并且绑定队列和交换机

声明队列, 交换机, 绑定队列和交换机

java 复制代码
//发布/订阅模式
public static final String FANOUT_QUEUE1 = "fanout_queue1";
public static final String FANOUT_QUEUE2 = "fanout_queue2";
public static final String FANOUT_EXCHANGE_NAME ="fanout_exchange";
//2. 发布订阅模式
//声明2个队列, 观察是否两个队列都收到了消息
@Bean("fanoutQueue1")
public Queue fanoutQueue1() {
	return QueueBuilder.durable(Constants.FANOUT_QUEUE1).build();
}
@Bean("fanoutQueue2")
public Queue fanoutQueue2() {
	return QueueBuilder.durable(Constants.FANOUT_QUEUE2).build();
}
//声明交换机
@Bean("fanoutExchange")
public FanoutExchange fanoutExchange() {
	return ExchangeBuilder.fanoutExchange(Constants.FANOUT_EXCHANGE_NAME).durable(true).build();
}
//队列和交换机绑定
@Bean
public Binding fanoutBinding(@Qualifier("fanoutExchange") FanoutExchange exchange, @Qualifier("fanoutQueue1") Queue queue) {
	return BindingBuilder.bind(queue).to(exchange);
}
@Bean
public Binding fanoutBinding2(@Qualifier("fanoutExchange") FanoutExchange exchange, @Qualifier("fanoutQueue2") Queue queue) {
	return BindingBuilder.bind(queue).to(exchange);
}

使用接口发送消息

java 复制代码
@RequestMapping("/fanout")
public String fanoutProduct(){
	//routingKey为空, 表⽰所有队列都可以收到消息
	rabbitTemplate.convertAndSend(Constants.FANOUT_EXCHANGE_NAME,"","hello spring boot: fanout");
	return "发送成功";
}

编写消费者代码

交换机和队列的绑定关系及声明已经在生产方写完, 所以消费者不需要再写了

定义监听类, 处理接收到的消息即可

java 复制代码
import com.bite.rabbitmq.constant.Constants;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class FanoutListener {
	//指定监听队列的名称
	@RabbitListener(queues = Constants.FANOUT_QUEUE1)
	public void ListenerQueue(String message){
		System.out.println("["+Constants.FANOUT_QUEUE1+ "]接收到消息:"+message);
	}
	
	@RabbitListener(queues = Constants.FANOUT_QUEUE2)
	public void ListenerQueue2(String message){
		System.out.println("["+Constants.FANOUT_QUEUE2+ "]接收到消息:"+message);
	}
}

运行程序, 观察结果

  1. 运行项目, 调用接口发送消息:http://127.0.0.1:8080/producer/fanout

也可以把监听类注释掉, 观察两个队列的信息

  1. 监听类收到消息, 并打印
java 复制代码
[fanout_queue1]接收到消息:hello spring boot: fanout
[fanout_queue2]接收到消息:hello spring boot: fanout

3.3 Routing (路由模式)

交换机类型为 Direct 时, 会把消息交给符合指定 routing key 的队列.

队列和交换机的绑定, 不是任意的绑定了, 而是要指定⼀个 RoutingKey (路由key)

消息的发送方在向 Exchange 发送消息时, 也需要指定消息的 RoutingKey

Exchange 也不再把消息交给每⼀个绑定的 key, 而是根据消息的 RoutingKey 进行判断, 只有队列的 RoutingKey 和消息的 RoutingKey 完全⼀致, 才会接收到消息

步骤:

  1. 引入依赖(同上)
  2. 编写生产者代码
  3. 编写消费者代码

编写生产者代码

和发布订阅模式的区别就是: 交换机类型不同, 绑定队列的 RoutingKey 不同

声明队列, 交换机, 绑定队列和交换机

java 复制代码
//routing模式
public static final String DIRECT_QUEUE1 = "direct_queue1";
public static final String DIRECT_QUEUE2 = "direct_queue2";
public static final String DIRECT_EXCHANGE_NAME = "direct_exchange";
//Routing模式
@Bean("directQueue1")
public Queue routingQueue1() {
	return QueueBuilder.durable(Constants.DIRECT_QUEUE1).build();
}
@Bean("directQueue2")
public Queue routingQueue2() {
	return QueueBuilder.durable(Constants.DIRECT_QUEUE2).build();
}
//声明交换机
@Bean("directExchange")
public DirectExchange directExchange() {
	return ExchangeBuilder.directExchange(Constants.DIRECT_EXCHANGE_NAME).durable(true).build();
}
//队列和交换机绑定
//队列1绑定orange
@Bean
public Binding directBinding(@Qualifier("directExchange") DirectExchange exchange, @Qualifier("directQueue1") Queue queue) {
	return BindingBuilder.bind(queue).to(exchange).with("orange");
}
//队列2绑定black, green
@Bean
public Binding directBinding2(@Qualifier("directExchange") DirectExchange exchange, @Qualifier("directQueue2") Queue queue) {
	return BindingBuilder.bind(queue).to(exchange).with("black");
}
@Bean
public Binding directBinding3(@Qualifier("directExchange") DirectExchange exchange, @Qualifier("directQueue2") Queue queue) {
	return BindingBuilder.bind(queue).to(exchange).with("green");
}

使用接口发送消息

java 复制代码
@RequestMapping("/direct")
public String directProduct(String routingKey){
	//routingKey作为参数传递
	rabbitTemplate.convertAndSend(Constants.DIRECT_EXCHANGE_NAME, routingKey,"hello spring boot: direct "+routingKey);
	return "发送成功";
}

编写消费者代码

交换机和队列的绑定关系及声明已经在生产方写完, 所以消费者不需要再写了

定义监听类, 处理接收到的消息即可

java 复制代码
import com.bite.rabbitmq.constant.Constants;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class DirectListener {
	//指定监听队列的名称
	@RabbitListener(queues = Constants.DIRECT_QUEUE1)
	public void ListenerQueue(String message){
		System.out.println("["+Constants.DIRECT_QUEUE1+ "]接收到消息:"+ message);
	}
	
	@RabbitListener(queues = Constants.DIRECT_QUEUE2)
	public void ListenerQueue2(String message){
		System.out.println("["+Constants.DIRECT_QUEUE2+ "]接收到消息:"+ message);
	}
}

运行程序, 观察结果

  1. 运行项目
  2. 调用接口发送 routingkey 为 orange 的消息:http://127.0.0.1:8080/producer/direct?routingKey=orange

观察后端日志, 队列1收到消息

java 复制代码
[direct_queue1]接收到消息:hello spring boot: direct orange
  1. 调用接口发送 routingkey 为 black 的消息:http://127.0.0.1:8080/producer/direct?routingKey=black

观察后端日志, 队列2收到消息

java 复制代码
[direct_queue2]接收到消息:hello spring boot: direct black
  1. 调用接口发送 routingkey 为 green 的消息:http://127.0.0.1:8080/producer/direct?routingKey=green

观察后端日志, 队列2收到消息

java 复制代码
[direct_queue2]接收到消息:hello spring boot: direct green

3.4 Topics(通配符模式)

Topics 和 Routing 模式的区别是:

  1. topics 模式使用的交换机类型为 topic(Routing模式使用的交换机类型为direct)

  2. topic 类型的交换机在匹配规则上进行了扩展, Binding Key 支持通配符匹配

步骤:

  1. 引入依赖(同上)
  2. 编写生产者代码
  3. 编写消费者代码

编写生产者代码

和发布订阅模式的区别就是: 交换机类型不同, 绑定队列的 RoutingKey 不同

声明队列, 交换机, 绑定队列和交换机

java 复制代码
//topics模式
public static final String TOPICS_QUEUE1 = "topics_queue1";
public static final String TOPICS_QUEUE2 = "topics_queue2";
public static final String TOPICS_EXCHANGE_NAME = "topics_exchange";
//topic模式
@Bean("topicsQueue1")
public Queue topicsQueue1() {
	return QueueBuilder.durable(Constants.TOPICS_QUEUE1).build();
}
@Bean("topicsQueue2")
public Queue topicsQueue2() {
	return QueueBuilder.durable(Constants.TOPICS_QUEUE2).build();
}
//声明交换机
@Bean("topicExchange")
public TopicExchange topicExchange() {
	returnExchangeBuilder.topicExchange(Constants.TOPICS_EXCHANGE_NAME).durable(true).build();
}
//队列和交换机绑定
//队列1绑定error, 仅接收error信息
@Bean
public Binding topicBinding(@Qualifier("topicExchange") TopicExchange exchange, @Qualifier("topicsQueue1") Queue queue) {
	return BindingBuilder.bind(queue).to(exchange).with("*.error");
}
//队列2绑定info, error: error,info信息都接收
@Bean
public Binding topicBinding2(@Qualifier("topicExchange") TopicExchange exchange, @Qualifier("topicsQueue2") Queue queue) {
	return BindingBuilder.bind(queue).to(exchange).with("#.info");
}
@Bean
public Binding topicBinding3(@Qualifier("topicExchange") TopicExchange exchange, @Qualifier("topicsQueue2") Queue queue) {
	return BindingBuilder.bind(queue).to(exchange).with("*.error");
}

使用接口发送消息

java 复制代码
@RequestMapping("/topics")
public String topicProduct(String routingKey){
	//routingKey为空, 表⽰所有队列都可以收到消息
	rabbitTemplate.convertAndSend(Constants.TOPICS_EXCHANGE_NAME, 
	routingKey,"hello spring boot: topics "+routingKey);
	return "发送成功";
}

编写消费者代码

定义监听类, 处理接收到的消息.

java 复制代码
import com.bite.rabbitmq.constant.Constants;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class TopicListener {
	//指定监听队列的名称
	@RabbitListener(queues = Constants.TOPICS_QUEUE1)
	public void ListenerQueue(String message){
		System.out.println("["+Constants.TOPICS_QUEUE1+ "]接收到消息:"+message);
	}
	
	@RabbitListener(queues = Constants.TOPICS_QUEUE2)
	public void ListenerQueue2(String message){
		System.out.println("["+Constants.TOPICS_QUEUE2+ "]接收到消息:"+message);
	}
}

运行程序, 观察结果

  1. 运行项目
  2. 调用接口发送 routingkey 为 order.error 的消息:http://127.0.0.1:8080/producer/topics?routingKey=order.error

观察后端日志, 队列1和队列2均收到消息

java 复制代码
[topics_queue2]接收到消息:hello spring boot: topics order.error
[topics_queue1]接收到消息:hello spring boot: topics order.error
  1. 调用接口发送 routingkey 为 order.pay.info 的消息:http://127.0.0.1:8080/producer/topics?routingKey=order.pay.info

观察后端日志, 队列2收到消息

java 复制代码
[topics_queue2]接收到消息:hello spring boot: topics order.pay.info

四.基于 SpringBoot+RabbitMQ 完成应用通信

作为⼀个消息队列, RabbitMQ 也可以用作应用程序之间的通信. 上述代码生产者和消费者代码放在不的应用中即可完成不同应用程序的通信

接下来我们来看, 基于 SpringBoot+RabbitMQ 完成应用间的通信

需求描述:

用户下单成功之后, 通知物流系统, 进行发货. (只讲应用通信, 不做具体功能实现)

订单系统作为⼀个生产者, 物流系统作为⼀个消费者

4.1 创建项目

为方便讲解, 把两个项目放在⼀个项目中(也可独立创建)

  1. 创建⼀个空的项目 rabbitmq-communication (其实就是⼀个空的文件夹)
  1. 在这个项目立, 创建Module
  1. 后续流程和创建 SpringBoot 项目⼀样

添加对应依赖

创建两个项目

  1. logistics-service
  2. order-service
  1. 最终结构如下

4.2 订单系统(生产者)

  1. 完善配置信息
yaml 复制代码
server.port=8080
#amqp://username:password@Ip:port/virtual-host
spring.rabbitmq.addresses=amqp://study:study@110.41.51.65:15673/bite
  1. 声明队列
java 复制代码
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class RabbitConfig {
	//1. ⼯作模式队列
	@Bean("workQueue")
	public Queue workQueue() {
		return QueueBuilder.durable("order.create").build();
	}
}
  1. 编写下单接口, 下单成功之后,发送订单消息
java 复制代码
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RequestMapping("/order")
@RestController
public class OrderController {
	@Autowired
	private RabbitTemplate rabbitTemplate;
	
	@RequestMapping("/createOrder")
	public String createOrder(){
		//下单相关操作, ⽐如参数校验, 操作数据库等, 代码省略
		//发送消息通知
		String orderId = UUID.randomUUID().toString();
		rabbitTemplate.convertAndSend("", "order.create","下单成功, 订单ID:"+orderId);
		return "下单成功";
	}
}
  1. 启动服务, 观察结果
  1. 访问接口, 模拟下单请求: http://127.0.0.1:8080/order/createOrder

可以观察到消息发送成功

查看消息

4.3 物流系统(消费者)

  1. 完善配置信息

8080 端口已经被订单系统占用了, 修改物流系统的端口号为9090

java 复制代码
server.port=9090
#amqp://username:password@Ip:port/virtual-host
spring.rabbitmq.addresses=amqp://study:study@110.41.51.65:15673/bite
  1. 监听队列
java 复制代码
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class OrderCreateListener {
	//指定监听队列的名称
	@RabbitListener(queues = "order.create")
	public void ListenerQueue(String message){
		System.out.println("接收到消息:"+ message);
		//收到消息后的处理, 代码省略
	}
}

4.4 启动服务, 观察结果

访问订单系统的接口, 模拟下单请求: http://127.0.0.1:8080/order/createOrder

在物流系统的日志中, 可以观察到, 通过 RabbitMQ, 成功把下单信息传递给了物流系统

4.5 发送消息格式为对象

如果通过 RabbitTemplate 发送⼀个对象作为消息, 我们需要对该对象进行序列化. Spring AMQP 推荐使用 JSON 序列化,Spring AMQP 提供了 Jackson2JsonMessageConverter 和 MappingJackson2MessageConverter 等转换器, 我们需要把⼀个 MessageConverter 设置到 RabbitTemplate 中.

java 复制代码
@Bean
public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
	return new Jackson2JsonMessageConverter();
}

@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
	RabbitTemplate template = new RabbitTemplate(connectionFactory);
	template.setMessageConverter(jackson2JsonMessageConverter()); // 设置消息转换器
	return template;
}

定义⼀个对象

java 复制代码
@AllArgsConstructor
@NoArgsConstructor
@Data
public class OrderInfo {
	private String orderId;
	private String name;
	private long price;
}

生产者代码:

java 复制代码
@RequestMapping("/createOrder")
public String createOrder(){
	//下单相关操作, ⽐如参数校验, 操作数据库等, 代码省略
	//发送消息通知
	String orderId = UUID.randomUUID().toString();
	OrderInfo orderInfo = new OrderInfo(orderId, "商品", 536);
	rabbitTemplate.convertAndSend("", "order.create",orderInfo);
	return "下单成功";
}

消费者代码:

java 复制代码
@Component
public class OrderCreateListener {
	@RabbitHandler
	@RabbitListener(queues = "order.create")
	public void ListenerQueue(OrderInfo message){
		System.out.println("接收到消息:"+ message);
		//收到消息后的处理, 代码省略
	}
}

@RabbitListener(queues = "order.create") 可以加在类上, 也可以加在方法上, 用于定于⼀个类或者方法作为消息的监听器.

@RabbitHandler 是⼀个方法级别的注解, 当使用 @RabbitHandler 注解时, 这个方法将被调用处理特定的消息.

相关推荐
Lyqfor28 分钟前
云原生学习
java·分布式·学习·阿里云·云原生
流雨声1 小时前
2024-09-01 - 分布式集群网关 - LoadBalancer - 阿里篇 - 流雨声
分布式
floret*1 小时前
用pyspark把kafka主题数据经过etl导入另一个主题中的有关报错
分布式·kafka·etl
william8232 小时前
Information Server 中共享开源服务中 kafka 的__consumer_offsets目录过大清理
分布式·kafka·开源
P.H. Infinity2 小时前
【RabbitMQ】10-抽取MQ工具
数据库·分布式·rabbitmq
落落落sss3 小时前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
Hsu_kk5 小时前
Kafka 安装教程
大数据·分布式·kafka
苍老流年5 小时前
1. kafka分布式环境搭建
分布式·kafka
sj11637394035 小时前
Kafka参数了解
数据库·分布式·kafka
Hsu_kk5 小时前
Kafka Eagle 安装教程
分布式·kafka