在我们开发中,随着业务的不断复杂和调用链路的不断增长,我们可能会慢慢引入越来越多的中间件来更好的服务于我们的系统,但是每样技术都是一把双刃剑,在提高我们系统性能的同时,我们也要想办法来减少它对系统带来稳定性的影响,今天要带来的是如何让RabbitMQ的可靠性达到保证。
要想了解如何保证RabbitMQ的可靠性,首先要从它的执行流程开始了解。
执行流程
- 生产者发送消息或者消费者进行消费消息都会先与主机建立起一条长连接,由长连接里的channal来传送消息。
- 长连接优点:消费者如果出现宕机或者下线,mq会感知到,没法继续派发后会把这条消息再次存储起来,避免造成消息大面积丢失
- 消息由消息头+消息体+路由键组成。
- 消息发送出去后,首先进入mq服务器指定的一个虚拟主机中,由虚拟主机中的exchange交换机收到后,通过消息的路由键和绑定关系,最终决定发往那个 队列。
- 消费者通过监听指定队列拿到消息。
由执行流程就可以看到,消息不管是在生产者发送到MQ服务器的过程中或者是在消费的过程中都存在着丢失的风险,那怎么办呢?
消息确认机制-可靠抵达
事务
提到保证可靠性的问题,小伙伴们肯定首先可以想到的是事务机制,RabbitMQ也提供了事务消息,不过官方文档也写到事务消息让MQ的性能下降250倍,所以说在当今项目对性能要求很高的情况下,显然不适合去使用事务消息。
那只能从其他几个方面来保证可靠性了。
这是一张消息发送到消费的简图,要保证可靠性的话,要从三个方面来来考虑。
publisher → Broker,confirmCallback机制
- 在创建connectionFactory的时候设置publisherConfirm(true)选项,开始confirmCallback。
- 消息只要被broker收到就会执行confirmCallback,如果是cluster模式,需要所有broker都接收到才会调用confirmCallback。
- 被broker接收到只能表示message已经抵达服务器,并不能保证消息一定被投递到目标queue里。所以需要用到接下来的returnCallback。
#配置yml文件
spring:
rabbitmq:
#开启发送单确认
publisher-confirms: true
//定制abbitTemplate
@PostConstruct //MyRabbitConfig初始化完成后,执行这个方法
public void initRabbitTemplate(){
// 服务器收到消息确认回调
/*
correlationData 消息的唯一id
ack 消息是否成功收到
cause 失败原因
*/
rabbitTemplate.setConfirmCallback(((correlationData, ack, cause) -> {
log.info("confirm---->correlationData{},-------->ack{},-------->cause{}",correlationData,ack,cause);
}));
}
Exchange → Queue,returnCallback 机制
- confirm模式只能保证消息抵达broker,不能保证消息准确投递到目标queue里。在一些业务场景下,需要保证消息一定要投递到目标queue里,此时就需要用到return退回模式。
- 这样如果未能投递到目标queue里将会调用returnCallback ,可以记录下详细到投递数据,定期的巡检或者自动纠错都将需要这些数据。
#配置yml文件
spring:
rabbitmq:
#开启发送端消息抵达队列确认
publisher-returns: true
#只要抵达队列,以异步方式优先回调这个returnconfirm
template:
mandatory: true
//定制abbitTemplate
@PostConstruct //MyRabbitConfig初始化完成后,执行这个方法
public void initRabbitTemplate(){
//设置消息抵达queue的确认回调 (消息没有投递给指定队列,才会触发这个失败回调)
/*
message 投递失败的详细信息
replyCode 回复的状态码
replyText 回复的文本内容
exchange 这个消息发送给哪个交换机
routingKey 这个消息用的哪个路由键
*/
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info("return---->message{},-->replyCode{},-->replyText{},-->exchange{},-->routingKey{}",
message,replyCode,replyText,exchange,routingKey);
});
}
Queue → Consumer,ack消息确认机制(消费端)
- 默认自动ack,消息被消费者收到,就会从broker的queue中移除
- 问题:收到很多消息,自动回复给服务器ack,只处理一个消息,服务器就宕机了。这时消息就会全部丢失,所以要关闭默认的自动ack机制。
- 消费者获取到消息,成功处理,可以回复ack给broker
- basic.ack用于肯定确认;broker将移除此消息
- basic.nack用于否定确认;可以指出broker是否丢弃此消息,可以批量
- basic.reject用于否定确认;同上,但不能批量
- queue无消费者,消息依然会被存储,直到消费者消费
- 消费者收到消息,默认会自动ack。但是如果无法确认此消息是否被处理完成,或者成功处理。我们可以开启手动ack模式
- 消息处理成功,ack(),接受下一个消息,此消息broker就会移除
- 消息处理失败,nack()/reject(),重新发送给其他人进行处理,或者容错处理后ack
- 消息一直都没有调用ack/nack方法,broker认为此消息正在被处理,不会投递给别人。此时客户端断开。消息不会被broker移除,会投递给别人
- 手动ack机制下,只要没有明确告诉mq消息被消费,没有ack,消息就一直是unacked状态。即使consumer宕机,消息不会丢失,会变为ready状态,下次一有新的consumer连接进来就发给他
#配置yml文件
spring:
rabbitmq:
#切换为手动ack
listener:
direct:
acknowledge-mode: manual
完成以上三个配置后,我们用于消费消息的代码就会变成这样
/**
* @author lp
* @date 2020/8/9 15:05
*/
@Service
@Slf4j
@RabbitListener(queues = "demo.queue")
public class DemoListener {
@Autowired
private DemoService service;
@RabbitHandler
public void listener(DemoEntity entity, Channel channel, Message message) throws IOException {
log.info("-----------开始消费消息----------");
try {
//具体业务
service.doSomething(entity);
//成功处理回复acl
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
//失败处理重新返回queue
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
当然仅仅是这三步配置还是不够的,因为上述步骤并没有做到消息持久化,在做好持久化方案后,我们的消息将无敌。(夸张的修辞手法)
小结:
- 消息发送出去,由于网络问题没有抵达服务器
- 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式。
- 做好日志记录(给数据库保存每一个消息的详细信息),每个消息状态是否都被服务器收到,应记录。
- 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发。
- 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机
- publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
- 自动ack的状态下。消费者收到消息,但还没来得及处理消息,宕机
- 一定开启手动ack,消息消费成功后才移除,失败或者没来得及处理就noAck并重新入队。
做好这些后,消息丢失的可能性已经很小很小了,但是又会有新的问题出现,比如说重复消费,消息过多导致消费者宕机等。
防止消息重复
- 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
- 消费失败时,由于重试机制,自动又将消息发送出去
- 成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
- 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
- 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
- rabbitMq每一次消息都有redelivered字段,可以获取是否被重新投递过来的,而不是第一次被投递过来的
关于具体如何保证幂等性,关注我,我以后会另开一起详细说明.
消息积压
- 产生原因:
- 消费者宕机
- 消费者能力不足积压
- 发送者发送流量太大
- 如何解决:
- 上线更多消费者,进行正常消费
- 上线专门的消息队列服务,将消息先批量取出来,记录到数据库,离线慢慢处理