使用MQ的时候一定是先操作数据库再发送消息,这样是为了避免数据库回滚导致数据不一致。但是如果先操作数据后发送消息,一旦消息出了问题,一样会出现数据的不一致。此篇文章处理MQ可靠性投递的问题。
在使用RabbitMQ收发信息的时候,有以下几个环节需要注意:
第一个环节生产者如何确定Broker有没有正确的接收到消息。
在RabbitMQ里面提供两种截止服务端确认机制,也就是生产者发送消息给RabbitMQ服务端的时候,服务端会通过某种方式返回一个应答(ACK),只要生产者收到了这个应答(ACK),就知道消息发送成功了。
我们可以通过一个channel.txSelect();的方法吧信道设置成事务模式,然后就可以发布消息给RabitMQ了,如果channel.txCommit();方法调用成功,则说明事务提交成功,这个消息一定到达了RabbitMQ中。
如果在事务提交之前由于RabbitMQ异常崩溃活其他原因抛出异常,这个时候我们便可以捕获异常,并执行channel.txRollback(); 方法来实现事务回滚。
注意:在事务模式里面,只有收到了服务端的Commit-OK的指令,才能提交成功。所以可以解决生产者和服务端确认的问题。但是事务模式有一个缺点,它是阻塞的,一条消息没有发送完毕,不能发送下一条消息,它会榨干RabbitMQ服务器的性能。
示例:
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
String msg = "Hello world, Rabbit MQ";
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
channel.queueDeclare("ORIGIN_QUEUE", false, false, false, null);
try {
channel.txSelect();
// 发送消息
// String exchange, String routingKey, BasicProperties props, byte[] body
channel.basicPublish("", "ORIGIN_QUEUE", null, (msg).getBytes());
// int i =1/0;
channel.txCommit();
System.out.println("消息发送成功");
} catch (Exception e) {
channel.txRollback();
System.out.println("消息已经回滚");
}
channel.close();
conn.close();
}
要在spring boot里设置:
rabbitTemplate.setChannelTransacted(true);
Confirm模式既可以保证消息被Broker接收,有不大量消耗性能的方式。
在生产者调用channel.confirmSelect();方法将信道设置为Confirm模式,然后发送消息。一旦消息被投递到所匹配的队列后,RabbitMQ就会发送一个应答(ACK)给生产者。调用 channel.waitForConfirms()返回true,这样生产者就知道消息被服务端接收了。
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
String msg = "Hello world, Rabbit MQ ,Normal Confirm";
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
channel.queueDeclare("QUEUE_NAME", false, false, false, null);
// 开启发送方确认模式
channel.confirmSelect();
channel.basicPublish("", "QUEUE_NAME", null, msg.getBytes());
// 普通Confirm,发送一条,确认一条
if (channel.waitForConfirms()) {
System.out.println("消息发送成功" );
}
channel.close();
conn.close();
}
普通确认模式是发送一条确认一条,这样的效率还是不太高,因此有的批量确认方式。在生产者开启Confirm模式后,先发送一批消息,只要channel.waitForConfirmsOrDie()方法没有抛异常,则表示消息都被服务端接收。
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
String msg = "Hello world, Rabbit MQ ,Batch Confirm";
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
channel.queueDeclare("QUEUE_NAME", false, false, false, null);
try {
channel.confirmSelect();
for (int i = 0; i < 5; i++) {
// 发送消息
// String exchange, String routingKey, BasicProperties props, byte[] body
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();
}
channel.close();
conn.close();
}
批量确认方式虽然比单条效率要高,但是其确定是的是数量,不会准确到单条信息。对于不通的业务要多少条消息确定一次?太少的话影响效率,太多的话失败一条则所有的都要重发。因此出来了异步确认模式。
异步确认模式需要添加一个ConfirmListener,并且用一个SortedSet来维护没有被确认的消息。
配置:
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
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);
}
});
rabbitTemplate.setChannelTransacted(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (!ack) {
System.out.println("发送消息失败:" + cause);
throw new RuntimeException("发送异常:" + cause);
}
}
});
return rabbitTemplate;
}
生产者:
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
// 建立连接
Connection conn = factory.newConnection();
// 创建消息通道
Channel channel = conn.createChannel();
String msg = "Hello world, Rabbit MQ, Async Confirm";
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
channel.queueDeclare("QUEUE_NAME", false, false, false, null);
// 用来维护未确认消息的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) {
// headSet表示后面参数之前的所有元素,全部删除
confirmSet.headSet(deliveryTag + 1L).clear();
} 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);
// 这里注释掉的原因是如果先关闭了,可能收不到后面的ACK
//channel.close();
//conn.close();
}
第二个环节就是从交换机路由到队列。消息无法路由到正确的队列的原因有 1)、路由键错误 2)、队列不存在。
有两种方式处理无法路由的消息,一种是让服务器重发给生产者,一种是让交换机路由到另一个备份的交换机。
使用mandatory参数和ReturnListener(在Spring AMQP中是ReturnCallback)。
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
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);
}
});
在创建交换机的时候,从属性中指定备份交换机。
// 在声明交换机的时候指定备份交换机
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);
注意:队列可以指定死信交换机;交换机可以指定备份交换机
第三个环节是消息在队列存储,如果没有消费者的话,队列会一直存在数据库中。
如果RabbitMQ的服务或者硬件发生故障,可能会导致内存中的消息丢失,所以要把消息本身和元数据(队列、交换机、绑定)都保存到磁盘中。
在RabbitConfig中配置
@Bean("GpQueue")
public Queue QueueGpQueue(){
//queueName,durable,exclusive,autoDelete,Properties
return new Queue("GP_TEST_QUEUE",true,false,false,newHashMap<>());
}
@Bean("GpExchange")
public DirectExchange exchange(){
//exchangeName,durable,exclusive,autoDelete,Properties
return new DirectExchange("GP_TEST_EXCHANGE",true,false,newHashMap<>());
}
public class ProducerApp {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ProducerApp.class);
RabbitAdmin rabbitAdmin = context.getBean(RabbitAdmin.class);
RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
rabbitAdmin.declareExchange(new DirectExchange("GP_RELIABLE_SEND_EXCHANGE", true, false, new HashMap<>()));
MessageProperties messageProperties = new MessageProperties();
// 消息持久化
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
messageProperties.setContentType("UTF-8");
Message message = new Message("哈哈哈哈哈".getBytes(), messageProperties);
rabbitTemplate.send("GP_RELIABLE_SEND_EXCHANGE", "gupao.tech", message, new CorrelationData("201906180001"));
rabbitTemplate.send("GP_RELIABLE_SEND_EXCHANGE", "gupao.tech.wrong", message, new CorrelationData("201906180002"));
}
}
如果只有一个RabbitMQ节点,即使交换机、队列、消息持久化,如果服务崩溃或者硬件发生故障,其服务一样是不可用的,为了提高MQ服务的可用性,保障消息的传输,则需要搭建多个节点。
如果消费者收到消息后没来得及处理便发生异常,或者处理过程中中断,会导致④失败。服务端应该以某种方式得知消费者对消息的接收情况,并决定是否重新投递这条消息给其他消费者。
RabbitMQ提供了消费者的消息确认机制(message acknowledgement),消费者可以自动或者手动发送ACK给服务端。
没有收到ACK的消息,消费者断开连接后,当autoAck,RabbitMQ会把这条消息发送给其他消费者。如果没有其他消费者,消费者重启后会重新消费这条消息,重复执行业务逻辑。
消费者在订阅队列时,可以指定autoAck参数,当autoAck等于false时,RabbitMQ会等待消费者显式的回复确认信号后才会从对垒中移去消息。
手动设置ACK
SimpleRabbitListenerContainer或者SimpleRabbitListenerContainerFactory
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
application.properties
spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode=manual
注意:
NONE:自动ACK
MANUAL:手动ACK
AUTO:如果方法未抛出异常,则发送ack。
当抛出AmqpRejectAndDontRequeueException异常的时候,则消息会被拒绝,且不重新入队。当抛出ImmediateAcknowledgeAmqpException异常,则消费者会发送ACK。其他的异常,则消息会被拒绝,且requeue=true会重新入队。
如果消息无法处理或者消费失败,也有两种拒绝的方式,Basic.Reject()拒绝单条,Basic.Nack()批量拒绝。如果requeue参数设置为true,可以把这条消息重新存入队列,以便发给下一个消费者(当然,只有一个消费者的时候,这种方式可能会出现无限循环重复消费的情况。可以投递到新的队列中,或者只打印异常日志)。
例如:提单系统给其他系统发送了碎屏保消息后,其他系统必须在处理完消息后调用提单系统提供的API,来修改提单系统中数据的状态。只要API没有被调用,数据状态没有被修改,提单系统就认为下游系统没有收到这条消息。
例如:商业银行与人民银行二代支付通信,无论是人行收到了商业银行的消息,还是商业银行收到了人行的消息,都必须发送一条响应消息(叫做回执报文)。
如果生产者的API就是没有被调用,也没有收到消费者的响应消息,怎么办?
不要着急,可能是消费者处理时间太长或者网络超时。
生产者与消费者之间应该约定一个超时时间,比如5分钟,对于超出这个时间没有得到响应的消息,可以设置一个定时重发的机制,但要发送间隔和控制次数,比如每隔2分钟发送一次,最多重发3次,
否则会造成消息堆积。
重发可以通过消息落库+定时任务来实现。
比如:
ATM机上运行的系统叫C端(ATMC),前置系统叫P端(ATMC),它接收ATMC的消息,再转发给卡系统或者核心系统。
1)、如果客户存款,没有收到核心系统的应答,不知道有没有记账成功,最多发送5次存款确认报文,因为已经吞钞了,所以要保证成功;
2)、如果客户取款,ATMC未得到应答时,最多发送5次存款冲正报文。因为没有吐钞,所以要保证失败。
如果消费者每一次接收生产者的消息都成功了,只是在响应或者调用API的时候出了问题,会不会出现消息的重复处理?例如:存款100元,ATM重发了5次,核心系统一共处理了6次,余额增加了600元。所以,为了避免相同消息的重复处理,必须要采取一定的措施。RabbitMQ服务端是没有这种控制的(同一批的消息有个递增的DeliveryTag)
,它不知道你是不是就要把一条消息发送两次,只能在消费端控制。
如何避免消息的重复消费?
消息出现重复可能会有两个原因:
1、生产者的问题,环节①重复发送消息,比如在开启了Confirm模式但未收到确认,消费者重复投递。
2、环节④出了问题,由于消费者未发送ACK或者其他原因,消息重复投递。
3、生产者代码或者网络问题。对于重复发送的消息,可以对每一条消息生成一个唯一的业务ID,通过日志或者消息落库来做重复控制。
在RabbitMQ中,一个队列有多个消费者时,由于不同的消费者消费消息的速度是不一样的,顺序无法保证。只有一个队列仅有一个消费者的情况才能保证顺序消费(不同的业务消息发送到不同的专用的队列)。