在上一篇博客,笔者简单的介绍了一些RabbitMQ相关的内容,在这一篇博客会根据RabbitMQ官网的入门介绍,结合笔者自身的理解更深入的在代码方面介绍RabbitMQ的入门使用,。同样,这篇博客主要的目的也是整理记录自己的学习笔记,加深自己对RabbitMQ的使用与理解。
RabbitMQ入门(一)
RabbitMQ入门(二)
RabbitMQ入门(三)
目录
1.入门
创建消息生产者
创建消息消费者
2.工作队列
创建新的消费者
测试
设置AutoAck为false的注意事项
3.发布/订阅模式在RabbitMQ中实现方式
4.部分API使用说明(个人理解,仅供参考)
channel.queueDeclare
channel.queueBind
channel.basicPublish
5.补充说明
首先我们先来看RabbitMQ官网的一段消息生产者相关代码:
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setVirtualHost("myVirualHost");
try (Connection conn = factory.newConnection(); Channel channel = conn.createChannel()) {
channel.exchangeDeclare("helloExchange ", "direct", true);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, " helloExchange ", "bindKey");
String message = ”message from send“;
channel.basicPublish("helloExchange", "bindKey", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
}
}
这里可以看到首先 我们是创建了ConnectionFacotry 并设置了 Host和VirtualHost 因为端口 和用户名 密码我们使用默认的所以并没有设置。然后 fatory.newConnection 创建Connection,之后 Connection.createChannel创建channel,而我们的操作基本都是基于 channel的。这里使用了try-with-resources语句,在try语句中声明channel等资源,这样我们不用手动关闭资源。
channel.exchangeDeclare("helloExchange", "direct", true);
定义一个名为 helloExchange 类型是 direct的exchange. 如果RabbitMQ此VirtualHost中 已经有同名同类型的Exchange 那么可能会报错 这里设置为true就能避免。(但是如果有类型不同的同名Exchange 则还是会报错,那么只能删除此句或者去控制页面删除Exchange)
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
定义名为 QUEUE_NAME的队列 第二个参数是防止有同名Queue。具体函数的参数定义可以查看后面的部分API说明。
channel.queueBind(QUEUE_NAME, " helloExchange ", "bindKey");
将名为QUEUE_NAME的队列与helloExchange绑定在一起,并且Key为bindKey.这样发送到helloExchange的Routing Key为bindKey的消息就会进入此队列。
channel.basicPublish("helloExchange", "bindKey", null, message.getBytes());
发布消息到helloExchange Routing Key为bingKey。
从上面的代码我们可以看出,就跟上一节我们说明的一样,我们发送消息并没有直接给Queue,而是发送给Exchange 并且设置Routing Key 之后RabbitMQ 内部将根据Exchange 类型和与Exchange绑定的Queue设置的Binding key 将信息放到队列或者舍弃。如果没有设置Exchange 也就是传递空字符串 就会发送到默认的Exchange。
相关代码如下:
public class Recv {
private static final String QUEUE_NAME="hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setVirtualHost("myVirualHost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
DeliverCallback deliverCallback = (consumerTag,delivery)->{
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true,deliverCallback,consumerTag->{});
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
}
}
可以看出来消息消费者 之前操作也类似 就是 创建ConnectionFacotry Connection Channel 然后通过Channel做操作。
DeliverCallback deliverCallback = (consumerTag,delivery)->{
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
这里是定义收到消息后的回调函数,这里面也就是我们实际处理消息的地方。
channel.basicConsume(QUEUE_NAME, true,deliverCallback,consumerTag->{});
创建 一个消费者,这个消费者从QUEUE_NAME队列中取信息 然后进行消费。这里第二个参数是 消费者是否自动返回ack。这里暂时设置为true(这样设置 会导致 如果处理消息过程中,消费者出现了异常或者关闭了,消息会丢失,下节我们会做一些处理避免因为消费者消费失败 导致消息丢失)。
上节我们说到如果消费者已经取得消息 但是处理过程中发生了异常,会导致消息丢失的问题,这节我们就处理一下。由于生产者没有太大变化,我们这里只是将消息改为了从控制台获得。即
String message = String.join(“ ”,args);
public class Worker {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setVirtualHost("myVirualHost");
Connection conn = factory.newConnection();
Channel channel = conn.createChannel();
// channel.queueDeclare(QUEUE_NAME, false, false, false, null);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(" [x] Done");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
boolean autoAck = false; // acknowledgment is covered below
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {
});
}
private static void doWork(String task) throws InterruptedException {
for (char ch : task.toCharArray()) {
if (ch == '.')
Thread.sleep(5000);
}
}
}
可以从上面代码看出来,我们的主要区别之处主要在于 回调函数中 我们增加了模拟处理消息的方法,并且在处理完成之后 调用
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
这代表着我们告诉RabbitMQ,此条信息已经处理完成。
并且我们在声明消费者时候 使用如下代码:
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {});
设置了自动返回ack为false。
这样我们就可以避免 消费者取到信息 处理信息过程中出现异常 导致信息丢失了。
执行消息生产者 并分别传入 参数1.1.1.1 Message 和 2.2.2.2 Message
启动消费者 并在显示
而未显示[x] Done之前 关闭此消费者。
启动第二个消费者,发现 这个消费者最后打印
说明 我们的消息 1.1.1.1 Message并没有因为第一个消费者消费失败 而丢失。这是因为如果消费者没有将成功处理消息的标识返回给RabbitMQ ,那么RabbitMQ会在此消费者 关闭之后将消息重新入栈。
我们将
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {});
改为上一节的
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {});
之后重复操作 会发现信息发生了丢失。从而说明如果设置默认返回Ack的话,会有消息处理出现异常从而导致消息丢失的风险。
之前我们已经设置了AutoAck为false,这样的话 我们要注意一定要在处理完消息之后使用发送过来这条消息的channel(使用其他channel会报错!)调用channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); 的代码 将消息被消费的信息 告诉RabbitMQ 否则的话就会出现 消息被消费了 但是还是在队列中,也就一直在内存中保存此消息,更严重的是当这个消费此消息的消费者关闭之后,这些消息 由于是unacked状态的,会再次被其他消费者消费。
在RabbitMQ中 如果我们想实现 类似之前ActiveMQ 那种 topic 模式的消息处理,也就是一个发布者发布消息 能让多个订阅者收到,并且 也是订阅者只收到自己订阅后的消息,那么我们可以 创建一个exchange 其类型是 fanout,也就是不管routingkey 此exchange收到的消息 将发送到所有它绑定的队列中,那么我们订阅者发送消息给fanout的exchange时候不用指定routingkey(指定也没作用)channel.basicPublish("fanoutExchange", "",null, msgByte);
而消费者端 会定义一个临时队列,建立的临时队列
String queueName = channel.queueDeclare().getName();
将此临时队列 与 fanout类型的EXCHANGE绑定
channel.queueBind(queueName, EXCHANGE_NAME, "");
然后channel.basicConsume(queueName, true, deliverCallback, consumerTak->{});
创建消费者,这样每次启动消费者 都会建立一个临时队列 并且与之前的fanoutExchange相绑定,从而接收到发送到fanoutExchange的消息,并且关闭消费者 就会自动销毁此临时队列。
channel.queueDeclare(queue, durable, exclusive, autoDelete, arguments)
queue | 队列名称(queue如果设置为空字符串,那么会自动生成队列名称,使用queueDeclare().getName()就能在创建队列之后 获取自动生成的队列名称) |
durable | 是否持久化 |
exclusive | 是否只有一个channel和这个queue连接,如果是,那么当这个channel断开删除该队列 |
autoDelete | 是否自动删除 |
arguments | 是其他参数 可以设置队列长度限制 最大优先级数等等。 |
下面表格是对于arguments的说明。
x-max-length | 限定队列消息的最大值长度,超过指定长度会把最早的几条删除 |
x-max-length-bytes | 限定队列的最大占用空间大小 |
x-dead-letter-exchange | 将因为长度不够或者过期的消息 从队列删除 并推送到指定交换机中而不是丢弃 |
x-dead-letter-routing-key | 上面删除的消息 推送到指定交换机的指定路由键的队列中 |
x-max-priority | 优先级队列,声明队列时先定义最大优先级值(定义最大值一般不要太大),在发布消息的时候指定该消息的优先级, 优先级更高(数值更大的)的消息先被消费 |
x-queue-mode | 为lazy 则先将消息保存到磁盘上,不放在内存中,当消费者开始消费的时候才加载到内存中 |
x-expires | 当队列在指定的时间没有被访问(consume, basicGet, queueDeclare…)就会被删除 |
x-message-ttl | 设置队列中的所有消息的生存周期(统一为整个队列的所有消息设置生命周期), 也可以在发布消息的时候单独为某个消息指定剩余生存时间,单位毫秒, 类似于redis中的ttl,生存时间到了,消息会被从队里中删除,注意是消息被删除,而不是队列被删除 |
当设置exclusive 以及autoDelete 都为true,也就是这个queue只能被一个channel使用 而且channel断开后这个queue直接被删除(queue如果设置为空字符串 那么就会直接删除)
在RabbitMQ中 使用channel.queueDeclare 第二个参数是 是否持久化 。如果发布者和消费者都调用了queueDeclare (并且设置的QueueName是一样的) 那么这个参数要一致 否则会报错。ExchangeDeclare同样可以设置是否持久化。如果想RabbitMQ被异常关闭之后 消息也不会丢失,那么我们可以将queue设置为持久化,然后在发送消息的时候 将第三个参数 也就是设置消息是持久化的文本形式!!
channel.basicPublish("leiTopicExchange", "leiTopicMsg",MessageProperties.PERSISTENT_TEXT_PLAIN, msgByte);
这样 RabbitMQ关闭之后 重启 原来在消息队列的消息也不会丢失。
channel.queueBind(queue, exchange, routingKey, arguments)
主要用来绑定队列和exchange
queue | 要绑定的队列名 |
exchange | 绑定的exchange名 |
routingKey | 绑定exchange和队列的Bindkey (如果是fanout或者headers其实无所谓 可以传递空字符串) |
arguments | 绑定传递的额外参数,如exchange是headers时候 添加的键值对,从而让消息根据发送消息设置的headers与这里的headers对比,符合条件才发送到这个queue |
channel.basicPublish(exchange, routingKey, props, body)
exchange | 是发送消息到哪个exchange |
routingKey | exchange为topic或者direct时候 决定routingKey和绑定在此exchange的queue的bindingKey 是否匹配的值 |
props | 消息的其他信息 具体可以查看BasicProperties 这个类,里面可以设置信息过期等 |
body | 就是传递的消息体 |
其中的props我们可以从BasicProperties这个类中得到一些信息:
public BasicProperties(
String contentType,
String contentEncoding,
Map headers,
Integer deliveryMode,
Integer priority,
String correlationId,
String replyTo,
String expiration,
String messageId,
Date timestamp,
String type,
String userId,
String appId,
String clusterId)
{
this.contentType = contentType;
this.contentEncoding = contentEncoding;
this.headers = headers==null ? null : Collections.unmodifiableMap(new HashMap(headers));
this.deliveryMode = deliveryMode;
this.priority = priority;
this.correlationId = correlationId;
this.replyTo = replyTo;
this.expiration = expiration;
this.messageId = messageId;
this.timestamp = timestamp;
this.type = type;
this.userId = userId;
this.appId = appId;
this.clusterId = clusterId;
}
如contentType就是消息的类型如text/plain就代表是普通文本,contentEncoding就是消息的编码,而headers就决定在消息进入Type是headers的Exchange的时候,消息该发往哪个队列,priority代表消息的优先级,后续的一些在之后用到的时候会再进行讲解。
之前我们设置中 会发现 两个Worker,有时候会出现一个Worker一直执行,而另外一个Worker好像几乎不工作 这是因为RabbitMQ只是在消息进入队列的时候发送消息,而不考虑消费者未确认的数量,为了避免这种情况,我们可以在消费者使用channel.basicQos(prefetchcount) prefetchcount是整数,也就是预存计数,这就告诉rabbitMQ 一次最多向消费者发送prefetchcount条信息。(也可以认为是这个消费者最多能持有prefetchcount条信息,比如设置为3 ,如果消息够多 那么每次处理并发送ack给rabbitMQ,rabbitMQ会再给一条信息,也就是保持消费者有3条信息)如果设置为1,那么就是在worker处理并确认一条消息之前,不向这个worker发送新消息 相反他会发送给下一个不忙的工人。
RabbitMQ中消息的状态,1.准备交付(未给消费者),2.已交付但是消费者尚未确认(没发送ack)。