快速入门(java)
-
首先安装rabbitmq(单机版)
在我自己租的云服务器上,直接用docker进行安装(一行命令搞定)
docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.9-management
然后在阿里云的控制台,放开
5672
和15672
端口随后,可以直接登录rabbitmq的管理后台
http://127.0.0.1:15672
,便能看到rabbitmq的情况rabbit会创建一个默认的用户,用户名
guest
,密码guest
-
基于java编写一个简单的生产者和消费者
创建一个简单的
maven
项目,引入rabbitmq
的java依赖包<dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.9.0</version> </dependency>
把rabbitmq的相关信息放在一个常量类中
package com.yogurt.demo.rabbit; /** * @Author yogurtzzz * @Date 2021/12/14 9:42 **/ public class Constants { private Constants() { } public static final String RABBIT_IP = "127.0.0.1"; public static final int RABBIT_PORT = 5672; public static final String RABBIT_USER = "guest"; public static final String RABBIT_PASSWORD = "guest"; public static final String RABBIT_QUEUE_NAME = "hello"; }
编写一个生产者,负责推送消息到rabbit
package com.yogurt.demo.rabbit; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.BufferedReader; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import static com.yogurt.demo.rabbit.Constants.*; public class Send { public static void main(String[] argv) throws Exception { // 连接工厂 ConnectionFactory factory = new ConnectionFactory(); // 设置连接信息, ip, 端口号, 账号, 密码 factory.setHost(RABBIT_IP); factory.setPort(RABBIT_PORT); factory.setUsername(RABBIT_USER); factory.setPassword(RABBIT_PASSWORD); // 创建连接, 发送消息 (使用try-with-resource) try (Connection connection = factory.newConnection()) { String message = "Hello Rabbit"; Channel channel = connection.createChannel(); //如果该名称的队列不存在, 则新建一个 channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null); // 向该队列发送一条消息 channel.basicPublish("", RABBIT_QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8)); System.out.println(" [x] Sent '" + message + "'"); } } }
跑起来!
然后我们登录管理页面看看
可以看到名为
hello
的队列中,有1条消息,我们可以点击队列的名称,然后点击Get Messages
,获取队列中的消息,可以看到这条消息的内容是Hello Rabbit
说明消息成功发送到rabbitmq当中了
随后,我们编写一个消费者
package com.yogurt.demo.rabbit; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DeliverCallback; import java.nio.charset.StandardCharsets; import static com.yogurt.demo.rabbit.Constants.*; /** * @Author yogurtzzz * @Date 2021/12/14 9:42 **/ public class Recv { public static void main(String[] args) { ConnectionFactory factory = new ConnectionFactory(); factory.setHost(RABBIT_IP); factory.setPort(RABBIT_PORT); factory.setUsername(RABBIT_USER); factory.setPassword(RABBIT_PASSWORD); // 获取连接 Connection connection = null; try { connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null); System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); DeliverCallback deliverCallback = (consumerTag, delivery) -> { String message = new String(delivery.getBody(), StandardCharsets.UTF_8); System.out.println(" [x] Received '" + message + "'"); }; channel.basicConsume(RABBIT_QUEUE_NAME, true, deliverCallback, consumerTag -> { }); } catch (Exception e) { e.printStackTrace(); } } }
跑起来!
消费者成功消费到了
上面的示例就是一个最基本的模型,只有一个生产者,一个队列,一个消费者。
下面演示一个生产者,多个消费者的情况
这是一种竞争消费的模式,在一个队列上,绑定了多个消费者,消费者会争抢着消费消息。
生产者
package com.yogurt.demo.rabbit;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import static com.yogurt.demo.rabbit.Constants.*;
public class Send {
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(RABBIT_IP);
factory.setPort(RABBIT_PORT);
factory.setUsername(RABBIT_USER);
factory.setPassword(RABBIT_PASSWORD);
// 获取连接, 发送消息
try (Connection connection = factory.newConnection()) {
// 从控制台读入
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String message = reader.readLine();
// 输入 -1 则表示退出
if ("-1".equals(message)) return;
Channel channel = connection.createChannel();
channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null);
channel.basicPublish("", RABBIT_QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}
消费者
package com.yogurt.demo.rabbit;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static com.yogurt.demo.rabbit.Constants.*;
/**
* @Author yogurtzzz
* @Date 2021/12/14 9:42
**/
public class Recv implements Runnable{
@Override
public void run() {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(RABBIT_IP);
factory.setPort(RABBIT_PORT);
factory.setUsername(RABBIT_USER);
factory.setPassword(RABBIT_PASSWORD);
long threadId = Thread.currentThread().getId();
// 获取连接
Connection connection = null;
try {
connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null);
System.out.println("Thread " + threadId + " [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println("Thread " + threadId + " [x] Received '" + message + "'");
};
channel.basicConsume(RABBIT_QUEUE_NAME, true, deliverCallback, consumerTag -> { });
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
Runnable runnable = new Recv();
// 启动5个消费者
for (int i = 0; i < 5; i++) {
new Thread(runnable).start();
}
// stuck here
System.in.read();
}
}
先启动5个消费者
可以在管理后台看到现在有5个连接
再启动生产者,并在控制台输入一些信息
可以看到发送到rabbitmq的三条消息,成功被消费者消费(5个消费者争抢着消费,一条消息只会被一个消费者消费,此种模式下,rabbitmq会依次将消息推送给消费者,根据下图可以观察到,消费者的启动顺序为15,16,13,14,12。rabbitmq也按照这个顺序(轮询,Round-Robin)依次把消息交给对应的消费者进行消费)
快速入门(springboot)
上面介绍的是基于java
的简单教程,但是通常我们开发一个应用,会使用到框架,其中又以springboot为代表。下面介绍rabbitmq整合springboot的基本使用
-
创建一个springboot项目
-
pom.xml
中添加如下依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
-
在
application.yml
中配置rabbitmq的地址等spring: application: name: rabbitmq-demo rabbitmq: host: 127.0.0.1 port: 5672 username: yogurt password: yogurt virtual-host: /test
-
添加配置类,配置队列,consumer工厂,消息转换器等
package com.demo.rabbitmq.config; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMqConfig { /** * 注册一个 MessageConverter, 发送消息时可以直接发送一个POJO **/ @Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } /** * 新建一个队列, 队列名为 yogurt * **/ @Bean public Queue yogurt() { return new Queue("yogurt"); } /** * 配置consumer工厂 * **/ @Bean public SimpleRabbitListenerContainerFactory consumerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); // consumer 的 prefetch 设置 factory.setPrefetchCount(30); // 并发配置 - 同时开启5个消费者(5个线程) factory.setConcurrentConsumers(5); // 最大并发配置 (当消息堆积时, 会新开线程来处理, 最大能到20个) // 有点类似jdk的线程池 factory.setMaxConcurrentConsumers(20); // 消费者开启 手动ack 机制 factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); // 接收消息时, 可以直接将消息反序列化为 POJO factory.setMessageConverter(new Jackson2JsonMessageConverter()); configurer.configure(factory, connectionFactory); return factory; } }
-
定义一个POJO,表示发送到rabbitmq的消息
public class UserInfo implements Serializable { private String name; private Integer age; private String career; private String gender; private String hometown; // 省略了构造函数和 getter/setter }
-
创建生产者
package com.demo.rabbitmq.component; import com.demo.rabbitmq.data.UserInfo; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Queue; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; /** * @Author yogurtzzz * @Date 2021/12/15 14:55 **/ @Profile("sender") @Component public class RabbitMqSender { private int cnt = 0; // 由 rabbitmq-starter 自动注册进来的, 其实现目前只有1个 RabbitTemplate // 但为了依赖于接口, 最好用 AmqpTemplate 来接收 @Autowired private AmqpTemplate template; // 这里的 Queue 就是前面配置的名称为 yogurt 的队列 @Autowired private Queue queue; /** * 每4秒发送一条消息 * */ @Scheduled(fixedRate = 5000, initialDelay = 2000) public void send() { cnt++; UserInfo info = new UserInfo("yogurt-" + cnt, 26, "Software Engineer", "Male", "China"); // 发送一个 UserInfo 对象到 rabbitmq template.convertAndSend(queue.getName(), info); System.out.println("[x] Sent"); } }
-
创建消费者
package com.demo.rabbitmq.component; import com.demo.rabbitmq.data.UserInfo; import com.rabbitmq.client.Channel; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.context.annotation.Profile; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Map; import java.util.concurrent.TimeUnit; /** * @Author yogurtzzz * @Date 2021/12/15 15:02 **/ @Component @Profile("receiver") public class RabbitMqReceiver { // 指定要监听的队列名称, 以及消费者的 factory @RabbitListener(queues = "yogurt", containerFactory = "consumerFactory") @RabbitHandler public void receive(@Payload UserInfo info, @Headers Map<String, Object> headers, Channel channel) throws InterruptedException, IOException { long id = Thread.currentThread().getId(); System.out.println("Consumer " + id + " has received message : " + info + ""); System.out.println("handling..."); // 模拟处理... TimeUnit.SECONDS.sleep(5); // 获取消息的 deliveryTag Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); // 手动ack channel.basicAck(deliveryTag, false); System.out.println("Consumer " + id + " finished handle"); } }
-
由于用到了
@Scheduled
注解,注意在springboot启动类上加上@EnableScheduling
注意:SpringBoot中使用 @Bean创建的Queue或者Exchange,其实是延迟创建的,是会等到某个生产者或某个消费者使用到了这个Queue,才会实际在RabbitMQ上创建这个Queue。所以在SpringBoot刚启动时,你可能会觉得奇怪:“诶,RabbitMQ的控制台中,为什么看不到我的Queue啊”。
进阶
消息确认机制
mq通常被用来进行系统解耦,限流削峰等。若mq中的一条消息,对应了一个耗时任务。那么当一个消费者获取到一条消息后,会执行该耗时任务,如果消费者拿到该任务,但是在执行过程中出错了,或者该消费者宕机了,那该耗时任务实际就没有被执行成功,也就是该消息实际是丢失掉了。
为了防止消息丢失,rabbitmq提供了一种消息确认机制,即当rabbbitmq把某条消息推送给某消费者后,还需要该消费者返回一个ack
信号给到rabbitmq,随后rabbitmq才会将该消息安全的删除掉。若rabbitmq将某条消息推送给某消费者,该消费者还没有返回ack
信号(有一个超时时间的配置,Delivery Acknowledgement Timeout
,默认是30分钟),消费者和rabbitmq的连接就断掉了,那么rabbitmq会将该条消息重新推送给另外的消费者进行处理。
当消费者发送一个ack
信号给rabbitmq,就是告诉rabbitmq,某个特定的消息已经被接收并处理完毕,rabbitmq可以删除该条消息了。
特别注意:某条消息的ack
信号,必须由接收该条消息的channel
发出。若尝试使用不同的channel
发送ack
信号, 则会报异常(channel-level protocol exception)。
我们可以在消费者中通过让线程休眠的方式,来模拟耗时任务的处理,修改消费者的代码如下
package com.yogurt.demo.rabbit;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import static com.yogurt.demo.rabbit.Constants.*;
/**
* @Author yogurtzzz
* @Date 2021/12/14 9:42
**/
public class Recv implements Runnable{
@Override
public void run() {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(RABBIT_IP);
factory.setPort(RABBIT_PORT);
factory.setUsername(RABBIT_USER);
factory.setPassword(RABBIT_PASSWORD);
long threadId = Thread.currentThread().getId();
// 获取连接
Connection connection = null;
try {
connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RABBIT_QUEUE_NAME, false, false, false, null);
System.out.println("Thread " + threadId + " [*] Waiting for messages. To exit press CTRL+C");
// 创建一个callback, 处理消息回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println("Thread " + threadId + " [x] Received '" + message + "'");
try {
// 模拟耗时任务的处理
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 发送ack信号给rabbitmq
// 第一个参数可以认为是该消息的唯一id, 用来表示需要确认的是哪条消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
// 第二个参数是 autoAck, 之前我们设置的是 true , 表示消息被消费后, 立刻返回确认
// 现在改为 false, 则需要消费者手动发送 ack 信号给 rabbitmq
// 开始消费
channel.basicConsume(RABBIT_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
Runnable runnable = new Recv();
// 启动5个消费者
for (int i = 0; i < 5; i++) {
new Thread(runnable).start();
}
// stuck here
System.in.read();
}
}
现在我们启动生产者,随便发送几条消息
进入管理后台,看到有3条信息仍然是unacked
状态
等待一会儿后,会发现unacked
的消息变为了0
小节:为了防止消息丢失,rabbitmq提供了ack
,消息确认机制。注意某条消息的ack
信号,必须通过接收该消息的相同channel
发送回去。
消息持久化
上面的消息确认机制,保证了在消费者异常(宕机等)时,消息不会丢失;但如果rabbitmq服务自身宕机了呢?
这就涉及到另一个机制,消息持久化(Message Durability)
若rabbitmq退出或者崩溃了,它会丢失所有的队列(queues)和消息(messages),除非我们显式地将队列和消息标记为可持久化(durable)。
boolean durable = true;
channel.queueDeclare("hello", durable, false, false, null);
上面这行代码本身没错,但是在我们先前的场景下不会起作用。因为先前我们已经创建了一个名为hello
的队列(并且没有声明为durable
),rabbitmq不允许对已存在的队列进行重新定义。
所以,我们可以创建一个不同名称的新的队列,并标记为durable
boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);
至此,我们保证了队列已经是可持久化的了。我们还需保证消息也是可持久化的。
在生产者push
一条消息到队列里时,标记该消息为持久化即可,如下
import com.rabbitmq.client.MessageProperties;
// PERSISTENT_TEXT_PLAIN 标记该消息为持久化消息
channel.basicPublish("", "task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
将某条消息标记为持久化,并不能100%保证该消息就一定不会丢失。
从rabbitmq接收到一条消息,到rabbitmq将该消息持久化,中间仍然有一段很短的时间窗口(若rabbitmq在这个时间窗口内宕机,该消息仍然会丢失)。
rabbitmq并不是对每条消息都会用fsync(2)
进行刷盘,消息可能仅仅是存在了cache
中,但并没有实际写入disk
。
如果需要对消息持久化,提供更强的保证,可以使用publisher confirms
机制(publisher confirms - 官网文档)
消息公平分发
考虑这样一个场景,有2个消费者监听同一个队列。这个队列中,偶数位置的消息非常重量,奇数位置的消息非常轻量。
由于rabbitmq默认采用round-robin
的轮询策略。则会固定将奇数位置的消息,分发给消费者C1
,而将偶数位置的消息,分发给消费者C2
。
由于偶数位置的消息非常重量,需要较多处理时间,而奇数位置的消息比较轻量,只需要较少处理时间。这就会导致,消费者C2
始终处于busy
状态(甚至很多消息堆积着等待C2
处理),而C1
则非常空闲。
这是因为rabbitmq在进行消息分发时,不会检查某个消费者的unacked
的消息数量(如果检查这个信息的话,就能得知某个消费者很忙,堆积了很多消息没处理完毕),它仅仅依照round-robin
进行消息分发。
为了更加公平地进行消息分发,可以使用basicQos
方法,设置prefetchCount=1
。
int prefetchCount = 1;
channel.basicQos(prefetchCount);
这告诉了rabbitmq,每次不要分发超过1条消息给一个consumer
。换句话说,当某个consumer
正在处理某条消息,并且还没有返回ack
时,rabbitmq就不会将消息分发给该consumer
,而会将该消息分发给下一个空闲的consumer
。
上面只是简单的演示代码,为了简化,省略了很多内容,上面的示例代码,不应该用于正式环境。仍然需要了解rabbitmq的其他内容,其中,推荐下面的几个文档:
Publisher Confirms and Consumer Acknowledgements
消息安全性
Consumer Acknowledgements & Publisher Confirms
上述2者都是为了确保data safety
其中,Publisher Confirms,确保了从publishers
到 rabbitmq
节点的消息可靠投递;
Consumer Acknowledgements 确保了从rabbitmq
节点到consumers
的消息可靠投递。
一次消息的投递(delivery
),通过delivery tag
进行唯一标识。当rabbitmq把一条消息推送给一个consumer
时,会同时带上这条消息的delivery tag
。这个delivery tag
,在当前这个channel
上,是唯一的,且delivery tag
是单调递增的,由于其是一个64位的long
型整数,所以其最大值为9223372036854775807
故,delivery tag
的作用范围是每个channel
。
由于delivery tag
的作用范围是channel
,所以,某个delivery
的ack
信号,必须在同一个channel
上发送。
Because delivery tags are scoped per channel, deliveries must be acknowledged on the same channel they were received on. Acknowledging on a different channel will result in an “unknown delivery tag” protocol exception and close the channel.
rabbitmq可以在发出一个消息后立刻认为该消息被ack
了(自动),也可以通过consumer
手动返回ack
信号(手动),手动返回的ack
信号,可以有如下几种(协议中的方法)
basic.ack
:告诉rabbitmq这条信息被成功处理了,rabbitmq可以将其丢弃basic.nack
basic.reject
自动ack的使用场景
Another thing that’s important to consider when using automatic acknowledgement mode is consumer overload. Manual acknowledgement mode is typically used with a bounded channel prefetch which limits the number of outstanding (“in progress”) deliveries on a channel. With automatic acknowledgements, however, there is no such limit by definition. Consumers therefore can be overwhelmed by the rate of deliveries, potentially accumulating a backlog in memory and running out of heap or getting their process terminated by the OS
手动ack可以支持批量操作(一次性ack
多个deliveries
),以减少网络开销。
// 第二个参数 mutile 置为 true,
channel.basicAck(deliveryTag, true);
若在某个channel
上,有4个还没有被ack
的delivery
,他们的delivery tag
分别是,5,6,7,8,若8的这个delivery
到达,并且准备调用ack
,并将multiple
置为true
,则会一次性ack
8之前的全部delivery
,即5,6,7,8都会被ack
。
若是否定的ack
,则rabbitmq可以选择丢弃,也可以选择重新入队,这个行为是通过属性requeue
来控制的。
boolean requeue = false;
channel.basicReject(deliveryTag, requeue); // basicReject / basicNack
//否定的ack, 并且该消息不会被requeue
Prefetch
Channel Prefetch Settings
Because messages are sent (pushed) to clients asynchronously, there is usually more than one message “in flight” on a channel at any given moment. In addition, manual acknowledgements from clients are also inherently asynchronous in nature. So there’s a sliding window of delivery tags that are unacknowledged
The value defines the max number of unacknowledged deliveries that are permitted on a channel
Once the number reaches the configured count, RabbitMQ will stop delivering more messages on the channel unless at least one of the outstanding ones is acknowledged. (A value of 0 is treated as infinite, allowing any number of unacknowledged messages.)
The QoS setting can be configured for a specific channel or a specific consumer. The Consumer Prefetch guide explains the effects of this scoping
使用basic.qos
来设置prefetch
属性(这个prefetch
属性可以针对channel
,或者针对consumer
来进行设置)
channel.basicQos(10); // Per consumer limit
channel.basicQos(10, false); // Per consumer limit
channel.basicQos(15, true); // Per channel limit
prefetch
属性对consumer
吞吐量的影响
Acknowledgement mode and QoS prefetch value have significant effect on consumer throughput. In general, increasing prefetch will improve the rate of message delivery to consumers. Automatic acknowledgement mode yields best possible rate of delivery.
However, in both cases the number of delivered but not-yet-processed messages will also increase, thus increasing consumer RAM consumption.
如果增大了prefetch
的值,或者开启了自动ack机制(自动ack机制开启后,prefetch
没有限制,相当于无限大),如果在consumer
端堆积了很多已接收但还未被处理的消息,会很吃consumer
的内存。
所以,自动ack机制,或,手动ack+无限制prefetch,者两种模式都应该谨慎采用。
Values in the 100 through 300 range usually offer optimal throughput and do not run significant risk of overwhelming consumers
100-300的配置,通常能提供较为优秀的吞吐量表现。
小节:
prefetch
设置可以是per consumer
,也可以是per channel
prefetch
设为0
表示无限制- 无限制的
prefetch
可能会导致consumer
端消息堆积,而非常消耗consumer
的内存 prefetch
通常设为100到300的区间,能够使得consumer
取得较好的吞吐量表现prefetch
设为1是最保守的,会显著的降低吞吐量
当设置了手动ack,并且某个consumer宕机或者连接丢失了,会自动将该消息重新入队,requeue
。该消息随后会被重新投递(redelivery
),并且在头信息中,会有一个布尔属性redelivery=true
。
注意,考虑这样一个场景,若某个consumer消费并成功处理完一条消息,在返回ack信号时,由于网络抖动,rabbitmq没有收到这个ack,则rabbitmq进行requeue,这条消息随后可能会被另外的consumer重新消费到。此时会导致消息重复消费。需要注意在consumer
端通过一定的策略来保证消息消费的幂等性。
其他消息模式
发布订阅模型
在上面的介绍中,我们都是先创建一个queue
,然后每条消息都会被投递给某一个consumer
。
现在,我们介绍发布-订阅模式(pub/sub
,或publish/subscribe
)。在这种模式下,一条消息可以被投递给多个consumer
。
设想这样一个场景,我们准备构建一个日志系统。其中包含两类程序,第一类为生产者,它会发送日志信息到rabbitmq,第二类为消费者,它会接收日志信息。
在日志系统中,消费者程序有2个实例,其一会直接将日志持久化到磁盘,其二会将日志直接输出到屏幕,方便观察。也就是说,同一条消息,会被2个消费者重复消费。
本质上来讲,由生产者推送到mq的日志信息,会被广播给全部的消费者。这就是发布-订阅模型。
在前面,我们只介绍了如何向一个队列发送消息,或从一个队列接收消息。现在,我们将介绍rabbitmq中完整的消息模型。
我们先回顾一下先前的模型中,所包含的组件
-
生产者(
producer
):一个用户程序,负责发送消息 -
队列(
queue
):一个缓冲区(buffer
),负责存储消息 -
消费者(
consumer
):一个用户程序,负责接收消息
Rabbitmq中一个核心的概念是:producer
发送消息时,其实不是直接发送给某个queue
的。
实际上,producer
对任何queue
都一无所知。producer
只能把消息发送给一个交换机(exchange
)
exchange
是个很简单的东西,一方面,它从producer
那里接收消息;另一方面,它将消息推送到queues
。
当exchange
从producer
那里接收到一条消息后,它需要决定怎么做,比如
- 将这条消息追加到某个特定的
queue
- 将这条消息追加到多个
queue
- 将这条消息丢弃
- …
根据上述的针对消息的不同处理方式,将exchange
分为了不同的类型,这种类型称为exchange type
。
有如下几种的exchange type
direct
topic
headers
fanout
下面我们将特别关注最后一个exchange type
,即fanout
这个fanout
也非常的简单,从其名称就能看出来,它做的仅仅是,将它从producer
那里收到的消息,广播给所有它知道的全部queue
。而这种类型的exchange
,恰好是我们前面讨论的发布-订阅模型所需要的。
在先前的教程中,我们对exchange
都没有感知,但仍然能够向某个特定的queue
发送消息,这是因为我们拥有一个默认的exchange
,这个exchange
用空字符串""
来标识,回想我们前面快速入门中的生产者的代码
// 第一个参数就是exchange的名称, 前面的教程中我们直接将其设为了空字符串, 表示使用默认exchange
channel.basicPublish("", RABBIT_QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
这个默认exchange
,会直接将消息路由给指定名称的队列(由routeKey
标识)
当我们创建了几个queue
,以及一个fanout
类型的exchange
后,我们需要告诉exchange
,将消息发送到哪些queue
。此时我们需要把queue
和exchange
进行绑定(binding
)。
channel.queueBind("queue_name", "exchange_name", "routing_key");
如果某个exchange
没有绑定任何的queue
,则发送到该exchange
的消息,将会丢失。
下面的示例代码,演示了exchange
的使用
rabbitmq配置信息:
package com.demo.rabbitmq.simple;
/**
* @Author yogurtzzz
* @Date 2021/12/14 9:42
**/
public class Constants {
private Constants() { }
public static final String RABBIT_IP = "127.0.0.1";
public static final int RABBIT_PORT = 5672;
public static final String RABBIT_USER = "yogurt";
public static final String RABBIT_PASSWORD = "yogurt";
public static final String RABBIT_VIRTUAL_HOST = "/test";
public static final String RABBIT_QUEUE_NAME = "hello";
public static final String EXCHANGE_NAME = "logs";
}
生产者:
package com.demo.rabbitmq.fanout;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
import static com.demo.rabbitmq.simple.Constants.*;
/**
* @Author yogurtzzz
* @Date 2021/12/16 10:36
**/
public class LogEmitter {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(RABBIT_IP);
connectionFactory.setPort(RABBIT_PORT);
connectionFactory.setUsername(RABBIT_USER);
connectionFactory.setPassword(RABBIT_PASSWORD);
connectionFactory.setVirtualHost(RABBIT_VIRTUAL_HOST);
try(Connection connection = connectionFactory.newConnection()) {
Channel channel = connection.createChannel();
// 新建一个 exchange
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 从控制台读取输入
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String message;
while(!"-1".equals(message = reader.readLine())) {
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes(StandardCharsets.UTF_8));
}
}
}
}
消费者:
package com.demo.rabbitmq.fanout;
import com.rabbitmq.client.*;
import com.rabbitmq.client.impl.ChannelN;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
import static com.demo.rabbitmq.simple.Constants.*;
import static com.demo.rabbitmq.simple.Constants.RABBIT_VIRTUAL_HOST;
/**
* @Author yogurtzzz
* @Date 2021/12/16 10:54
**/
public class LogReceiver {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(RABBIT_IP);
connectionFactory.setPort(RABBIT_PORT);
connectionFactory.setUsername(RABBIT_USER);
connectionFactory.setPassword(RABBIT_PASSWORD);
connectionFactory.setVirtualHost(RABBIT_VIRTUAL_HOST);
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 创建一个 临时queue (默认是 autoDelete, exclusive 的)
// 即, 这个临时queue, 默认是被当前 connection 独占, 且当前 connection 断开后会自动删除
String queueName = channel.queueDeclare().getQueue();
// 将该 queue 绑定到 exchange
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 创建回调函数
DeliverCallback callback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
// 开始监听
channel.basicConsume(queueName, true, callback, consumerTag -> {});
}
}
跑起来!先运行2个消费者实例
进入管理界面,能够看到已经创建了2个临时queue
并且能看到有一个名为logs
的exchange
并且这个exchange
是绑定到了2个临时queue
的
随后运行生产者,并在控制台输入一些信息,便能在2个消费者看到接收到了消息。
于是,借助fanout
类型的exchange
,我们实现了前面所说的发布-订阅模型
上面的示例是基于java的实现,而基于springboot实现的发布-订阅模型,参考官方文档
死信交换机
基于死信交换机,实现延迟队列,参考这篇文章 -> RabbitMQ实现延迟队列