一、如何保证消息100%投递成功
①保障消息成功发出
②保障MQ节点成功接收
③发送端收到MQ节点(Broker)确认应答
④完善的消息进行补偿机制(因为前面可以能哪一步出现网络问题导致失败)
解决方案如下:
1、消息入库,然后发送成功后修改状态,外加定时任务轮询发送失败的消息
生产者在发送消息前,先将业务信息和要发送的消息分布入库到对应的数据库;第二步在将消息发送的MQ中;第三步是MQ收到的结果应答给生产端;第四步监听确认接收到了消息更新消息状态。
①如果步骤一都失败那就需要使用快速失败;
②有一个分布式定时任务间隔时间去扫描隔了多少分钟消息状态还是失败将进行消息重发机制,当重发次数达到一定次数将不再重发(可能是exchange、routingkey等删除或格式和内容问题导致)
这种方案如果使用事务的在高并发情况下,会遇见IO瓶颈,对数据库操作两次
2、消息延迟投递,做二次确认,回调检查
上游服务先入库业务然后发送两条消息(一条是直接发送,另一条是延迟投递检查消息)到MQ,下游服务监听到消息进行处理后,将结果组成一条新的消息发送到MQ中,回调服务监听到消息就会入库消息状态等信息。
延迟投递检查消息(具有相同业务内容,只是发送到不同队列)发送到MQ,被回调服务监听到,然后查询数据库,没有记录则或者处理结果为失败场景则进行RPC回调上游服务查询后重发。
二、幂等性
同一个操作,一次请求和多次请求的结果都是一样的。
消费者端 幂等性保障;在海量的订单产生的业务高峰期,如何避免消息的重复消费;由于网络等原因重发多次,即使收到了多条一样消息只能消费一次。
方案一: 唯一id +指纹码 机制,利用数据库去重。
业务操作前先去数据库查询,没有进行插入数据,有就丢弃 表示已经处理过了;
这里还加指纹码是为了保证某一瞬间连续操作可能导致不唯一问题(指纹码可以是时间戳或业务规则)。
优点是实现简单,在高并发情况下有数据库写入瓶颈(解决方案:根据id分表分库进行算法路由)
方案二: 利用redis原子性实现,(比如看是否存在、自增)
三、消息确认confirm
生产者将消息投递到MQ broker,然后broker确认消息给生产者应答,生产者用来确定消息是否正常送达到broker,这就是可靠性投递核心保障(ack失败重发)。
步骤:①confirmSelect、②addConfirmListener
public class Product {
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接工厂,并配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.25.128");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
//创建一个连接
Connection connection = connectionFactory.newConnection();
//创建一个channel用通信
Channel channel = connection.createChannel();
//开启消息确认模式
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 {
System.err.println("接收到不可达消息 replyCode " + replyCode);
System.err.println("接收到不可达消息 replyText " + replyText);
System.err.println("接收到不可达消息 exchange " + exchange);
System.err.println("接收到不可达消息 routingKey " + routingKey);
System.err.println("接收到不可达消息 body" + new String(body,"UTF-8"));
}
});
//发布一条消息
String msg = "test one message!";
//mandatory如果为true,则return监听器会接收到不可达消息
channel.basicPublish("test_direct_exchange","test_direct1",true,null,msg.getBytes());
//异步监听broker接收反馈
channel.addConfirmListener(new ConfirmListener() {
@Override//消息成功处理
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//deliveryTag;唯一消息标签 multiple:是否批量
System.err.println("Broker ack 成功了!");
}
@Override //消息失败处理
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.err.println("Broker ack 失败了!");
}
});
}
}
}
接收到不可达消息 replyCode 312
接收到不可达消息 replyText NO_ROUTE
接收到不可达消息 exchange test_direct_exchange
接收到不可达消息 routingKey test_direct
接收到不可达消息 bodytest one message!
Broker ack 成功了!
四、Return消息机制
用于处理一些生产者发布的不可路由的消息,比如exchange或者Routingkey不存在,我们就需要使用return Listener去监听
mandatory:如果为true,则监听器会接收到不可达消息,然后进行处理;false broker端会自动删除该消息(例子在上面这块代码中);只有设置mandatory为true条件下return监听器才有用
步骤:①mandatory设置为true ②addReturnListener添加监听器
五、消费端限流
从MQ Broker 中堆积着巨量的消息,推送消费端会造成服务器崩溃或故障。消息生产端不可能做限流因为就是这么大并发需求,所以只能使用MQ做一个流量削峰,然后做消费端限流(不然有可能消费端崩溃或性能下降)。
RabbitMQ提供了qos服务保障功能,在非自动ack前提下,如果一定数目的消息未被确认前(通过基于consumer或者channel设置Qos值),不进行消费新的消息
步骤:①设置消费端手动签收 ②basicQos开启条件限流;
public class Product {
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接工厂,并配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.25.128");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
//创建一个连接
Connection connection = connectionFactory.newConnection();
//创建一个channel用通信
Channel channel = connection.createChannel();
//开启消息确认模式
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 {
System.err.println("接收到不可达消息 replyCode" + replyCode);
System.err.println("接收到不可达消息 replyText" + replyText);
System.err.println("接收到不可达消息 exchange" + exchange);
System.err.println("接收到不可达消息 routingKey" + routingKey);
System.err.println("接收到不可达消息 body" + new String(body/*,"UTF-8"*/));
}
});
//发布一条消息
String msg = "test one message!";
for (int i = 0; i < 5; i++) {
//mandatory如果为true,则return监听器会接收到不可达消息
channel.basicPublish("test_direct_exchange", "test_direct", true, null, msg.getBytes());
}
//异步监听broker回传
channel.addConfirmListener(new ConfirmListener() {
@Override//消息成功处理
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//deliveryTag;唯一消息标签 multiple:是否批量
System.err.println("Broker ack 成功了!");
}
@Override //消息失败处理
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.err.println("Broker ack 失败了!");
}
});
}
}
}
public class Customer {
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接工厂,并配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.25.128");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setAutomaticRecoveryEnabled(true);//自动重连
connectionFactory.setNetworkRecoveryInterval(3000);//自动重连时间间隔3秒
//创建一个连接
Connection connection = connectionFactory.newConnection();
//创建一个channel用通信
Channel channel = connection.createChannel();
//声明一个test_direct_exchange名称、类型为direct、持久化的交换机
String exchangName = "test_direct_exchange";
channel.exchangeDeclare(exchangName,"direct",true);
//声明一个队列
String queueName = "test_direct_queue";
channel.queueDeclare(queueName,true,false,false,null);
//将队列绑定到交换机上,按照键test_direct路由
channel.queueBind(queueName,exchangName,"test_direct");
//prefetchSize为0是不限制消息大小;prefetchCount为限制一次传送给消费者的消息数;
// global false:消费服务器级别限制(常用) true:channel级别限制
channel.basicQos(0,1,false);
//监听队列,AutoAck为false才会消费一个,成功签收后才能继续消费
channel.basicConsume(queueName,false,new MyConsumer(channel));
}
}
public class MyConsumer extends DefaultConsumer {
//MQ传输消息的信道
private Channel channel;
public MyConsumer(Channel channel) {
super(channel);
this.channel = channel;
}
@Override //处理MQ传送的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.err.println("接收到消息 consumerTag: " + consumerTag);
System.err.println("接收到消息 envelope: " + envelope);
System.err.println("接收到消息 properties: " + properties);
System.err.println("接收到message:"+ new String(body));
//签收操作 deliveryTag是消息的唯一性标签; multiple:false不批量签收
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
如果出现消费服务消息没有处理完,所以没有ack签收消息,所以MQ将不会再次发生消息过来,这样就达到限流效果。
消费服务端:
接收到消息 consumerTag: amq.ctag-zl-ZR63L3dzIbvhG3psrKQ
接收到消息 envelope: Envelope(deliveryTag=1, redeliver=false, exchange=test_direct_exchange, routingKey=test_direct)
接收到消息 properties: #contentHeader<basic>(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
接收到message:test one message!
生产者服务端:这里正在处理第二条消息
Broker ack 成功了!
Broker ack 成功了!
六、消费端ACK与重回队列机制
步骤:①设置消费端手动签收,如果没有给返回ack应答,那么这条消息会继续存在unacked状态下,占据Broker队列的空间。
public class Customer {
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接工厂,并配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.25.128");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setAutomaticRecoveryEnabled(true);//自动重连
connectionFactory.setNetworkRecoveryInterval(3000);//自动重连时间间隔3秒
//创建一个连接
Connection connection = connectionFactory.newConnection();
//创建一个channel用通信
Channel channel = connection.createChannel();
//声明一个test_direct_exchange名称、类型为direct、持久化的交换机
String exchangName = "test_direct_exchange";
channel.exchangeDeclare(exchangName,"direct",true);
//声明一个队列
String queueName = "test_direct_queue";
channel.queueDeclare(queueName,true,false,false,null);
//将队列绑定到交换机上,按照键test_direct路由
channel.queueBind(queueName,exchangName,"test_direct");
//prefetchSize为0是不限制消息大小;prefetchCount为限制一次传送给消费者的消息数;
// global false:消费服务器级别限制(常用) true:channel级别限制
// channel.basicQos(0,1,false);
//监听队列,AutoAck为false才会消费一个,成功签收后才能继续消费
channel.basicConsume(queueName,false,new MyConsumer(channel));
}
}
public class MyConsumer extends DefaultConsumer {
//MQ传输消息的信道
private Channel channel;
public MyConsumer(Channel channel) {
super(channel);
this.channel = channel;
}
@Override //处理MQ传送的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.err.println("接收到message:"+ new String(body));
if((Integer)properties.getHeaders().get("num") == 0){
//消费端回传不签收 requeue: true重新到队列的尾部,false丢弃
channel.basicNack(envelope.getDeliveryTag(),false,true);
//消费丢弃
//channel.basicNack(envelope.getDeliveryTag(),false,false);
}else {
// deliveryTag是消息的唯一性标签; multiple:false不批量签收
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
}
控制台:会有消息0一直重回队列尾部,然后重发给消费端。
接收到message:test one message!0
接收到message:test one message!1
接收到message:test one message!2
接收到message:test one message!3
接收到message:test one message!4
接收到message:test one message!0
接收到message:test one message!0
接收到message:test one message!0
七、TTL消息详解
消息、队列的过期时间,从消息进入队列开始计算,超过指定时间将会被自动清除。
八、死信队列
DLX:dead-letter-exchange
这条消息没人去消费,变成死信了。RabbitMQ会将这种消息重新publish到另一个DLX的exchange的队列上。
1、消息变成死信有如下几种情况:
- 消息被拒绝(basic.reject/basic.nack)并且requeue=false
- 消息TTL过期(消息设置属性过期时间 或者 设置 队列过期时间)
- 队列达到最大长度
public class Customer {
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接工厂,并配置
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.25.128");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setAutomaticRecoveryEnabled(true);//自动重连
connectionFactory.setNetworkRecoveryInterval(3000);//自动重连时间间隔3秒
//创建一个连接
Connection connection = connectionFactory.newConnection();
//创建一个channel用通信
Channel channel = connection.createChannel();
//声明一个test_direct_exchange名称、类型为direct、持久化的交换机
String exchangName = "test_direct_exchange";
channel.exchangeDeclare(exchangName,"direct",true);
//声明一个队列
String queueName = "test_direct_queue_dxl";
//定义当前队列死信后publish到死信交换机dlx.exchange对应的队列
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "dlx.exchange");
channel.queueDeclare(queueName,true,false,false,arguments);
//将队列绑定到交换机上,按照键test_direct路由
channel.queueBind(queueName,exchangName,"test_direct");
//定义一个dlx.exchange交换机死信队列dlx.queue
channel.exchangeDeclare("dlx.exchange","topic",true,false,null);
channel.queueDeclare("dlx.queue",true,false,false,null);
channel.queueBind("dlx.queue","dlx.exchange","#");
}
}
如下图所示由于消息没有被消费导致死信,然而也配置了对应的死信队列,所以如下显示