1 、哪些问题适合使 ⽤ 消息队列来解决?
异步处理。 可以更快响应请求并返回结果;减少等待,并行化的处理,提升系统总体的性能。
流量控制。 隔离上游直接交互,能根据下游处理速度自行调节流量,达到“削峰填⾕”的作⽤。但也增加调用链,增加系统复杂性。
令牌桶控制流量的原理是:单位时间内只发放固定数量的令牌到令牌桶中,规定服务在处理请求之前必须先从令牌桶中拿出⼀个令牌,如果令牌桶中没有令牌,则拒绝请求。
服务解耦。 无论增加还是减少下游系统,业务服务无需做任何更改,从而实现上下游服务解耦。
在订单系统中,主要使用场景就会服务解耦和异步化流程。比如:家长支付完成订单后,需要确认订单,也就是开始履约订单。在履约包含给用户增加课时,发送上课短信提醒等。
2 、该如何选择消息队列?
产品考虑标准: 是否开源,社区是否活跃,功能是否完善等方面综合考虑。
常用消费队列产品分析:
RabbitMQ是采用Erlang语言编写的支持AMQP协议的消息队列,支持队列模型,通过exchange模块实现生产消息发送到不同队列,供多个消费者消费。其优点:轻量级容易部署。缺点:对消息堆积支持不好,处理数据qps在几万到几十万,采用小众语言Erlang扩展和二次开发比较难。
RocketMQ是阿⾥开源的消息队列产品,采用Java语言开发,支持订阅发布模型。其中文社区活跃,使用java语言开发易于扩展,响应延时比较低适合在线业务。但周边生态集成比较差点。
Kafka是由LinkedIn开发开源产品,使用使⽤ Scala 和 Java 语⾔开发,采用了批量处理和异步思想设计,在海量数据处理方便性能比较,但是时延比较高。适合日志,监控等离线业务。
3 、消息模型中队列模型和订阅发布模型区别?
队列模型: 就是生产者发送消息就是入队,消费者消费消息就是出队。服务端容器就是队列。如果需要将发送消费供多个消费者消费,就只能通过发送到多个队列的方式解决。RabbitMQ采用的就是队列模型,只是通过Exchange模块来解决多个消费者的问题。
同⼀份消息如果需要被多个消费者来消费,需要配置Exchange将消息发送到多个队列,每个队列中都存放⼀份完整的消息数据,可以为⼀个消费者提供消费服务。
订阅发布模型: 消息发送方称为发布者,消费接收方称为订阅者,存放消费的容器称为主题。发布者将消费发送到主题,订阅者在接收消息之前需要先订阅主题,每个订阅者都能接收到主题所有消费。
RocketMQ和Kafka采用的就是订阅发布模型。
4 、RocketMQ消息模型
生产阶段,⽣产者先将消息发送给服务端,也称Broker。Broker收到消息后,将消息写入队列并给生产者返回确认响应。
消费阶段,消费者接收到消息之后,完成自己的业务逻辑后,给broker返回消费成功响应。
其实消费者是基于消费者组的方式进行订阅的,一个消费者组内可以包含多个消费者,每个消费者组都可以消费消息。一条消息可以被定于的多个消费者组消费,但是一条消息只能被消费者组内的一个消费者消费。在RocketMQ中会为每个队列维护一个消费位置offSet,用来标识这个位置之前的消息都是被消费过的,之后的都是未消费过的。
5 、如何确保消息不会丢失?
在生产和消费阶段通过确认机制来保证消息不丢失。在存储阶段通过消息刷盘落入磁盘方式(改成同步或者修改异步时间)和集群部署(主从模式多副本,高可用)来保证不丢失时。当然在生产阶段也需要处理异常的捕获及重发。消费阶段需要处理完成本地逻辑后,在返回确认响应。
6 、如何处理消费过程中的重复消息?
消息提供三种服务质量标准可以通过参数去配置,分别为至多一次,至少一次,恰好一次。来控制消费发送的。绝大部分消息队列都是推荐使用至少一次的方式。这样就可能会出现有重复的消息。对于消费端来说,需要通过幂等方式解决重复消费问题。引起原因大部分都是网络原因引起,如消费成功没有返回ack,导致下次重复。
7 、如果 ⽤ 幂等性解决重复消息问题?
幂等就是多次执行所产生的影响与一次执行的结果相同。常用幂等方法:
利⽤数据库的唯⼀约束实现幂等,如数据库主键或者唯一键,当然这个和业务耦合性比较高。或者全局锁,如redis的setNx锁。
如果是更新数据,也可以通过为更新数据设置前置条件的方式实现。比如订单从待支付改成支付成功
8 、消息积压了该如何处理?
消息积压出现的场景本质原因是由于消费速度和生产速度不匹配。即发送变快了或者消费变慢。 可以先来粗略的检查判断。如果是发送快了,消费方可通过扩容来解决。由于受一个队列只能由一个消费方消费限制,扩容时候需要考虑是否可以扩容已经最多扩容机器数。另一种方式就是降级生产方,减少一些非核心场景发送。如果是消费满了,扩容。如果查看消费也正常,但是还是出现堆积。需要考虑消费方是否出现了异常或者死锁,导致一直消费失败,没有返回ack。
追问:如果Consumer和Queue不对等,上线了多台也在短时间内无法消费完堆积的消息怎么办?
快速处理方式就是扩容,如果消费者和队列不对等,只能采用增加临时queue,修改代码解决。比如,可以新建一个临时topic,queue是原来的N倍,然后上线一台服务,将原topic的消息消费搬移到新的临时topic,不过任何处理,只进行数据迁移。上线N台消费者同时消费搬运过来的topic消息。改bug后,恢复到消费原来的topic继续消费。
追问:堆积时间过长消息超时了?
RocketMQ中的消息只会在commitLog被删除的时候才会消失,不会超时。也就是说未被消费的消息不会存在超时删除这情况。
追问:堆积的消息会不会进死信队列?
不会,消息在消费失败后会进入重试队列(%RETRY%+ConsumerGroup),到达重试次后18次才会进入死信队列(%DLQ%+ConsumerGroup)。
9 、如何让RocketMQ保证消息的顺序消费
RocketMQ只能保证同一个topic中同一个queue中的消息顺序性。发送消息的时候一个线程去发送消息,消费的时候 一个线程去消费一个queue里的消息。
追问:怎么保证消息发到同一个queue?
Rocket MQ给我们提供了MessageQueueSelector接口,可以自己重写里面的接口实现。
10 、如何利 ⽤ 事务消息实现分布式事务?
⾸先,订单系统在消息队列上开启⼀个事务。
然后,订单系统给消息服务器发送⼀个“半消息”,和普通唯一区别未提交事务前,消费者不可见的。
第三,半消息发送成功后,订单系统就可以执⾏本地事务了。创建订单记录,并提交本地事务。
第四,根据本地事务结果决定提交或者回滚事务消息。
第五,如果订单创建成功,那就提交事务消息,购物⻋系统就可以消费到这条消息继续后续的流程。如果订单创建失败,那就回滚事务消息,购物⻋系统就不会收到这条消息。
针对第四步提交事务消息时失败了怎么办?
Kafka 的解决⽅案⽐较简单粗暴,直接抛出异常,由用户自行处理,或者重试,或者删除。
RocketMQ 增加了事务反查的机制来解决事务消息提交失败的问题。如果Producer 也就是订单系统,在提交或者回滚事务消息时发⽣⽹络异常,RocketMQ 的 Broker没有收到提交或者回滚的请求,Broker 会定期去 Producer 上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。
11 、RocketMQ 中的分布式事务实现
RocketMQ分布式事务中,需要业务提供一个反查本地事务状态的接口,告知RocketMQ本地事务是成功还是失败。反查类实现RocketMQLocalTransactionListener接口,完成executeLocalTransaction和checkLocalTransaction方法逻辑。反查逻辑不依赖发送方,broker会去定时查询topic对应的生产者,如果有一个服务宕机了,也不影响,会在下一次请求到别的机器,最终确保事务的完整性。
RocketMQ实现核心:
a、Half Message:预处理消息,当broker收到此类消息后,会存储到RMQ_SYS_TRANS_HALF_TOPIC的消息消费队列中。
b、检查事务状态:Broker会开启一个定时任务,消费RMQ_SYS_TRANS_HALF_TOPIC队列中的消息,每次执行任务会向消息发送者确认事务执行状态(提交、回滚、未知),如果是未知,Broker会定时去回调在重新检查。
c、超时:如果超过回查次数,默认回滚消息。
也就是它并未真正进入Topic的queue,而是用了临时queue来放所谓的half message,等提交事务后才会真正的将half message转移到topic下的queue。