前言
前面在深入浅出RabbitMQ中,我们大致介绍了下RabbitMq的一些基础概念,包括4种交换机模型。在实际企业应用中,我们往往需要考虑消息的可靠性。如果单纯的为了异步削峰,不考虑消息投递的可靠性,最终导致数据不一致,这肯定是不能接受的。
消息可能丢失的四个环节
首先,我来看下消息在被生产到发送到MQ,再被路由到队列,紧接着被消费。其中大致流程如下图所示
从图上我们大概可以看出来一个消息会经历四个节点,只有保证这四个节点的可靠性才能保证整个系统的可靠性。
- 生产者发出后保证到达了MQ。
- MQ收到消息保证分发到了消息对应的Exchange。
- Exchange分发消息入队之后保证消息的持久性。
- 消费者收到消息之后保证消息的正确消费。
生产者发送消息到MQ失败
我们的生产者发送消息之后可能由于网络抖动等各种原因导致我们的消息并没有发送到MQ之中,但是这个时候我们生产端又不知道我们的消息没有发出去,这就会造成消息的丢失。为了解决这个问题,RabbitMQ
引入了事务机制和发送方确认机制,由于事务机制过于耗费性能所以一般不用,这里我着重讲述发送方确认机制。这个机制很好理解,就是消息发送到MQ那端之后,MQ会回一个确认收到的消息给我们。
打开此功能需要配置,接下来我来演示一下配置:
spring:
rabbitmq:
addresses: 127.0.0.1
host: 5672
username: guest
password: guest
virtual-host: /
# 打开消息确认机制
publisher-confirm-type: correlated
我们只需要在配置里面打开消息确认即可。
public void sendAndConfirm() {
User user = new User();
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(Producer.QUEUE_NAME,user,correlationData);
log.info("消息发送完毕。");
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("CorrelationData content : " + correlationData);
log.info("Ack status : " + ack);
log.info("Cause content : " + cause);
if(ack){
log.info("消息成功发送,订单入库,更改订单状态");
}else{
log.info("消息发送失败:"+correlationData+", 出现异常:"+cause);
}
}
});
}
生产者代码里我们看到又多了一个参数:CorrelationData
,这个参数是用来做消息的唯一标识,同时我们打开消息确认之后需要对rabbitTemplate
多设置一个setConfirmCallback
,参数是一个匿名类,我们消息确认成功or失败之后的处理就是写在这个匿名类里面。比如一条订单消息,当消息确认到达MQ确认之后再行入库或者修改订单的节点状态,如果消息没有成功到达MQ可以进行一次记录或者将订单状态修改。消息确认失败不只有消息没发过去会触发,消息发过去但是找不到对应的Exchange,也会触发。
MQ接收失败或者路由失败
生产者的发送消息处理好了之后,我们就可以来看看MQ端的处理,MQ可能出现两个问题:
- 消息找不到对应的Exchange。
- 找到了Exchange但是找不到对应的Queue。
这两种情况都可以用RabbitMQ
提供的mandatory
参数来解决,它会设置消息投递失败的策略,有两种策略:自动删除或返回到客户端。我们既然要做可靠性,当然是设置为返回到客户端(true是返回客户端,false是自动删除)。
spring:
rabbitmq:
addresses: 127.0.0.1
host: 5672
username: guest
password: guest
virtual-host: /
# 打开消息确认机制
publisher-confirm-type: correlated
# 打开消息返回
publisher-returns: true
template:
mandatory: true
生产者:
public void sendAndReturn() {
User user = new User();
log.info("Message content : " + user);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange,
routingKey) -> {
log.info("被退回的消息为:{}", message);
log.info("replyCode:{}", replyCode);
log.info("replyText:{}", replyText);
log.info("exchange:{}", exchange);
log.info("routingKey:{}", routingKey);
});
rabbitTemplate.convertAndSend("fail",user);
log.info("消息发送完毕。");
}
这里我们可以拿到被退回消息的所有信息,然后再进行处理,比如放到一个新的队列单独处理,路由失败一般都是配置问题了。
消息入队之后MQ宕机
到这一步基本都是一些很小概率的问题了,比如MQ突然宕机了或者被关闭了,这种问题就必须要对消息做持久化,以便MQ重新启动之后消息还能重新恢复过来。消息的持久化要做,但是不能只做消息的持久化,还要做队列的持久化和Exchange的持久化。
@Bean
public DirectExchange directExchange() {
// 三个构造参数:name durable autoDelete
return new DirectExchange("directExchange", false, false);
}
@Bean
public Queue erduo() {
// 其三个参数:durable exclusive autoDelete
// 一般只设置一下持久化即可
return new Queue("erduo",true);
}
创建Exchange和队列时只要设置好持久化,发送的消息默认就是持久化消息。设置持久化时一定要将Exchange和队列都设置上持久化:单单只设置Exchange持久化,重启之后队列会丢失。单单只设置队列的持久化,重启之后Exchange会消失,既而消息也丢失,所以如果不两个一块设置持久化将毫无意义。 这些都是MQ宕机引起的问题,如果出现服务器宕机或者磁盘损坏则上面的手段统统无效,必须引入镜像队列,做异地多活来抵御这种不可抗因素。
消费者无法正常消费
最后一步会出问题的地方就在消费者端了,不过这个解决问题的方法我们之前的文章已经说过了,就是消费者的消息确认。
spring:
rabbitmq:
addresses: 127.0.0.1
host: 5672
username: guest
password: guest
virtual-host: /
# 手动确认消息
listener:
simple:
acknowledge-mode: manual
打开手动消息确认之后,只要我们这条消息没有成功消费,无论中间是出现消费者宕机还是代码异常,只要连接断开之后这条信息还没有被消费那么这条消息就会被重新放入队列再次被消费。当然这也可能会出现重复消费的情况,不过在分布式系统中幂等性是一定要做的,所以一般重复消费都会被接口的幂等给拦掉。
消息可靠性案例
这个图是我很早之前画的,是为了记录当时使用RabbitMQ
做消息可靠性的具体做法,这里我正好拿出来做个例子给大家看一看。这个例子中的消息是先入库的,然后生产者从DB里面拿到数据包装成消息发给MQ,经过消费者消费之后对DB数据的状态进行更改,然后重新入库。这中间有任何步骤失败,数据的状态都是没有更新的,这时通过一个定时任务不停的去刷库,找到有问题的数据将它重新扔到生产者那里进行重新投递。