简单的整理RabbitMQ的相关的知识,我们初步了解了RabbitMQ的相关知识,那么我们怎么在我们的工作中使用RabbitMQ呢,使用RabbitMQ应该需要注意哪些问题。我们以面试题的方法来展示,不过本次的题目不包括集群和高可用方便的,这个等我梳理集群和高可用的相关知识后再整理对应的面试题
本博文参考网上很多关于RabbitMQ的经典面试题,整理归纳并加上自己的理解,意在梳理自己对RabbitMQ的理解以及更好地帮助广大的码友们,如有错误,敬请指正
1. RabbitMQ的实用优点
总得来说,有三大确实的好处
- 应用解耦
举例说明:有这样一个业务场景:我们在网上购买一件商品,支付成功之后库存要减少一,如果是传统的软件架构中,必须是先支付,然后再减少库存,这两个操作必须是在同一事务中,即操作原子性,但是这样做的话效率是极其低下的,如果使用RabbitMQ的话,我们需要将消息发送给各自的队列来进行消息处理,支付和库存的操作之间没有了关联性,这样支付系统和库存系统之间就进行了解耦。
- 流量削峰
rabbitMQ可以使用缓冲队列的方式,在访问量急剧增大的时候,减少并发访问的压力,比较常见的业务场景就是秒杀和签到系统,一般来说流量的削峰有两个处理方式:
- 上游队列缓冲,限速发送
- 下游队列缓冲,限速执行
当然,常见的场景是采用第二种方式,不影响客户使用的响应速度和使用体验等,
我们知道RabbitMQ中消息是通过信道Channel来传给对应的队列的,而消费端监听这个队列处理其中的消息也是有处理时间的,这时我们需要解决的就是如果队列上有一定数量的消息未被确认,则不进行新的消息的消费
rabbitMQ提供channel.basicQos方法来限制信道上的消费者所能保持的最大未确认消息的数量,说到未确认,我们需要先知道RabbitMQ为了保证消息可靠的到达消费者那里, 提供了消息确认机制,通过autoAck参数来控制,如果autoAck为true,默认消息消费者自动确认消息,此时消息可能未被处理结束,如果autoAck为false的话,则需要消费者手动来确认消息
结合上面两点,我们可以利用rabbitMQ在服务的下游来限速执行达到流量削峰的目的
- 异步处理
很多的业务场景中,需要发出一个指示,但是并不要求立即执行,可能对什么时候执行,或者只要执行就可以了有不同的需求,而对象这样的RabbitMQ提供不同的解决方法,用户发送发送的消息储存在RabbitMQ中,由rabbitMQ传递给消费者来进行消费,也可以通过死信队列来实现延迟队列的效果,让消息定时被消费等等。
当然RabbitMQ还有很多其他的好处,比如:很容易实现集群环境的搭建,能定制路由设置消息传递的规则以及消息分发和消息缓冲等优点
2. 消息基于什么传输,这样做有什么优点?
RabbitMQ是基于信道Channel的方式来传输数据,排除了使用TCP链接来进行数据的传输,因为TCP链接创建和销毁对于系统性能的开销比较大,且并发能力受系统资源的限制,这样很容易造成rabbitMQ的性能瓶颈。
消费者链接RabbitMQ其实就是一个TCP链接,一旦链接创建成功之后,就会基于链接创建Channel,每个线程把持一个Channel,Channel复用TCP链接,减少了系统创建和销毁链接的消耗,提高了性能
3. 如何确保消息正确的发送到RabbitMQ
发送方发送消息到RabbitMQ,有可能发送失败,失败的原因有如下的可能:
- 交换器无法根据自身的类型和路由键匹配到队列(mandatory)
- 当与路由键匹配的所有队列都没有消费者时(延时队列和死信队列)
对于上面的情况,RabbitMQ提供发送方确认机制来去报消息正确发送到RabbitMQ服务
发送方确认机制是指消息生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上发布的消息都会指派一个唯一的id,一旦消息被投递到RabbitMQ服务中国,RabbitMQ就会发送一个确认给生产者。
发送方确认模式是异步的,不影响生成者继续发送消息,可扩展性也就增大了
其实还有另外一种解决方法,发送方如果觉得异步确认对于代码而言变的复杂了,那么可以不设置mandatory
额外补充下,发送方确认其实还有事务机制,主要有如下的方法进行设置:
- channel.txSelect: 将当前的Channel设置为事务模式
- channel.txCommit: 提交当前的事务
- channel.txRollback: 事务回滚
跟数据库的事务比较像,也的确能解决发送方能确认消息是否发送到RabbitMQ中,但是事务机制很耗费性能,所以不提倡使用事务,仅供了解即可
4. 如何确保消息接收方消费消息
消费方通过监听队列,从Channel中获取队列中存储的数据并进行消费——即为消费者订阅队列,可以执行autoAck参数,当autoAck为false时,RabbitMQ会等待消费者显示的回复确认信号之后,才会从内存(或者硬盘)中删除消息。
所以确保消费者消费消息,只需要设置autoAck参数为false即可,这样就确保RabbitMQ会等待消息消费完成之后才删除消息。但是这个特性却衍生出新的问题,如果消费端处理消息失败,没有手动显示回复确认信号, 则RabbitMQ不会在内存或硬盘中删除该消息,导致该消息会阻塞在队列中,后续的消息也会被阻塞住导致消息无法消费。
对于上面出现的问题,我们可以使用RabbitMQ提供的补偿机制和死信队列来实现消费失败的消息保存。
rabbitMQ提供配置参数来开启消费者重试机制,也能配置配置最大重试次数和重试间隔时间,rabbitMQ对于消息消费失败达到一定次数后,就会放弃该消息,我们可以手动实现,如果消费失败达到最大重试次数后,将数据转发到死信队列上,由死信队列的消费者来实现消息的持久化到数据库或者日志文件中,一般重试次数我们设置为3此,间隔时间为5s。
也可以消费者手动实现业务层判断,如果同一消息消费了多次,则可以手动拒绝该消息,然后该消息自动进入死信队列,这样基于rabbitMQ的死信队列的特性来自动实现消息的保存,优点是rabbitMQ的封装框架不需要进行额外的代码,性能方面有了保障,缺点是这个工作交给了消费者手动实现。具体的还得看实际业务的需求。
5. 如何避免消息的重复消费和重复投递
在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费
6. 消息如何分发(路由)
个人觉的消息分发和消息路由属于同一问题,生产者将消息发送到Exchange上,然后Exchange将消息路由到一个或者多个队列上,如果路由不到,将消息根据发送者确认机制回传给生产者,或者直接丢弃;消费者订阅队列上的消息,以上就是消息的流经的整个流程
- 生产者链接到RabbitMQ,建立一个Connection,然后基于该Connection开启Channel
- 生产者声明一个Exchange,并设置相关属性(Exchange类型fanout、direct、topic和headers、是否持久化)
- 生产者声明一个Queue,并设置相关属性(是否持久化)
- 生产者将消息发送给Exchange,一般会指定一个RoutingKey,用来指定这个消息的路由规则,而这个RoutingKey需要与Exchange类型和BindingKey联合作用才能最终生效
- 相应的Exchange根据接收到的消息的路由键查找匹配的队列,如果匹配上将消息发送到队列上,如果查询不到根据发送者确认模式,是否返回给发送者或者丢掉
- 消费者链接到RabbitMQ,建立一个Connection,然后基于该Connection开启 Channel
- 消费者订阅队列,根据autoAck是否为false来决定是否手动确认消息
- RabbitMQ服务接收到消息确认,删除队列中该消息
7. 如何确保消息不丢失
消息丢失有以下三种情况:
- 生产者发送消息到RabbitMQ中,如果没有对应Exchange、或者Exchange没有匹配队列,或者队列没有任何消费者都可能导致消息的丢失
对于发送失败的,我们可以使用生产者确认机制来让发送失败的消息回传给生产者,或者使用备份交换机的方式来处理发送失败的消息
2. rabbitMQ服务重启、关闭、宕机情况下导致的消息丢失
RabbitMQ持久化包括三个部分:Exchange的持久化、Queue的持久化和Message的持久化
我们要持久化消息,则必须持久化Queue,因为Message是存储在Queue上的,如果Queue不持久化的话,Message即便是持久化了,重启服务也会因为没有存储的载体导致Message的丢失
这里注意下,将所有的消息持久化,这样会严重影响RabbitMQ的性能,对于可靠性不是那么高的消息可以不采用持久化来提高系统整体的吞吐量
3. 消费者设置autoAck为true,可能导致消费者还没有来得及消费就宕机了,其实也是变相的消息丢失
这个需要我们在消费消息时,设置autoAck为true,同时注意解决消费异常的情况,具体的参考面试题4