这部分学习客户端如何和消息队列交互,如何发送消息、接收消息等,官方文档如下
https://www.rabbitmq.com/tutorials/tutorial-one-java.html
0 Java程序准备
我们使用RabbitMQ官方提供的一个客户端包,添加maven依赖,后两个是日志框架,不加会报个警告,也不影响功能
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
1 发送、接收消息
现在声明一个新队列,并发送Hello World消息
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("服务器IP"); // 设置服务器
factory.setUsername("admin"); // 账号
factory.setPassword("password"); // 密码
try (Connection connection = factory.newConnection(); // 创建连接和通道
Channel channel = connection.createChannel()) {
channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 声明队列
String message = "Hello World";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); // 创建新生产者,发送消息
System.out.println(" [x] Sent '" + message + "'");
}
}
}
现在web页面上能看见这个名为hello的队列
消息已经成功在队列里,现在写一个消费端把消息取出来
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("服务器IP"); // 设置服务器
factory.setUsername("admin"); // 账号
factory.setPassword("password"); // 密码
Connection connection = factory.newConnection(); // 创建连接和通道
Channel channel = connection.createChannel();
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
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 -> {}); // 创建新消费者,等待消息
}
}
2 消费可靠性
生产者和消费者都需要一个机制来确保消息传输的可靠性,在协议AMQP 0-9-1中称为confirm(生产者)和acknowledgement(消费者)
消息队列给消费者交付一个消息时,需要考虑什么情况下可以判断消息已经成功发送。协议AMQP 0-9-1认为消费者调用了basic.consume方法或者basic.get API就是消息成功发送的标志。RabbitMQ交付消息时,有两种acknowledgement模式。第一种模式叫手工确认,客户端接收消息后,发送一个确认消息给RabbitMQ,然后RabbitMQ才会认为交付完成,删除消息;第二种模式叫自动确认,当RabbitMQ把消息发送出去后即认为消息成功交付,原消息会被删除,如果此时客户端链接断开或者客户端应用程序狗带,这个消息就丢失了,所以自动确认是不安全的一种模式。此外手工确认模式对通道的预取数量有限制(QoS),自动确认模式没有限制,可能会造成一些性能问题,比如客户端赶不上消息交付速度,过多的未确认tag会占节点内存。消息的发送和确认必须在同一个通道中完成
1.1中的代码为自动确认模式
手工确认模式下,如果出现TCP连接断开、消费者程序挂掉等各种异常,那么未被确认的消息会重新入队。因此消费者必须具备处理重复消息的能力,包括一个消费者收到另一个消费者之前接收到的消息这种情况。重新发送的消息redeliver这个属性为true
手工确认有正负之分,正确认表示消息成功接收处理,可以删除该消息,负确认表示消息还没有被处理,但同样可以删除该消息
2.1 手工正确认
手工正确认调用的方法是basic.ack,第一个参数表示要确认的deliveryTag,第二个参数表示是否允许批确认。一个手工正确认的消费者代码如下,开启后,多点几次发送,deliveryTag分别是1、2、3这样递增的正整数。deliveryTag是用来在一个通道内区分各个分发消息的标志
import com.rabbitmq.client.*;
import java.io.IOException;
public class RecvWithAck {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("服务器IP"); // 设置服务器
factory.setUsername("admin"); // 账号
factory.setPassword("password"); // 密码
boolean autoAck = false; // 不使用自动确认
Connection connection = factory.newConnection(); // 创建连接和通道
Channel channel = connection.createChannel();
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
@Override
// 处理消息
public void handleDelivery(String s, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
long deliveryTag = envelope.getDeliveryTag();
channel.basicAck(deliveryTag, false);
System.out.println("deliveryTag is " + deliveryTag);
System.out.println("message is " + new String(bytes));
}
};
channel.basicConsume(QUEUE_NAME, autoAck, "a-consumer-tag", consumer); // 创建新消费者,等待消息
}
}
手工确认也可以批量完成,跟TCP的确认其实有一点类似,例如现在有5、6、7、8四个未确认的tag,这时客户端确认tag 8并设置multiple为true,RabbitMQ认为5、6、7、8都被确认;如果设置multiple为false,只有8被确认,5、6、7不被确认
java实现非常简单,basicAck第二个参数就是multiple,我们先写一个测试,只有delivery tag为4才发送确认信息,启动消费端后,发送4个消息
import com.rabbitmq.client.*;
import java.io.IOException;
public class RecvWithMulAck {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("服务器IP"); // 设置服务器
factory.setUsername("admin"); // 账号
factory.setPassword("password"); // 密码
boolean autoAck = false; // 不使用自动确认
Connection connection = factory.newConnection(); // 创建连接和通道
Channel channel = connection.createChannel();
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
@Override
// 处理消息
public void handleDelivery(String s, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
long deliveryTag = envelope.getDeliveryTag();
if (deliveryTag == 4) { // 只单个确认tag 4
channel.basicAck(deliveryTag, false);
}
System.out.println("deliveryTag is " + deliveryTag);
System.out.println("message is " + new String(bytes));
}
};
channel.basicConsume(QUEUE_NAME, autoAck, "a-consumer-tag", consumer); // 创建新消费者,等待消息
}
}
消费端接收到了4个消息
但是去web页面上看队列的情况,仍有3个消息在等待接收
现在修改程序
channel.basicAck(deliveryTag, true); // 开启批确认
同样接收完4个消息,去web页面上看,队列是空的,所有消息发送已经被确认
2.2 手工负确认
负确认有两种方法,basic.reject和basic.nack,basic.reject不包含multiple这个参数,所以为了支持批处理RabbitMQ扩展了协议,有了basic.nack。负确认方法还有另外一个重要的参数是入队requeue,如果这一项为true,表示消费者无力处理这条消息,将这条消息重新入队等待其他消费者处理
现在写一个负确认时丢弃消息的消费者,运行后,发送若干条消息,消息全部被接收,消息队列内没有残余消息
所以不带入队的负确认在实际效果上和正确认是相同的
import com.rabbitmq.client.*;
import java.io.IOException;
public class RecvWithNegative {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("服务器IP"); // 设置服务器
factory.setUsername("admin"); // 账号
factory.setPassword("password"); // 密码
boolean autoAck = false; // 不使用自动确认
Connection connection = factory.newConnection(); // 创建连接和通道
Channel channel = connection.createChannel();
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
@Override
// 处理消息
public void handleDelivery(String s, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
long deliveryTag = envelope.getDeliveryTag();
channel.basicReject(deliveryTag, false); // 丢弃消息
System.out.println("deliveryTag is " + deliveryTag);
System.out.println("message is " + new String(bytes));
}
};
channel.basicConsume(QUEUE_NAME, autoAck, "a-consumer-tag", consumer); // 创建新消费者,等待消息
}
}
现在看一下入队的负确认,只要修改一个地方,启动后,发送一条消息,会看到十分神奇的情况
channel.basicReject(deliveryTag, true); // 消息重新入队
因为只有一个消费者,而这名消费者表示自己无力处理这条消息,消息不断重新入队,又不断被这个消费者读出,就造成了循环取到消息的情况,同时队列中有一条消息残余
最后负确认也可以批量完成,要使用basic.nack,其第二个参数就是multiple,第三个参数是requeue
实验效果和正确认是一样的,接收到4条消息,且队列无残余
import com.rabbitmq.client.*;
import java.io.IOException;
public class RecvWithMulNegativeAck {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("服务器IP"); // 设置服务器
factory.setUsername("admin"); // 账号
factory.setPassword("password"); // 密码
boolean autoAck = false; // 不使用自动确认
Connection connection = factory.newConnection(); // 创建连接和通道
Channel channel = connection.createChannel();
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
@Override
// 处理消息
public void handleDelivery(String s, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
long deliveryTag = envelope.getDeliveryTag();
if (deliveryTag == 4) { // 接收到第四个消息后,批量确认
channel.basicNack(deliveryTag, true, false);
}
System.out.println("deliveryTag is " + deliveryTag);
System.out.println("message is " + new String(bytes));
}
};
channel.basicConsume(QUEUE_NAME, autoAck, "a-consumer-tag", consumer); // 创建新消费者,等待消息
}
}
3 生产可靠性
调用发送消息的接口完成,并不代表消息成功到达了服务器。协议中唯一保证发送成功的方法就是事务,但事务会将吞吐量降低250倍。RabbitMQ用confirm来解决这个问题。使用confirm.select来开启confirm模式,然后开始计数,计数从1开始,1对应的是confirm.select这条信息,服务器每接收到一条信息,就调用basic.ack发送确认,这个确认和消费端的确认很类似,也可以进行批确认等操作。如果服务器除了某些问题,会发送负确认basic.nack
对于routable message来说,消息进入队列后,服务器才会发送basic.ack,例如在镜像队列情况下,所有镜像队列都接收到这个消息才会发送确认,持久化的消息要写到磁盘才发送确认。绝大部分情况下服务器会按照顺序发出确认,但确认可能不按顺序到达
最简单的confirm代码如下,当然这样写效率很低,异步处理confirm信息将提高吞吐量
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class SendWithConfirm {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("服务器IP"); // 设置服务器
factory.setUsername("admin"); // 账号
factory.setPassword("password"); // 密码
try (Connection connection = factory.newConnection(); // 创建连接和通道
Channel channel = connection.createChannel()) {
channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 声明队列
channel.confirmSelect(); // 设置通道为confirm模式
String message = "Hello World";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); // 创建新生产者,发送消息
if (!channel.waitForConfirms()) { // 等待confirm
System.out.println(" [x] Sent failed");
} else {
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}