消息何去何从
mandatory 和immediate 是在消息传递过程中不可达目的地时返回消息给生产的开关. rabbitMQ还提供了Altername Exchange(备份交换器),用以存放未被路由成功的消息存储,防止返回给生产者.
mandatory
当出现消息无法路由到队列时,mandatory参数值不同,处理方式不同
- 为true:
RabbitMQ 将调用Basic.Return 命令将消息返回给生产者,客户端处理示例
channel.basicPublish(EXCHANGE_NAME, "", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "mandatory test".getBytes());
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
log.info(messages);
}
});
- 为false:
直接丢弃消息
immediate RabbitMQ3.0开始不支持这个参数
当交换器将消息路由到队列上,发现队列上没有消费者时,immediate 参数设置不同,处理方式不同
- 为true: RabbitMQ将会调用Basic.return将消息返回给消费者.
- 为false: 直接放入队列
备份交换器AE(Alternate Exchange)
在没有mandatory的情况下,可以使用AE实现消息存储
- 备份交换器和队列声明
Map<String, Object> map = new HashMap<>();
map.put("alternate-exchange", "myAe");
channel.exchangeDeclare("myAe", BuiltinExchangeType.FANOUT, true, false, map);
channel.queueDeclare("unroutedQueue", true, false, false, null);
channel.queueBind("unroutedQueue", "myAe", "");
备份交换器如果找不到适合的队列,消息丢失. 备份交换器的优先级高于mandatory 参数.
过期时间(TTL)
- 消息的TTL
两种方式设置消息过期时间,如果超过过期时间未被消费,消息就会变成"Dead Message",如果不设置标识消息不会过期,如果设置为0,标识如果此时可以直接投递到消费者,则直接投递,否则直接丢弃
- 设置队列属性
在声明队列时添加参数 x-message-ttl 时间单位是毫秒,如果过期,消息直接从队列中丢弃 - 对消息本身设置
在channel.basicPublish中加入expiration参数, 在投递消息时判断是否过期,如果过期则丢弃,不投递.
同时设置两者时,以时间短的为准
- 队列的TTL
通过在声明队列时添加参数 x-expires 控制队列被自动删除前处于未使用(没有任何消费者,队列没有被重新声明,并且没有被拉取过消息)状态的时间. 在重启RabbitMQ 后,队列的过期时间会被重算.
死信队列
DLX(Dead-Letter-Exchange) ,死信交换器,当消息在队列中变成死信之后,它可以被重新发送到另外一个交换器中,这个交换器就是DLX,和DLX绑定的队列称为死信队列.
死信来源:
- 消息被拒绝(Basic.Reject/Basic.Nack), 并且requeue参数为false
- 消息过期
- 队列达到最大长度
可以用过在声明队列时添加参数 x-dead-letter-exchange 为队列添加DLX.
为DLX指定路由键 通过添加参数 x-dead-letter-routing-key .
channel.exchangeDeclare("exchange.dlx", "direct", true);
channel.exchangeDeclare("exchange.normal", "fanout", true);
Map<String, Object> queueMap = new HashMap<>();
queueMap.put("x-message-ttl", 1000);
queueMap.put("x-dead-letter-exchange", "exchange.dlx");
queueMap.put("x-letter-routing-key", "routingkey");
channel.queueDeclare("queue.normal", true, false, false, queueMap);
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());
延迟队列
使用场景: 支付场景中的30分钟内支付. 只能设备控制,指定时间后开始运行.
RabbitMQ本身是没有这种功能的,通过前面介绍的DLX和TTL模拟出队列的功能.
优先级队列
优先级较高的消息具备优先被消费的特权,只有在broken中出现消息堆积的时候才会有效果,如果不出现消息堆积,设置优先级毫无意义.
通过设置队里的x-max-priority
Map<String, Object> priMap = new HashMap<>();
priMap.put("x-max-priority", 10); //队列里设定的最大优先级
channel.queueDeclare("queue.priority", true, false, false, priMap);
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.priority(5); //消息的优先级,
AMQP.BasicProperties properties = builder.build();
channel.basicPublish("exchange_priority", "rk_priority", properties, "message".getBytes());
RPC实现
Remote Procedure Call 的简称,远程过程调用.
简单实现:
//简单实现
String callBackQueueName = channel.queueDeclare().getQueue();
AMQP.BasicProperties props = new AMQP.BasicProperties().builder().replyTo(callBackQueueName).build();
channel.basicPublish("","rpc_queue",props,messages.getBytes());
通过设置replyTo设置一个回调队列/ correlationId 用来关联request和RPC之后的回复(response). 以上缺陷: 为每个RPC请求创建一个回调队列,非常低效.
官方demo:
package com.example.demo.rabbitmq;
import com.rabbitmq.client.*;
import com.sun.org.apache.bcel.internal.generic.FADD;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @program: demo
* @description:
* @author: Jiliang.Lee
* @create: 2019-09-16 17:31
**/
@Slf4j
public class RpcServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
log.info("[x] Awaiting RPC requests");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
AMQP.BasicProperties build = new AMQP.BasicProperties()
.builder()
.correlationId(properties.getCorrelationId())
.build();
String response = "";
try {
String message = new String(body, "UTF-8");
int n = Integer.parseInt(message);
System.out.println( " [ . ] fib( " + message + " ) " );
response += fib(n);
}catch (RuntimeException e){
log.error(e.getMessage());
}finally {
channel.basicPublish("", properties.getReplyTo(), build, response.getBytes("UTF-8"));
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
}
private static int fib(int n) {
if(n==0) return 0;
if(n==1) return 1;
return fib(n - 1) + fib(n - 2);
}
}
package com.example.demo.rabbitmq;
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.TimeoutException;
/**
* @program: demo
* @description:
* @author: Jiliang.Lee
* @create: 2019-09-16 18:01
**/
@Slf4j
public class RPCClient {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
private String replyQueueName;
private QueueingConsumer consumer;
public RPCClient() throws IOException, TimeoutException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection();
channel = connection.createChannel();
replyQueueName = channel.queueDeclare().getQueue();
consumer = new QueueingConsumer(channel);
channel.basicConsume(replyQueueName, true, consumer);
}
public String call(String message) throws IOException, InterruptedException {
String response = null;
String corrId = UUID.randomUUID().toString();
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
channel.basicPublish("", requestQueueName, properties, message.getBytes());
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
response = new String(delivery.getBody());
break;
}
}
return response;
}
public void close() throws IOException {
connection.close();
}
public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
RPCClient fibRpc = new RPCClient();
log.info("[x] Requesting fib(30)");
String call = fibRpc.call("30");
log.info("[.] Got '" + call + "'");
fibRpc.close();
}
}
持久化
RabbitMQ持久化分三个部分
- 交换器 durable 属性为true, 如果不是true不会导致数据丢失
- 队列 durable 属性为true,如果不是true会导致数据丢失,队列元数据丢失.但是不能保证内部的消息一定不丢失.
- 消息 消息持久化通过消息的投递模式 BasicProperties中的deliveryModel属性设置为2即可实现消息的持久化. MessageProperties.PERSISTENT_TEXT_PLAIN封装了这个属性. 持久化需要和队列同事设置为持久化状态.
这并不能保证消息一定不会丢,还需要确认机制的配合.但是还是不能保证消息一定不会丢,还有可能在服务宕机的时候丢数据,需要配合RabbitMQ的镜像队列机制.相当于配置了节点副本.这样能极大概率的提供消息不会丢失. 可以通过引入事务机制保证消息被正确的发送到RabbitMQ中,前提是交换器能正确的路由消息.
生产者确认
如何确保消息被成功的发送到RabbitMQ服务器
- 通过事务机制实现
RabbitMQ中与事务机制相关的方法有三个:channel.txSelect/channel.txCommit/channel.txRollback.分别对应着事务的开启,提交,回滚。
示例:
channel.txSelect();//信道设置为事务模式
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,MessageProperties.PERSISTENT_TEST_PLAIN,"transaction message".getBytest());
channel.txCommit();//提交事务
catch(Excepion (IOException e){
channel.txRollback();//回滚事务
}
使用事务会大大的降低RabbitMQ的性能。所以出现了下面这个方案。
- 通过发送发确认(publish confirm)机制实现。
通过设置信道为confirm模式,该信道上的每个消息都会被分配一个唯一的ID,从1开始。一旦消息被投递到所有匹配的队列之后 RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),如果是需要持久化的消息,则会在持久化完成后才返回这个唯一id, 在deliveryTag中。basicAck还有一个参数multiple.表示到这个序号之前的所有消息都已经得到了处理。
示例:
channel.confirSelect(); //将信道设置为confirm模式
//发送消息
if(!channel.waitForConfirms()){
//do something else .....
}
但是确认的动作还是同步的,和事务模式基本相同了,所有又衍生除了另外两种解决方案
- 批量confirm方法: 发送一批消息后,再调用channel.waitForConfirms方法,等待服务器的确认返回。
- 异步confirm方法: 提供一个回调方法,服务端确认了一条或者多条消息后客户端会回调这个方法进行处理。
批量确认模式中如果出现Basic.Nack或者超时情况时,客户端需要将这一批的消息全部重发。重复消息会过多。如果消息异常过多,导致重发消息太多反而会导致性能下降。
示例:
package com.example.demo.rabbitmq;
import com.rabbitmq.client.Address;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class confirmModelDemo {
private static final String QUEUE_NAME = "queue_demo";
private static final String IP_ADDRESS = "127.0.0.1";
private static final int PORT = 5672;
private static final int BATCH_COUNT=100;
public static void main(String[] args) throws IOException, TimeoutException {
Address[] addresses = new Address[]{new Address(IP_ADDRESS, PORT)};
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection(addresses);
Channel channel = connection.createChannel();
channel.confirmSelect();
int msgCount=0;
while (true) {
//发送消息
//缓存消息
if(++msgCount >=BATCH_COUNT){
msgCount=0;
try {
if (channel.waitForConfirms()) {
//清空缓存中的消息
}
} catch (InterruptedException e) {
e.printStackTrace();
//将缓存中的消息重新发送。
}
}
}
}
}
异步confirm:
通过在channel接口中提供的addConfirmListener方法可以添加ConfirmListener这个回调接口。
这个接口中有两个方法:
- handleAck
- hangleNack
分别用于处理RabbitMQ回传的Basic.Ack和 Basic.Nack,这两个方法中包含有一个参数deliveryTag (在发送确认模式下用来标记消息唯一有序序号)。通过为每一个信道维护一个“unconfirm”的消息序号集合。每发送一条消息 元素+1,每当调用confirmListener中的handleAck时,从集合中删除掉相应的一条未确认记录。
示例:
package com.example.demo.rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.SortedSet;
import java.util.concurrent.TimeoutException;
public class confirmModelDemo {
private static final String QUEUE_NAME = "queue_demo";
private static final String IP_ADDRESS = "127.0.0.1";
private static final int PORT = 5672;
private static final int BATCH_COUNT=100;
public static void main(String[] args) throws IOException, TimeoutException {
Address[] addresses = new Address[]{new Address(IP_ADDRESS, PORT)};
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
Connection connection = connectionFactory.newConnection(addresses);
Channel channel = connection.createChannel();
channel.confirmSelect();
// int msgCount=0;
// while (true) {
// //发送消息
// //缓存消息
// if(++msgCount >=BATCH_COUNT){
// msgCount=0;
// try {
// if (channel.waitForConfirms()) {
// //清空缓存中的消息
// }
// } catch (InterruptedException e) {
// e.printStackTrace();
// //将缓存中的消息重新发送。
// }
// }
// }
SortedSet confirmSet=null;
channel.addConfirmListener(new ConfirmListener()
@Override
public void handleAck(long l, boolean b) throws IOException {
if (b) {
confirmSet.headSet(l - 1).clear();
} else {
confirmSet.remove(l);
}
}
@Override
public void handleNack(long l, boolean b) throws IOException {
if (b) {
confirmSet.headSet(l - 1).clear();
} else {
confirmSet.remove(l);
}
}
});
//发送消息
while (true) {
long nextSeqNo = channel.getNextPublishSeqNo();
//发送消息
// channel.basicPublish();
confirmSet.add(nextSeqNo);
}
}
}
消费端要点介绍
- 消息分发
- 消息顺序性
- 弃用QueueingConsumer
消息分发:
主要设置channel的basicQos 数值和global,
global为true表示整个信道上所允许的最大未确认消息数量。
为false,表示每个消费者的最大未确认消息数量。
消息顺序性
RabbitMQ很难保证消息的顺序性,建议通过在消息体内维护顺序标识,业务自己控制消息顺序。
QueueingConsumer被弃用了,存在内存溢出,假死,替代方案是DefaultConsumer类
消息传输保障
传输保障三个层级
- At most once: 最多一次,可能丢失消息,但是绝对不会重复。
- At least once: 最少一次, 消息基本不会丢失,但是可能会重复传输。
- Exactly once: 恰好一次。 每条消息肯定会被传输一次。
RabbitMQ支持前两个