消息队列(MQ)
一、为什么要用消息队列(消息队列的应用场景)
- 应用解耦
- 异步任务
- 流量削峰
问题背景:学生向老师请教问题。如果学生A正在向老师请教问题,那么后面的学生依次排队等候,直到轮到自己请教问题。这样的模式会使整个系统的效率较低,学生排队等待时间太久,而且排队学生过多,老师也会忙不过来。引入消息队列后,即右图的班长,班长负责将所有学生的问题汇总,即学生按照固定的格式将请教的问题写在纸上,并按照先后顺序交给班长,即可做自己的事情去了。老师可以从班长那里依次获取问题,并逐一进行解答。
解决的问题:
(1)学生不需要直接面对老师,学生和老师通过班长进行通信,即解决了耦合调用的问题。
(2)老师无需当场回答问题,可以按照自己的处理进度依次回答问题,即解决了异步通信的问题。
(3)当提问的学生数量过多,通过班长(MQ)可以抵御洪峰流量,达到保护主业务的目的。
总结:
(1)解耦
系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用支付系统,库存系统,物流系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。
在使用消息队列(MQ)解耦后,系统的耦合性就会提高了。比如物流系统发生故障,需要几分钟才能修复,在这段时间内,物流系统要处理的数据被缓存到消息队列中,用户的下单操作正常完成。当物流系统恢复后,补充处理存在消息队列中的订单消息即可,终端系统感知不到物流系统发生过几分钟故障。
(2)异步
A系统接收一个请求,需要在自己本地写入数据库,还需在B,C,D三个系统中写入数据库,如采用直接调用方式,最终的请求总延时非常高,用户体验感很差。如果使用了消息队列(MQ),系统A接收到请求后,只需连续发送3条消息到消息队列(MQ)中,即可返回响应给用户,而无须等待B,C,D三个系统的响应结果。
一个系统跟另一个系统之间进行通信的时候,假如系统A希望发送一个消息给系统B,让他去处理,但是系统A不关注B到底怎么处理或者有没有处理好,所以系统A将消息发送给消息队列(MQ),然后就不管这条消息的“死活”了,接着B从消息队列里取出消息进行处理即可。至于怎么处理,是否处理完毕,什么时候处理,都是系统B的事儿,与系统A无关。
(3)削峰
应用系统如果遇到系统请求流量的瞬间猛增,有可能将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提高系统的稳定性和用户体验。
流量削峰的经济考量:
业务系统正常时段的QPS如果是1000,流量最高峰时10000,为了应对流量高峰配置高性能的服务器显然不划算,这样可以使用消息队列对峰值流量削峰
二、各种消息队列产品的比较
小结:
- ActiveMQ,早期使用较多,没有经过大规模吞吐量场景的验证,社区也不是很活跃,但是现在确实大家用的不多了,不推荐。
- RabbitMQ,开发语言erlang,阻止了大量的Java工程师去深入研究和掌握它,对公司而言,几乎处于不可控的状态,但是RabbitMQ是开源的,比较稳定的支持,活跃度也高,如不考虑二次开发,追求性能和稳定性,推荐使用。
- RocketMQ,开发语言是Java,在阿里内部经受过高并发业务的考验,稳定性和性能均不错,考虑后期可能二次开发,推荐使用
- Kafka,大数据领域的实时计算,日志采集等场景,用Kafka是业内标准的,社区活跃度很高,推荐使用。大数据领域日志采集等业务推荐使用。
三、消息队列的优点和缺点
优点:
- 解耦
- 异步
- 削峰
缺点:
- 系统可用性降低
- 系统复杂度提高
- 一致性问题
(1)系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦消息队列(MQ)宕机,就会对业务造成影响。
如何保证MQ的高可用?
(2)系统复杂度提高
MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。
消息丢失怎么办?
重复消息怎么处理?
如何保证消息传递的顺序性?
(3)一致性问题
A系统处理完业务,通过MQ给B,C,D三个系统发消息数据,如果B系统,C系统处理成功,D系统处理失败。
如何保证消息数据处理的一致性?
四、如何保证消息队列的高可用
项目中引入MQ导致系统的可用性降低,面试官想知道的是面试者针对可用性降低的思考和解决思路。
(1)RabbitMQ高可用---镜像集群
- 在多台机器上分别启动RabbitMQ实例
- 多个实例之间可以相互通信
- 每次生产者写消息到queue的时候,都会自动把消息同步到多个实例的queue上。每个RabbitMQ节点上都有queue的消息数据和元数据。
- 某一个节点宕机,其他节点依然保存完整数据,不影响客户端消费。
(2)RocketMQ高可用---双主双从
- 生产者通过Name Server发现Broker(生产者发送消息时首先会询问name server,给name server给其一个borker地址,随后生产者发送消息到broker)
- 生产者发送队列消息到2个Broker
- Broker主节点分别和各自从节点同步数据(当borker master1宕机,其从节点borker slave1也有备份数据,不影响正常的服务;与此同时,borker master1宕机,由于其从节点不能接收生产者的消息,但是另外一个主节点borker master2可以接收消息,不影响正常的服务)
- 消费者从主或从节点订阅消息
五、如何保证消息不丢失
消息丢失的原因:
情况一:消息生产者没有成功发送到MQ。
情况二:消息发送给MQ,但是MQ宕机导致内存中的消息数据丢失。
情况三:消费者获取到消息,但消费者还没有来得及处理宕机了,但此时MQ中的消息已经删除,消费者重启后不能再消费之前的消息。
确保消息不丢失的方案:
- 消息发送者发送给MQ后,MQ给生产者确认收到
- MQ收到消息后进行消息持久化
- 消费者收到消息处理完毕后手动进行ack确认
- MQ收到消费者ack确认后删除持久化的消息
总结:
消息丢失的原因:
- 发送方,MQ,消费方都有可能导致消息丢失。
如何保证消息不丢失:
- 发送方可靠发送
- MQ进行消息持久化
- 消费方消费完毕后进行ACK确认,MQ收到消费方的ACK确认再删除本地消息
六、如何保证消息不被重复消费(幂等性)
消息重复的根本原因是网络不可达。
发送时消息重复
当一条消息已被成功发送到服务端,此时出现网络闪断,导致服务端对客户端应答失败。如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同的消息。
消费时消息重复
消息消费的场景下,消息已投递到消费者并完成业务处理,当消费方给MQ服务端反馈应答的时候网络闪断。为了保证消息至少被消费一次,MQ服务端在网络恢复后再次尝试投递之前已被消费方处理过的消息,此时消费者就会收到两条内容相同的消息。
解决方案:
- 消息发送者发送消息时携带一个全局唯一的消息id
- 消费者获取消费后先根据id在Redis/DB中查询是否存在消费记录
- 如果没有消费过就正常消费,消费完毕后写入Redis/DB
- 如果消息消费过就直接舍弃
七、如何保证消息消费的顺序性
消息的顺序性消费:
消息有序指的是按照消息的发送顺序来消费,例如一笔订单产生了3条消息,分别是订单创建,订单付款,订单完成。消费时,要按照顺序依次消费才有意义。与此同时,多笔订单之间有时可以并行消费的。
局部顺序消费:
- 生产者根据消息ID将同一组消息发送到同一个Queue中。
- 多个消费者同时获取Queue中的消息进行消费。
- MQ使用分段锁保证单个Queue中的有序消费。(分段锁保证队列中的消息按顺序执行,当一个消息出队列被消费时,此时该队列被分段锁锁起来,直到该消息被消费完成,分段锁打开,继续消费第二个消息)
八、基于MQ的分布式事务实现
- 用户提交订单
- 库存服务操作库存DB,减库存
- 订单服务操作订单DB,生成订单数据
- 库存服务和订单服务要么同时成功,要么同时失败
本质上讲,分布式事务就是为了保证不同数据库的数据一致性。
消息的发送方在处理完自己的业务后,将消息首先保存到本地,并将消息状态置为1(代表消息未处理),接下来将消息发送到消息队列(MQ)中,消息的消费方监听到消息后,为了保证消息的幂等性(消息不被重复消费),首先判断是否为重复消息,若消息未被执行,则执行相应的业务逻辑,并将消息记录到本地数据库中,最后向MQ发送消息,以更改发送方消息的状态(将消息的状态置为2)。
特殊情况:若消息在发送方执行成功,但是在消息的消费方执行失败,此时两边业务系统的数据不一致。消息发送方的定时任务定时扫描消息发送方的本地消息表中teye=1(未处理的消息)的消息,并将该消息继续发送到消息队列(MQ)中,以供消费方重新消费。若消息未被成功消费,则定时任务一直发送消息到MQ中,这样可以保证所有消息一定会被消费。
总结:
(1)消息发送方:
- 处理业务逻辑
- 保存消息到本地数据库
- 发送消息给MQ
- 监听MQ消息方通知消息,更改消息状态为已处理
- 定时任务将长期未处理消息重新发送到MQ
(2)消息消费方:
- 监听MQ中间件消息
- 判断消息是否重复,重复就丢弃
- 消息未重复,执行本地业务
- 业务处理完毕,写消息记录到本地数据库
- 发送通知消息到MQ