在上篇文章中我们介绍了RabbitMQ的三种常见交换器类型的使用。今天我们在详细聊一聊生产者和消费者如何保证消息的准确到达。
RabbitMQ在消费消息时分为两种模式:Push模式和Pull模式。Push模式采用的是Basic.Consume进行消费,而Pull模式调用的Basic.Get进行消费。
我们常用的就是使用推模式:
消费端手动确认
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
try {
// 当我们正常消息,手动ack后,消息就会从mq中删除
// multiple为false表示一条一条确认
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
// 发生异常,发送nack,根据requeue参数来决定是将消息丢弃开始还是再重新放回队列
channel.basicNack(envelope.getDeliveryTag(), false, false);
System.out.println("1111");
}
log.info("get message, routingKey: {}, message: {}", envelope.getRoutingKey() ,message);
}
};
channel.basicConsume(queueName, false, consumer);
//public String basicConsume(String queue, boolean autoAck, Consumer callback)
在上面中autoAck为false,在接收到消息后手动ack,即channel.basicAck。这对消费者来说也是非常重要的,可以防止消息不必要的丢失。
生产者确认
在RabbitMQ的使用过程中,我们可以通过消息持久化操作来解决因为服务器异常崩溃导致消息丢失的问题,但是如果生产者将消息发送后,消息到底有没有到达服务器呢,如果不进行配置,是无法返回信息到生产者的。
RabbitMQ提供了两种方式:
(1)通过事务机制
(2)通过发送方确认机制
事务机制是只有消息成功被RabbitMQ接收,事务才能提交成功,否则便可捕获异常并进行回滚,进行消息重发。会导致RabbitMQ的性能极大损耗,所以一般采用发送确认机制。
public class ConfirmProducer {
public static final String EXCHANGE_NAME = "confirm_exchange";
public static void main(String[] args)throws Exception {
Connection connection = Utils.getRabbitmqConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 启用发布者确认模式
channel.confirmSelect();
String routingKey = "error";
for (int i = 0; i < 10; i++) {
String message = "hello rabbitmq " + i;
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());
// 一条一条确认,返回为true,则表示发送成功
if (channel.waitForConfirms()) {
log.info("send success, routingKey: {}, message: {}", routingKey ,message);
} else {
log.info("send fail, routingKey: {}, message: {}", routingKey ,message);
}
}
channel.close();
connection.close();
}
}
channel.confirmSelect();
此处将信道置为publisher confirm模式
channel.waitForConfirms()
channel.waitForConfirms(long timeout)
如果信道没有开启publisher confirm模式,在调用waitForConfirms方法时会报出java.lang.IllegalStateException。
注意:事务机制和publisher confirm机制是互斥的,不能共存。
public class AsyncConfirmProducer {
public static final String EXCHANGE_NAME = "async_confirm_exchange";
public static void main(String[] args) throws Exception {
Connection connection = Utils.getRabbitmqConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 启用发布者确认模式
channel.confirmSelect();
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
log.info("send message error, replyCode: {}, replyText: {}, exchange: {}, routingKey: {}",
replyCode, replyText, exchange, routingKey);
}
});
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
log.info("handleAck, deliveryTag: {}, multiple: {}", deliveryTag, multiple);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
log.info("handleNack, deliveryTag: {}, multiple: {}", deliveryTag, multiple);
}
});
String routingKey = "error";
for (int i = 0; i < 10; i++) {
String message = "hello rabbitmq " + i;
channel.basicPublish(EXCHANGE_NAME, routingKey, true, null, message.getBytes());
log.info("send message, routingKey: {}, message: {}", routingKey, message);
}
}
}
在publisher confirm确认中有两种方法:
批量confirm方法,没发送一批消息,调用channel.waitForConfirms方法,等待服务器确认返回
异步confirm方法,提供了一个回调方法,服务端确认了一条或多条消息后,会调用回调方法进行处理
异步confirm方法中,channel提供了addConfirmListener方法,可以添加ConfirmListener这个回调接口,包含两个方法:handleAck和handleNack,分别用来处理RabbitMQ回传的Basic.Ack和Basic.Nack,这两个方法中包含一个参数deliverTag(消息的有序序号),我们需要为每一个信道设置序号集合,每发一条消息,集合元素加1,每调用handlerAck方法,从集合中删除相应的一条(multiple设置为false),或者多条(multiple设置为true)
设置过期时间
在RabbitMQ中设置过期时间(TTL),一般常用的有两种方法。
第一种方法是通过设置队列属性,这样队列中所有的消息就都会有相同的过期时间。第二种是对消息本身进行单独设置,每条消息的TTL可以不一样。
设置队列属性是在channel.queueDeclare方法中加入x-message-ttl参数,单位为毫秒
Map<String.Object> args = new HashMap();
args.put("x-message-ttl",6000)
channel.queueDeclare(queueName, durable, exclusive, autoDelete, args);
每条消息设置TTL的方法是在channel.basicPublish方法中加入expiration的属性参数,单位为毫秒
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.deliveryMode(2);//持久化消息
builder.expiration("6000");
AMQP.BasicProperties properties = builder.build();
channel.basicPublish(EXCHANGE_NAME, routingKey, properties, message.getBytes());
死信队列
死信队列(DLX),也可以成为死信邮箱。当消息在一个队列中变为死信之后,他能重新被发送到另一个交换器中,这个交换器就是(DLX),绑定DLX的队列就叫死信队列。
消息造成死信的原因一般为以下几种情况:
(1)消息被拒绝(Basic.Reject/ Basic.Nack),并且设置requeue参数为false
(2)消息过期
(3)队列达到最大长度
DLX也是一个普通交换器,和其他的交换器没有区别。实际上就是在某个队列中设置属性,当队列找那个存在死信时,RabbitMQ就会自动将这个消息重新发布到设置的DLX上,从而被路由到另一个队列,也就是死信队列。
channel.exchangeDeclare("exchange.dlx","direct",true);
channel.exchangeDeclare("exchange.normal","fanout",true);
Map<String,Object> args1 = new HashMap<>();
args1.put("x-message-ttl",1000);
args1.put("x-dead-letter-exchange","exchange.dlx");
args1.put("x-dead-letter-routing-key","routingkey");
channel.queueDeclare("queue.normal",true,false,false,args1);
channel.queueBind("queue.normal","exchange.normal","");
channel.queueDeclare("queue.dlx",true,false,false,null);
channel.queueBind("queue.dlx","exchange.dlx","routingkey");
channel.basicPublish("exchange.normal","rk",MessageProperties.PERSISTENT_TEXT_PLAIN,"dlx".getBytes());
生产者发送路由键为“rk”的消息,经过交换器exchange.normal并进入队列queue.normal中。队列设置过期时间10s,10s内消费者没有消费消息,被丢给交换器exchange.dlx,此时找到队列queue.dlx,将消息存入队列queue.dlx中。
延迟队列
在实际项目中有很多用到了延迟队列的场景比如:订单系统,用户下订单后通常有30分钟的时间进行支付,如果30分钟内没有支付成功,订单将进行异常处理,这时就可以使用延迟队列来处理这些订单。
延迟一般可以听你通过上面的设置过期时间和死信队列进行实现。
消费者消息分发
在RabbitMQ队列拥有多个消费者时,队列收到的消息将以轮询的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者。但是这种情况有时也不是很友好,如果有些消费者任务范中锋来不及消费,有些比较空闲,就会造成整体应用的吞吐量下降。此时就需要用到channel.basicQos方法。这个方法可以限制信道上的消费者所能保持的最大未确认消息的数量。
接收消息的顺序性
所谓的消息顺序性也就是消费者接收到的消息和发送者发布的消息顺序是一致的。其实在很多情况下,RabbitMQ是无法保证消息的顺序性的。比如使用了事务机制,发送消息之后遇到异常,进行了事务回滚,就要重新补偿发送消息。如果启用publisher confirm时,发生超时、中断,或者接收到Basic.Nack命令,需要补偿发送时,与事务机制一样会错序。还有生产者设置了延迟队列,消息的顺序必然和生产者发送的消息不一致。
如果要保证消息的顺序性,需要业务方在使用RabbitMQ之后进一步处理,比如在消息体内添加全局有序标志等。