一.RabbitMQ的可靠性投递
1.MQ实现异步通信过程中,消息丢失或重复,可能导致业务数据不一致如何解决?
ps:在解决问题之前,必须清楚可靠性只是问题的一方面,发送消息的效率同样是需要考虑的问题,而这两个因素无法兼得。如果在发送消息的每一个环节都采取相关措施来保证可靠性,势必会对消息的收发效率造成影响。
会产生消息丢失的四个地方:
1.从生产者到Broker,Broker未接收
可能原因:网络连接或者Broker的问题(硬盘故障、硬盘写满了)导致消息发送失败,但生产者不能确认Broker有没有正确接收。
RabbltMQ提供了两种服务确认机制:
1)Transaction(事务)模式(效率低,不建议在生产环境使用)
在生产者创建channel,把信道设置成事务模式(channel.txSelect();springBoot中rabbitTemplate.setChannleTransacted(true)),如果channel.txCommit()的方法调用成功,就说明事务提交成功,则消息到达了Broker中。否则消息回滚
在事务模式里。只有收到服务端的Commit-OK的指令,才能提交成功。所以可以解决生产者和服务端的确认问题。但同时它是阻塞的,一条消息没有发送完毕,不能发送下一条消息。效率低,不建议在生产环境使用。
try {
channel.txSelect(); //打开事务模式
channel.basicPublish("", "ORIGIN_QUEUE", null, (msg).getBytes());
channel.txCommit(); //消息提交
System.out.println("消息发送成功");
} catch (Exception e) {
channel.txRollback(); //消息回滚
System.out.println("消息已经回滚");
}
2)Confirm(确认)模式
a.普通确认模式
在生产者这边调用channel.confirmSelect()方法将信道设置成Confirm模式。一但消息被投递到交换机之后,MQ会发送一个确认(Basic.Ack)给生产者,也就是调用channel.waitForConfirms()返回true。
// 开启发送方确认模式
channel.confirmSelect();
channel.basicPublish("", "ORIGIN_QUEUE", null, msg.getBytes());
// 普通Confirm,发送一条,确认一条
if (channel.waitForConfirms()) {
System.out.println("消息发送成功" );
}else{
System.out.println("消息发送失败");
}
b.批量确认模式
调用channel.waitForConfirmsOrDie()进行确认,只要有一个未被Broker确认就会IOExcepxianyution,反之如果没有抛异常,则代表消息都被服务端接收了。
try {
channel.confirmSelect();
for (int i = 0; i < 5; i++) {
// 发送消息
channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
}
// 批量确认结果,ACK如果是Multiple=True,代表ACK里面的Delivery-Tag之前的消息都被确认了
// 比如5条消息可能只收到1个ACK,也可能收到2个
// 直到所有信息都发布,只要有一个未被Broker确认就会IOException
channel.waitForConfirmsOrDie();
System.out.println("消息发送完毕,批量确认成功");
} catch (Exception e) {
// 发生异常,可能需要对所有消息进行重发
e.printStackTrace();
}
c.异步确认模式
异步确认模式需添加一个ConfirmListener,并且用一个SortedSet来维护一个批次中没有被确认的消息。
// 用来维护未确认消息的deliveryTag
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
// 这里不会打印所有响应的ACK;ACK可能有多个,有可能一次确认多条,也有可能一次确认一条
// 异步监听确认和未确认的消息
// 如果要重复运行,先停掉之前的生产者,清空队列
channel.addConfirmListener(new ConfirmListener() {
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("Broker未确认消息,标识:" + deliveryTag);
if (multiple) {
confirmSet.headSet(deliveryTag + 1L).clear();// headSet表示后面参数之前的所有元素,全部删除
} else {
confirmSet.remove(deliveryTag);
}
// 这里添加重发的方法
}
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
// 如果true表示批量执行了deliveryTag这个值以前(小于deliveryTag的)的所有消息,如果为false的话表示单条确认
System.out.println(String.format("Broker已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
if (multiple) {
// headSet表示后面参数之前的所有元素,全部删除
confirmSet.headSet(deliveryTag + 1L).clear();
} else {
// 只移除一个元素
confirmSet.remove(deliveryTag);
}
System.out.println("未确认的消息:"+confirmSet);
}
});
// 开启发送方确认模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
long nextSeqNo = channel.getNextPublishSeqNo();
// 发送消息
// String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
confirmSet.add(nextSeqNo);
}
System.out.println("所有消息:"+confirmSet);
2.2)SpringBoot下开启确认模式
rabbitTemplate对Channel进行了封装,可通过其开启Confirm模式
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息确认成功");
} else {
// nack
System.out.println("消息确认失败");
}
}
});
2.从交换机到队列,交换机可能未分发或未找到正确的队列。
消息无法路由的两种情况:1.routingkey 错误 2队列不存在.
处理无法路由消息的两种方式:
(1)服务端重发给生产者
spring中
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode,
String replyText,
String exchange,
String routingKey,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
System.out.println("=========监听器收到了无法路由,被返回的消息============");
System.out.println("replyText:"+replyText);
System.out.println("exchange:"+exchange);
System.out.println("routingKey:"+routingKey);
System.out.println("message:"+new String(body));
}
});
--------------------------------------------------------------------------------------
//SpringBoot中
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback(){
public void returnedMessage(Message message,
int replyCode,
String replyText,
String exchange,
String routingKey){
System.out.println("回发的消息:");
System.out.println("replyCode: "+replyCode);
System.out.println("replyText: "+replyText);
System.out.println("exchange: "+exchange);
System.out.println("routingKey: "+routingKey);
}
});
(2)让交换机路由到另一个备份的交换机,在创建交换机的时候,在属性中指定备份交换机
//Spring
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);
// 发送到了默认的交换机上,由于没有任何队列使用这个关键字跟交换机绑定,所以会被退回
// 第三个参数是设置的mandatory,如果mandatory是false,消息也会被直接丢弃
channel.basicPublish("TEST_EXCHANGE","qingshan2673",true, properties,"只为更好的你".getBytes());
--------------------------------------------------------------------------------
//SpringBoot
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
// 声明一个交换机
rabbitAdmin.declareExchange(new DirectExchange("GP_ADMIN_EXCHANGE", true, false,arguments));
3.消息队列内消息丢失
消息在消息队列如果没有消费者的话,队列一直存在在数据库中。如果MQ的服务或者硬件发生故障,则可能导致内存中的消息丢失。所以我们要把消息本身和元数据(队列、交换机、绑定)都并保存到磁盘
1)队列持久化
2)交换机持久化
durable:没有持久化的队列,服务重启队列和消息都会消失
autoDelete:没有消费者连接的时候,自动删除
exclusive: 1)只会首次声明它的连接可见 2)会在其连接断开的时候自动删除
return new DirectExchange("GP_ORI_USE_EXCHANGE", true, false, new HashMap<>());
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
3)消息持久化
如果消息没有持久化,保存到内存中。队列还在,但是消息会在重启后小时
// 对每条消息设置过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2) // 持久化消息
.contentEncoding("UTF-8")
.expiration("60000") // TTL
.build()
4)集群
如果只有一个RabbitMQ的节点,即使交换机、队列、消息做了持久化,如果服务崩溃或硬件故障,RabbitMQ的服务一样是不可用的。所以为了提高MQ服务的可用性,保障消息的传输,需要多个RabbitMq的节点。
4.消费者未接收,Broker不知道
如果消费者接收消息后没来得及处理即发生异常,或者处理中发生异常。服务端应以某种方式得知消费者对消息的接收情况,并决定是否重新投递消息给其他消费者。
1)消息确认机制
消费者自动或手动发送ACK给服务端,如果没收到ACK消息,消费者断开后,MQ会把这条消息发送给其他消费者。如果没有其他消费者,消费者重启后重新消费这条消息。
a.自动ACK
自动ACK是默认情况,也就是我们没有在消费者处编写ACK的代码。消费者会在收到消息的时候就自动发送ACK,而不是在方法执行完毕的时候发送ACK.(并不关心你有没有正常消息)
b.手动ACK
1.把autoAck设置成false
2.消费者显式地回复ACK,(如果不应答,队列中的消息会一直存在,重新连接的时候会重复消费)。MQ接收到回复后从队列中移除消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("Received message : '" + msg + "'");
if (msg.contains("拒收")){
// 拒绝消息
// requeue:是否重新入队列,true:是;false:直接丢弃,相当于告诉队列可以直接删除掉
// TODO 如果只有这一个消费者,requeue 为true 的时候会造成消息重复消费
channel.basicReject(envelope.getDeliveryTag(), false);
} else if (msg.contains("异常")){
// 批量拒绝
// requeue:是否重新入队列
// TODO 如果只有这一个消费者,requeue 为true 的时候会造成消息重复消费
channel.basicNack(envelope.getDeliveryTag(), true, false);
} else {
// 手工应答
// 如果不应答,队列中的消息会一直存在,重新连接的时候会重复消费
channel.basicAck(envelope.getDeliveryTag(), true);
}
}
};
2)SpringBoot下的消息确认
a. 两种方式
(1)通过类配置文件在SimpleMessageListenerContainer 或 SimpleRabbitListenerContainerFactory 配置签收模式。
(2)在application中配置 spring.rabbitmq.listener.direct.acknowledge-mode=manual spring.rabbitmq.listener.simple.acknowledge-mode=manual
NONE:自动ACK ;
MANUAL:收到ACK;
AUTO:若方法未抛出异常,则发送ack;若方法抛出异常但不是 Class AmqpRejectAndDontRequeueException 则发送nack,重新入队列;若异常是AmqpRejectAndDontRequeueException,则发送nack不会重新入队列
@Bean
public SimpleMessageListenerContainer messageContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setQueues(getSecondQueue(), getThirdQueue()); //监听的队列
container.setConcurrentConsumers(1); // 最小消费者数
container.setMaxConcurrentConsumers(5); // 最大的消费者数量
container.setDefaultRequeueRejected(false); //是否重回队列
container.setAcknowledgeMode(AcknowledgeMode.AUTO); //签收模式
container.setExposeListenerChannel(true);
container.setConsumerTagStrategy(new ConsumerTagStrategy() { //消费端的标签策略
@Override
public String createConsumerTag(String queue) {
return queue + "_" + UUID.randomUUID().toString();
}
});
return container;
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter()); // 消息转换器
factory.setAcknowledgeMode(AcknowledgeMode.NONE); // 签收模式
factory.setAutoStartup(true);
factory.setConcurrentConsumers(2); // 最小消费者数
factory.setMaxConcurrentConsumers(6); //最大消费者数
factory.setTransactionManager(rabbitTransactionManager(connectionFactory));
return factory;
}
b.消费者调用ACK
@RabbitListener(queues = "ACK_QUEUE", containerFactory="rabbitListenerContainerFactory")
public class ACKConsumer {
@RabbitHandler
public void process(String msgContent,Channel channel, Message message) throws IOException {
System.out.println("ACK Queue received msg : " + msgContent );
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
二.生产者确认消费者有没有消费成功
1)消费者消费成功后调用生产者API
例如订单系统给其他系统发送消息,其他系统处理完后,需调用订单系统的API,来修改订单系统的数据状态
2)发送响应消息给生产者
例如商业银行和人民银行的二代支付通信,无论人行收到了商业银行的消息,还是商业银行收到了人行的消息,都需发送一条响应消息。(回执报文)
三.补偿机制
1)生产者和消费者之间应该约定一个超时时间,对于超出这个时间没有得到响应的消息,才确认为消费失败。
2)对于超过约定时间的消息,才能判断消息消费失败。失败后,应重复消息
3)消息重复应由后台代码重复,创建一个定时任务,每30秒跑一次,找到业务表里面的这条业务状态是中间状态的记录查询出来,构建MQ消息,重新发送。也可以单独设计一张消息表,异步等级系统发送的状态是未回复的消息。消息重复需根据业务设置合理的发送间隔和最大重发次数
四. 消息幂等性
消息重复消费可能出现的原因:
(1)消费者状态正常,每一条消息正常处理,但在响应生产者或者给Broker发送ACK或者其他原因,导致消息重复消费
(2)生产者出现问题(例如生产者开启了Confirm,但一直收不到确认),重复向Broker 发送消息。消费者被重复投递。
解决方案
为了避免相同消息的重复处理。需采取一定的措施。RabbitMQ服务端没有这种控制(同一批的消息有个递增的DeliveryTag),但它不知道对于你的业务来说什么才是重复的消息只能在消费端控制。 对于重复发送的消息,可以对每一条消息生成唯一的业务ID,通过日志或者消息落库来做重复控制。
五.消息的顺序性
消息的顺序性指的是消费者消费消息的顺序跟生产者生产消息的顺序一致。
在RabbitMQ中,一个队列有多个消费者时,由于不同的消费者消费消息的速度是不一样的,顺序无法保证。只有一个队列只有一个消费者的情况次才能保障顺序消费(不同的业务队列发送到不同的专用队列)。除非负载情况,不要多个消费者消费消息。消费端捕获异常。