一、RabbitMQ有7种通讯模式,见官网教程,展示了各种通讯模式支持的客户端语言(java支持所有的通讯模式场景):
二、通用工具封装:
多数通讯模式都是基于Publisher、Consumer、Exchange、Queue等组件展开的,在建立连接、获取channel上并无多大差别,因此在代码实战中,将获取连接和channel封装成工具会比较方便使用:
1、引入包:
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.16.0</version>
</dependency>
2、创建工具类:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
public class RabbitmqConnectionUtil {
private static final String HOST = "127.0.0.1";
private static final int PORT = 5672;
private static final String USER_NAME = "guest";
private static final String PASSWORD = "guest";
private static final String VIRTUAL_HOST = "/";
public Channel getConnection() throws IOException, TimeoutException {
//1、新建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(HOST);
connectionFactory.setPort(PORT);
connectionFactory.setUsername(USER_NAME);
connectionFactory.setPassword(PASSWORD);
connectionFactory.setVirtualHost(VIRTUAL_HOST);
//2、建立连接
Connection connection = connectionFactory.newConnection();
//3、获取channel
Channel channel = null;
Optional<Channel> optionalChannel = connection.openChannel();
if(optionalChannel.isPresent()){
channel = optionalChannel.get();
}
return channel;
}
}
三、各种通讯模式实战:
1、Hello World:从官网下图中可以看出,hello world模式和work queue模式的区别是一个生产者发送消息到queue后,hello world的consumer只有一个,而work queue的consumer有2个;
b、hello world 生产者代码:
public void sendHelloWorldMessage() {
Channel channel = null;
try {
//1、获取channel
channel = RabbitmqConnectionUtil.getChannel();
//2、创建queue
//通过查看源代码,定义queue的时候指定参数
/*Params:
* queue – the name of the queue 队列名称
* durable – true if we are declaring a durable queue (the queue will survive(存活) a server restart)
是否持久化,即a server restart的时候是否存活
* exclusive – true if we are declaring an exclusive queue (restricted to this connection)
是否排他,即if true时一个队列只能有一个监听者
* autoDelete – true if we are declaring an autodelete queue (server will delete it when no longer in use)
是否自动删除,即if true,server will delete it when no longer in use
* arguments – other properties (construction arguments) for the queue 其他参数,
* body - 消息的byte数组,发送的消息
* Returns:
* a declaration-confirm method to indicate the queue was successfully declared
* Throws:
* IOException – if an error is encountered
* See Also:
* AMQP. Queue. Declare, AMQP. Queue. DeclareOk
* 在exclusive = true的情况下,如果写2个consumer监听该队列,会报错
*/
channel.queueDeclare("hello_world", true, true, false, null);
//3、发送生产者消息
String message = "test hello world";
//发送消息时,应指定exchange和routingkey,
// 在hello world模式下,如果不指定,则exchange使用默认的(AMQP default),而routingkey则是默认的queue的名字
channel.basicPublish("", "", null, message.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
System.out.println("=====获取channel失败======");
throw new RuntimeException(e);
}
}
c、消费者代码:
public void receiverMessage() throws IOException, TimeoutException {
//1、也要先获取连接和channel
Channel channel = RabbitmqConnectionUtil.getChannel();
//2、创建queue,参数必须与hell world publisher保持一致,否则会报错
channel.queueDeclare("hello_world", true, true, false, null);
//3、监听消息队列并打印消息
DefaultConsumer callback = new DefaultConsumer(channel) {
/**
* 回调的方式处理投送的消息
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body, StandardCharsets.UTF_8));
}
};
channel.basicConsume("hello_world",callback);
}
注意,在hello world模式下,虽然没指定具体的exchange和routingkey,但实际上是使用了默认值的值:AMQP default 和 hello_world(队列名称作为默认routingkey)
2、Work Queues:
Work Queue模式和Hello world模式一样,使用默认的exchange和routingkey,区别是可以有多个消费者,因此在生产者和消费者的queueDeclare方法中将exclusive都设置为false:
channel.queueDeclare(RabbitmqConstant.WORK_QUEUES_QUEUE, true, false, false, null);
开启2个线程启动Consumer:
public static void main(String[] args) {
Thread t1 = new Thread(()->{
WorkQueueConsumer workQueueConsumer = new WorkQueueConsumer();
try {
workQueueConsumer.receiverMessage();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
});
Thread t2 = new Thread(()->{
WorkQueueConsumer workQueueConsumer = new WorkQueueConsumer();
try {
workQueueConsumer.receiverMessage();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
});
t1.start();
t2.start();
}
扩展:
a、在当前模式下,每条消息只会被一个Consumer成功消费;
b、默认情况下,队列会以轮询的方式将消息发送给不同的Consumer;
c、当Consumer从队列中获取消息后,需要给Rabbitmq一个ack确认标识,当Rabbitmq收到ack后,会认为消息已经成功被Consumer消费,否则会认为消费失败,会将消息交给下一个Consumer;在上述代码中,ack确认的标识设置为了true,也就是说,当consumer代码执行完后,会自动给Rabbitmq一个ack确认标识;
场景:当因为某些网络或者服务器配置的原因,有些服务器consumer消费消息的性能好,有些服务的性能差,该如何解决这种类似消息数据量倾斜和流量控制问题?
RabbitMQ 提供了一种 QOS(服务质量保证)功能,即在非自动确认消息(autoAck=false)的前提下,如果一定数目的消息还未被消费确认,则不进行新消息的消费。
实现步骤:
(1)设置autoAck=false;
(2)当消息消费完,进行手动ack;
(3)设置消息预取值,即每次取几个消息;
为实现效果,可以在不同的消费端进行Thread.sleep(1000);
Consumer 1:
public void receiverMessage1() throws IOException, TimeoutException {
//1、也要先获取连接和channel
Channel channel = RabbitmqConnectionUtil.getChannel();
//2、创建queue,参数必须与hell world publisher保持一致,否则会报错
channel.queueDeclare(RabbitmqConstant.WORK_QUEUES_QUEUE, true, false, false, null);
//6、设置批量预取值为3,即每次取3个数据
channel.basicQos(1);
//3、监听消息队列并打印消息
DefaultConsumer callback = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1处理消息!");
System.out.println(new String(body, StandardCharsets.UTF_8));
//5、消息处理完毕,进行手动ack,
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
//4、设置autoAck=false
channel.basicConsume(RabbitmqConstant.WORK_QUEUES_QUEUE, false, callback);
System.in.read();
}
Consumer 2:
public void receiverMessage2() throws IOException, TimeoutException {
//1、也要先获取连接和channel
Channel channel = RabbitmqConnectionUtil.getChannel();
//2、创建queue,参数必须与hell world publisher保持一致,否则会报错
channel.queueDeclare(RabbitmqConstant.WORK_QUEUES_QUEUE, true, false, false, null);
//6、设置批量预取值为3,即每次取3个数据
channel.basicQos(1);
//3、监听消息队列并打印消息
DefaultConsumer callback = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2处理消息!");
System.out.println(new String(body, StandardCharsets.UTF_8));
//5、消息处理完毕,进行手动ack,
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//4、设置autoAck=false
channel.basicConsume(RabbitmqConstant.WORK_QUEUES_QUEUE, false, callback);
System.in.read();
}
Publisher 同时生成10条消息:
//3、发送生产者消息
for (int i = 0; i < 10; i++) {
String message = "test workqueues " + i;
//发送消息时,应指定exchange和routingkey,
// 在work_queues模式下,与hello world一样,不指定,则exchange使用默认的(AMQP default),而routingkey则是默认的queue的名字
channel.basicPublish("", RabbitmqConstant.WORK_QUEUES_QUEUE, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("消息发送成功!" + i);
}
会发现,Consumer 1 消费了9条消息,而Consumer 2只消费了一条:
因此,通过设置手动ack的方式,可以让性能更好的Consumer尽可能多的消费消息,从而提升消费端性能。
========================================================================
备注一:Publish/Subscribe、Routing、Topics这三种通讯模式,从官网的图可以看出,这三种模式的Consumer是一样的,区别在于新增了自定义exchange(不再使用默认的AMQP default和RoutingKey),而这种自定义的exchange,有三种类型:
Publish/Subscribe:同时向所有消费者进行发送消息,这种通讯模式,使用扇型交换机(fonout exchange)
Routing: 有选择性的进行消息投递,这种通信模式使用直接交换机(direct exchange)
Topics:基于主题的消息发布模式,这种通讯模式使用主题交换机(topic exchange)
备注二、使用这三种模式的步骤:
(1)自定义exchange,指定类型,exchange的类型有:fonout,direct,topic
(2)定义队列;
(3)将exchange和队列,通过routingKey进行绑定;
备注三、这三类的消费者实现与前面的实现没有差异,省略
3、Publish/Subscribe生产者实现:
/**
* 相当于定义了广播消息,不管是否指定routingkey,都会将消息路由到绑定的每一个queue,类似于广播
* @throws IOException
* @throws TimeoutException
*/
public void publishAndSubscribe() throws IOException, TimeoutException {
//1、获取channel
Channel channel = RabbitmqConnectionUtil.getChannel();
//2、自定义exchange,指定名称和类型
channel.exchangeDeclare(RabbitmqConstant.FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT);
//3、定义queue
channel.queueDeclare(RabbitmqConstant.FANOUT_QUEUE, true, false, false, null);
//4、将exchange和queue进行绑定,在FANOUT这种消息类型下,不管是否指定routingkey,都会将消息路由到绑定的每一个queue,类似于广播
channel.queueBind(RabbitmqConstant.FANOUT_QUEUE, RabbitmqConstant.FANOUT_EXCHANGE, "");
channel.basicPublish(RabbitmqConstant.FANOUT_EXCHANGE,"",null,"测试FANOUT类型的消息".getBytes(StandardCharsets.UTF_8));
}
4、Routing生产者实现:
/**
*
* 必须指定routingkey,只有精准匹配了该routingkey的值的绑定队列,才会被路由消息,点对点播
* @throws IOException
* @throws TimeoutException
*/
public void routing() throws IOException, TimeoutException {
//1、获取channel
Channel channel = RabbitmqConnectionUtil.getChannel();
//2、自定义exchange,指定名称和类型
channel.exchangeDeclare(RabbitmqConstant.DIRECT_EXCHANGE, BuiltinExchangeType.DIRECT);
//3、定义queue
channel.queueDeclare(RabbitmqConstant.DIRECT_QUEUE, true, false, false, null);
//4、将exchange和queue进行绑定,在DIRECT这种消息类型下,必须指定routingkey,只有精准匹配了该routingkey的值的绑定队列,才会被路由消息,点对点播
//声明2个队列,指定不同的routingkey
channel.queueBind(RabbitmqConstant.DIRECT_QUEUE, RabbitmqConstant.DIRECT_EXCHANGE, "rabbitmq");
channel.queueBind(RabbitmqConstant.DIRECT_QUEUE, RabbitmqConstant.DIRECT_EXCHANGE, "rabbitmp");
//由于指定了routingkey,能匹配上绑定的queue,因此消息会被路由
channel.basicPublish(RabbitmqConstant.DIRECT_EXCHANGE,"rabbitmq",null,"测试DIRECT类型的消息".getBytes(StandardCharsets.UTF_8));
//虽然消息发送到exchange,但exchange在做消息投递的时候,找不到对应的routingkey的queue,因此消息不会被投递
channel.basicPublish(RabbitmqConstant.DIRECT_EXCHANGE,"rabbitmo",null,"测试DIRECT类型的消息".getBytes(StandardCharsets.UTF_8));
}
5、Topics生产者实现:
/**
* topic通讯模式,基于占位符和通配符,可以实现类似于组播的效果,比如topic=项目名.业务模块.主题
* @throws IOException
* @throws TimeoutException
*/
public void topic() throws IOException, TimeoutException {
//1、获取channel
Channel channel = RabbitmqConnectionUtil.getChannel();
//2、自定义exchange,指定名称和类型
channel.exchangeDeclare(RabbitmqConstant.TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC);
//3、定义queue
channel.queueDeclare(RabbitmqConstant.TOPIC_QUEUE, true, false, false, null);
//4、将exchange和queue进行绑定,在DIRECT这种消息类型下,必须指定routingkey,只有精准匹配了该routingkey的值的绑定队列,才会被路由消息,点对点播
//routingkey的格式为aaa.bbb.ccc这种格式,加上*或者#,
//其中*代表占位符,表示可以匹配.的前后可以有一段字母,如*.aaa.bbb,表示111.aaa.bbb这类消息会被路由
//#代表通配符,表示可以匹配.前后的多段字段,如*.aaa.#,表示111.aaa.bbb.ccc.ddd这类消息会被路由
//基于topic的通讯模式,可以实现类似于组播的效果,比如topic=项目名.业务模块.主题
channel.queueBind(RabbitmqConstant.TOPIC_QUEUE, RabbitmqConstant.TOPIC_EXCHANGE, "*.aaa.#");
//由于指定了routingkey,能匹配上绑定的queue,因此消息会被路由
channel.basicPublish(RabbitmqConstant.TOPIC_EXCHANGE,"111.aaa.000.000",null,"测试TOPIC类型的消息".getBytes(StandardCharsets.UTF_8));
}
总结:
(1)当不指定exchange时,使用默认exchange;
当queue只有一个时,生产者将消息路由到默认交换机,指定queue,所有消息会路由到这一个queue;
当queue有多个时,生产者将消息路由到默认交换机,指定queue,消息会以轮询的方式进行路由;
(2)当指定exchange,分三种情况:
当exchange的类型为fonout时,exchange和queue进行默认绑定(忽略routingkey的绑定),当生产者将消息路由到exchange,指定queue,消息会进行广播,即所有订阅了该queue的consumer都会收到一份消息;
当exchange的类型为direct时,exchange和queue通过routingkey直接绑定,生产者将消息路由到exchange,指定queue,只有当消费者订阅了该queue,并且routingkey与绑定的routingkey完全一致时,才会收到消息;
当exchange的类型为toipc时,exchange和queue通过routingkey通过占位符和通配符绑定,生产者将消息路由到exchange,指定queue,只有当消费者订阅了该queue,并且routingkey与绑定的routingkey占位符或者通配符匹配时,才会收到消息;
(1)和(2)的区别是当消费者有多个时,轮询发消息和匹配的消费者都发一份;
6、RPC实现:
RPC模式类似于通过消息队列,实现服务之间的调用和响应;
7、Publisher Confirms:发布确认模式,实际上该模式是为了确保生产者在发布消息时保证消息不丢失的一种特殊情况;
根据官网介绍,使用同步(sync)回调和异步回调的方式进行消息发送确认:
同步回调:
单条消息确认:
//单条消息发送及确认
channel.basicPublish(CONFIRM_DIRECT_EXCHANGE, CONFIRM_DIRECT_ROUTING_KEY, true, null, "确保消息能发生成功".getBytes(StandardCharsets.UTF_8));
boolean confirms = channel.waitForConfirms(5000);
if (!confirms) {
System.out.println("========执行消息补救=========");
} else {
System.out.println("发送成功!");
}
批量消息确认:
//批量消息发送和确认
for (int i = 0; i < 5; i++) {
channel.basicPublish(CONFIRM_DIRECT_EXCHANGE, CONFIRM_DIRECT_ROUTING_KEY, true, null, "确保消息能发生成功".getBytes(StandardCharsets.UTF_8));
//任何一条消息发送失败都会抛出异常
try {
channel.waitForConfirmsOrDie(5000);
} catch (Exception e) {
System.out.println(e.getMessage());
System.out.println("========执行消息补救=========");
}
}
很显然,同步回调并不适合在真正开发中使用(本来mq就是用来解耦、异步、提升性能的,结果消息确认同步等待,性能影响比较大)
异步回调:
//1、获取连接和通道
Channel channel = RabbitmqConnectionUtil.getChannel();
//定义交换机,队列和routingkey
channel.exchangeDeclare(CONFIRM_DIRECT_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.queueDeclare(CONFIRM_DIRECT_QUEUE, true, false, false, null);
channel.queueBind(CONFIRM_DIRECT_QUEUE, CONFIRM_DIRECT_EXCHANGE, CONFIRM_DIRECT_ROUTING_KEY);
//3、开启消息确认
channel.confirmSelect();
//4、消息发送
channel.basicPublish(CONFIRM_DIRECT_EXCHANGE, CONFIRM_DIRECT_ROUTING_KEY, true, null, "确保消息能发生成功".getBytes(StandardCharsets.UTF_8));
//5、消息结果回调
channel.addConfirmListener((sequenceNumber, multiple) -> {
// code when message is confirmed
//sequenceNumber,消息的序列号
// multiple:是否批量消息
System.out.println("消息发送成功!");
}, (sequenceNumber, multiple) -> {
// code when message is nack-ed
System.out.println("消息发送失败!");
});