文章目录
保证消息100%投递
如何保证生产者可靠性投递消息
方案一 : 消息入库,对消息状态进行打标。
方案二 : 消息延迟投递,进行二次确认,回调检查
confirm确认消息
Return消息机制
消费端限流策略
消费端ACK与重回队列
消费端的手动ACK和NACK
消费端重回队列
TTL队列/消息
死信队列
死信队列设置
保证消息100%投递
如何保证生产者可靠性投递消息
保证消息成功发出
保障MQ节点成功接受
发送端收到MQ节点(Broker)确认应答
对消息进行补偿机制
方案一 : 消息入库,对消息状态进行打标。
step1:业务数据入库,消息数据入库(两次操作DB 高并发可能会受影响)
step2:producer向MQ发送消息
step3:producer异步监听MQ确认消息投递到队列
step4:producer监听到确认送达后,修改消息数据库状态
step5:补偿机制,通过定时任务轮询扫描消息表中,未确认的消息。
step6:补偿机制,将未确认的消息从新发送。
step7:补偿机制,记录重试次数,达到一定数值后,将消息状态该为失败。(通过人工处理)
方案二 : 消息延迟投递,进行二次确认,回调检查
异步操作,性能更高
整个流程需要三个消息队列来完成。
step1:先业务数据入库,然后发送业务消息。
step2:延迟发送第二条check消息。(延迟时间根据业务来确定)
step3:消费端监听队列并消费消息。
step4:消费端发送confirm消息。(此消息为从新发送的新消息在不同的队列中,非ack签收)
step5:回调服务监听消费端的confirm消息队列,并将消息进行持久化存储。
step6:回调服务监听check消息队列,查询数据库,如果业务消息没有消费,回调服务将告诉producer从新发送业务消息
confirm确认消息
消息的确认,是指生产者投递消息后,如果Broker收到消息,则会给我们生产者一个应答。
生产者进行接受应答,用来确认这条消息是否正常的发送到Broker,这种方式是消息可靠性投递的核心保障
第一步:在channel上开启消息确认模式:channel.confirmSelect();
第二步:在channel上添加监听,addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续操作。
//生产者
//省略 创建连接和channel
//打开消息确认模式
channel.confirmSelect();
String exchange = "test.confirm.exchange";
String routingKey = "confirm.routing-key";
String message = "hello rabbitmq";
channel.basicPublish(exchange, routingKey, null, message.getBytes());
//添加确认监听
channel.addConfirmListener(new ConfirmListener() {
//消息成功发送回调
public void handleAck(long deliverTag, boolean multiple) throws IOException {
System.err.println("----发送成功---");
}
//消息发送失败回调
public void handleNack(long deliverTag, boolean multiple) throws IOException {
//队列满、磁盘满 等等情况
System.err.println("----发送失败---");
}
});
//消费者
//省略 创建连接和channel
String queueName = "confirm-queue";
String exchange = "test.confirm.exchange";
String routingKey = "confirm.#";
//声明交换机
channel.exchangeDeclare(exchange, "topic");
//声明队列
channel.queueDeclare(queueName, false, false, false, null);
//队列、交换机、routing key 三者绑定
channel.queueBind(queueName, exchange, routingKey);
DefaultConsumer defaultConsumer = 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");
System.err.println(message);
}
};
channel.basicConsume(queueName, true, defaultConsumer);
}
Return消息机制
Return Listener 用于处理一些不可路由的消息
我们的生产者,通过指定一个Exchange和Routing key,把消息送达到某个消息队列,然后我们的消费者监听队列,进行消费操作。
但是在某些情况下,如果我们在发送消息的时候,当前的Exchange不存在或者指定的Routing key路由不到,此时我们需要监听这种不可达消息,就是用Return Listener。
Mandatory: 如果为true,则监听器会收到路由不可达的消息,然后进行处理,如果为false,Broker会自动删除消息。
//生产者
//省略 创建连接和channel
String exchange = "test.return.exchange";
String routingKey = "return.routing-key";
String message = "hello rabbitmq";
//声明交换机 不绑定队列 触发return 回调
channel.exchangeDeclare(exchange, "topic");
//添加 return 监听器
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyTest, String exchange, String routingKey,
AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
System.err.println("replyCode: " + replyCode);
System.err.println("replyTest: " + replyTest);
System.err.println("exchange: " + exchange);
System.err.println("routingKey: " + routingKey);
System.err.println("basicProperties: " + basicProperties);
System.err.println("body: " + new String(body, "UTF-8"));
}
});
//第三个参数 是否打开 return 机制
channel.basicPublish(exchange, routingKey, true,null, message.getBytes());
}
消费端限流策略
什么是消费端限流?
假设一个场景,首先,我们RabbitMQ服务器上有上万条未处理的消息,随便打开一个消费端,就会有巨量消息推送过来,压垮客户端。
RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动签收消息的前提下,如果一定数目的消息(通过基于consumer或者channel设置Qos的值)未被确认前,不进行新消息的消费。
在自动签收消息的情况下 不生效。
void BasicQos(int prefetchSize,short prefetchCount,boolean global);
prefetchSize:消息的大小限制,0 不限制。
prefetchCount:推送的消息数目。
global:true\false 是否将以上设置 应用于channel上
简单点说:就是上面限制是channel级别还是consumer级别
//生产者
//省略 创建连接和channel
String exchange = "test.qos.exchange";
String routingKey = "qos.routing-key";
String message = "hello rabbitmq";
for (int i = 0; i < 9; i++) {
channel.basicPublish(exchange, routingKey, null, message.getBytes());
}
channel.close();
connection.close();
1
2
3
4
5
//消费者
//省略 创建连接和channel
String queueName = “qos-queue”;
String exchange = “test.qos.exchange”;
String routingKey = “qos.routing-key”;
//声明交换机
channel.exchangeDeclare(exchange, “topic”);
//声明队列
channel.queueDeclare(queueName, false, false, false, null);
//队列、交换机、routing key 三者绑定
channel.queueBind(queueName, exchange, routingKey);
// 限流 第一件事:autoAck 设置为false
channel.basicQos(0, 3, false);
channel.basicConsume(queueName, false, 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");
System.err.println(message);
//第二个参数 是否批量签收
channel.basicAck(envelope.getDeliveryTag(), true);
}
});
消费端ACK与重回队列
ACK:签收,NACK不签收
消费端的手动ACK和NACK
消费端进行消费是,如果业务出现异常NACK消息,经历三到四次重试后依然异常,手动ACK,然后进行补偿。
消费端重回队列
为了将没有处理成功的消息重新投递到Queue的队尾中。
实际项目中,应用较少。
//生产者
//省略 创建连接和channel
String exchange = "test.ack.exchange";
String routingKey = "ack.routing-key";
for (int i = 0; i < 5; i++) {
Map<String, Object> headers = new HashMap<>();
headers.put("index", i);
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.deliveryMode(2) //持久化
.headers(headers)
.build();
String message = "hello rabbitmq " + i;
channel.basicPublish(exchange, routingKey, properties, message.getBytes());
}
//消费者
//省略 创建连接和channel
String queueName = "ack-queue";
String exchange = "test.ack.exchange";
String routingKey = "ack.routing-key";
//声明交换机
channel.exchangeDeclare(exchange, "topic");
//声明队列
channel.queueDeclare(queueName, false, false, false, null);
//队列、交换机、routing key 三者绑定
channel.queueBind(queueName, exchange, routingKey);
//必须手工签收 关闭autoACK
channel.basicConsume(queueName, false, 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");
System.err.println(message);
if ((Integer) properties.getHeaders().get("index") == 0) {
//第三个参数 是否重回队列
channel.basicNack(envelope.getDeliveryTag(), false, true);
}else{
//第二个参数 是否批量签收
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
});
TTL队列/消息
TTL 是 Time To Live 的缩写,也就是生存时间
RabbitMQ支持消息的过期时间设置,在消息发送时指定
RabbitMQ支持队列的过期时间设置,从消息入队时开始计算,只要超过队列的超时时间配置,消息自动清除
死信队列
消息变成死信有以下几种情况
消息被拒绝(basic.reject/basic.nack)并且requeue = false。
消息TTL过期
队列达到最大长度
当消息在一个队列中变成私信之后,它会被重新public到另外一个Exchange中,这个Exchange就是DLX(私信队列)
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它可以在任何的队列指定,实际上就是设置队列的属性。
当这个队列中有私信时,RabbitMQ就会自动的将这个消息重新发送到设置的Exchange上去,进而被路由到另一个队列
死信队列设置
首先需要设置私信队列的Exchange和queue,然后进行绑定。
例如:
Exchange:dlx.exchange
Queue:dlx.queue
Routing key:#
然后我们正常声明Exchange、Queue、binding,只不过我们需要在队列上加上一个参数即可:arguments.put(“x-dead-letter-exchange”,“dlx.exchange”)
简单点说:就是在队列上设置私信队列就可以。
//生产者
//省略 创建连接和channel
String exchange = “test.dlx.exchange”;
String routingKey = “dlx.routing-key”;
//进行私信队列的声明
channel.exchangeDeclare("dlx.exchange", "topic", true, false, false, null);
channel.queueDeclare("dlx.queue", false, false, false, null);
channel.queueBind("dlx.queue", "dlx.exchange", "#");
//声明普通队列和交换机
String queueName = "test.dlx-queue";
channel.exchangeDeclare(exchange, "topic");
//-----------------------设置队列的私信队列---------------------------------------
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "dlx.exchange");
//这个arguments属性要设置到声明队列上
channel.queueDeclare(queueName, false, false, false, arguments);
//-----------------------设置队列的私信队列---------------------------------------
channel.queueBind(queueName, exchange, routingKey);
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.expiration("1000") //设置过期时间 不启动消费者 使消息变为私信
.build();
String message = "hello rabbitmq";
channel.basicPublish(exchange, routingKey,properties, message.getBytes());
channel.close();
connection.close();