消息队列面试连环问:如何保证消息不丢失?处理重复消息?消息有序性?消息堆积处理?

你们好,我是 yes。java

最近我一直扎在消息队列实现细节之中没法自拔,已经写了 3 篇Kafka源码分析,还剩不少没肝完。以前还存着RocketMQ源码分析还没整理。今儿暂时先跳出来盘一盘大方向上的消息队列有哪些核心注意点。web

核心点有不少,为了更贴合实际场景,我从常见的面试问题入手:面试

  • 如何保证消息不丢失?
  • 如何处理重复消息?
  • 如何保证消息的有序性?
  • 如何处理消息堆积?

固然在剖析这几个问题以前须要简单的介绍下什么是消息队列,消息队列常见的一些基本术语和概念算法

接下来进入正文。数据库

什么是消息队列

来看看维基百科怎么说的,顺带学学英语这波不亏:后端

In computer science, message queues and mailboxes are software-engineering components typically used for inter-process communication (IPC), or for inter-thread communication within the same process. They use a queue for messaging – the passing of control or of content. Group communication systems provide similar kinds of functionality.缓存

翻译一下:在计算机科学领域,消息队列和邮箱都是软件工程组件,一般用于进程间或同一进程内的线程通讯。它们经过队列来传递消息-传递控制信息或内容,群组通讯系统提供相似的功能。服务器

简单的归纳下上面的定义:消息队列就是一个使用队列来通讯的组件微信

上面的定义没有错,但就如今而言咱们平常所说的消息队列经常指代的是消息中间件,它的存在不只仅只是为了通讯这个问题。网络

为何须要消息队列

从本质上来讲是由于互联网的快速发展,业务不断扩张,促使技术架构须要不断的演进。

从之前的单体架构到如今的微服务架构,成百上千的服务之间相互调用和依赖。从互联网初期一个服务器上有 100 个在线用户已经很了不起,到如今坐拥10亿日活的微信。咱们须要有一个「东西」来解耦服务之间的关系、控制资源合理合时的使用以及缓冲流量洪峰等等。

消息队列就应运而生了。它经常使用来实现:异步处理、服务解耦、流量控制

异步处理

随着公司的发展你可能会发现你项目的请求链路愈来愈长,例如刚开始的电商项目,能够就是粗暴的扣库存、下单。慢慢地又加上积分服务、短信服务等。这一路同步调用下来客户可能等急了,这时候就是消息队列登场的好时机。

调用链路长、响应就慢了,而且相对于扣库存和下单,积分和短信不必这么的 "及时"。所以只须要在下单结束那个流程,扔个消息到消息队列中就能够直接返回响应了。并且积分服务和短信服务能够并行的消费这条消息。

能够看出消息队列能够减小请求的等待,还能让服务异步并发处理,提高系统整体性能

服务解耦

上面咱们说到加了积分服务和短信服务,这时候可能又要来个营销服务,以后领导又说想作个大数据,又来个数据分析服务等等。

能够发现订单的下游系统在不断的扩充,为了迎合这些下游系统订单服务须要常常地修改,任何一个下游系统接口的变动可能都会影响到订单服务,这订单服务组可疯了,真 ·「核心」项目组

因此通常会选用消息队列来解决系统之间耦合的问题,订单服务把订单相关消息塞到消息队列中,下游系统谁要谁就订阅这个主题。这样订单服务就解放啦!

流量控制

想必你们都听过「削峰填谷」,后端服务相对而言都是比较「弱」的,由于业务较重,处理时间较长。像一些例如秒杀活动爆发式流量打过来可能就顶不住了。所以须要引入一个中间件来作缓冲,消息队列再适合不过了。

网关的请求先放入消息队列中,后端服务尽本身最大能力去消息队列中消费请求。超时的请求能够直接返回错误。

固然还有一些服务特别是某些后台任务,不须要及时地响应,而且业务处理复杂且流程长,那么过来的请求先放入消息队列中,后端服务按照本身的节奏处理。这也是很 nice 的。

上面两种状况分别对应着生产者生产过快和消费者消费过慢两种状况,消息队列都能在其中发挥很好的缓冲效果。

注意

引入消息队列当然有以上的好处,可是多引入一个中间件系统的稳定性就降低一层,运维的难度抬高一层。所以要权衡利弊系统是演进的

消息队列基本概念

消息队列有两种模型:队列模型发布/订阅模型

队列模型

生产者往某个队列里面发送消息,一个队列能够存储多个生产者的消息,一个队列也能够有多个消费者, 可是消费者之间是竞争关系,即每条消息只能被一个消费者消费。

发布/订阅模型

为了解决一条消息能被多个消费者消费的问题,发布/订阅模型就来了。该模型是将消息发往一个Topic即主题中,全部订阅了这个 Topic 的订阅者都能消费这条消息。

其实能够这么理解,发布/订阅模型等于咱们都加入了一个群聊中,我发一条消息,加入了这个群聊的人都能收到这条消息。那么队列模型就是一对一聊天,我发给你的消息,只能在你的聊天窗口弹出,是不可能弹出到别人的聊天窗口中的。

讲到这有人说,那我一对一聊天对每一个人都发一样的消息不就也实现了一条消息被多我的消费了嘛。

是的,经过多队列全量存储相同的消息,即数据的冗余能够实现一条消息被多个消费者消费。RabbitMQ 就是采用队列模型,经过 Exchange 模块来将消息发送至多个队列,解决一条消息须要被多个消费者消费问题。

这里还能看到假设群聊里除我以外只有一我的,那么此时的发布/订阅模型和队列模型其实就同样了。

小结一下

队列模型每条消息只能被一个消费者消费,而发布/订阅模型就是为让一条消息能够被多个消费者消费而生的,固然队列模型也能够经过消息全量存储至多个队列来解决一条消息被多个消费者消费问题,可是会有数据的冗余。

发布/订阅模型兼容队列模型,即只有一个消费者的状况下和队列模型基本一致。

RabbitMQ 采用队列模型,RocketMQKafka 采用发布/订阅模型。

接下来的内容都基于发布/订阅模型

经常使用术语

通常咱们称发送消息方为生产者 Producer,接受消费消息方为消费者Consumer,消息队列服务端为Broker

消息从Producer发往BrokerBroker将消息存储至本地,而后ConsumerBroker拉取消息,或者Broker推送消息至Consumer,最后消费。

为了提升并发度,每每发布/订阅模型还会引入队列或者分区的概念。即消息是发往一个主题下的某个队列或者某个分区中。RocketMQ中叫队列,Kafka叫分区,本质同样。

例如某个主题下有 5 个队列,那么这个主题的并发度就提升为 5 ,同时能够有 5 个消费者并行消费该主题的消息。通常能够采用轮询或者 key hash 取余等策略来将同一个主题的消息分配到不一样的队列中。

与之对应的消费者通常都有组的概念 Consumer Group, 即消费者都是属于某个消费组的。一条消息会发往多个订阅了这个主题的消费组。

假设如今有两个消费组分别是Group 1Group 2,它们都订阅了Topic-a。此时有一条消息发往Topic-a,那么这两个消费组都能接收到这条消息。

而后这条消息实际是写入Topic某个队列中,消费组中的某个消费者对应消费一个队列的消息。

在物理上除了副本拷贝以外,一条消息在Broker中只会有一份,每一个消费组会有本身的offset即消费点位来标识消费到的位置。在消费点位以前的消息代表已经消费过了。固然这个offset是队列级别的。每一个消费组都会维护订阅的Topic下的每一个队列的offset

来个图看看应该就很清晰了。

基本上熟悉了消息队列常见的术语和一些概念以后,我们再来看看消息队列常见的核心面试点。

如何保证消息不丢失

就咱们市面上常见的消息队列而言,只要配置得当,咱们的消息就不会丢。

先来看看这个图,

能够看到一共有三个阶段,分别是生产消息、存储消息和消费消息。咱们从这三个阶段分别入手来看看如何确保消息不会丢失。

生产消息

生产者发送消息至Broker,须要处理Broker的响应,不管是同步仍是异步发送消息,同步和异步回调都须要作好try-catch,妥善的处理响应,若是Broker返回写入失败等错误消息,须要重试发送。当屡次发送失败须要做报警,日志记录等。

这样就能保证在生产消息阶段消息不会丢失。

存储消息

存储消息阶段须要在消息刷盘以后再给生产者响应,假设消息写入缓存中就返回响应,那么机器忽然断电这消息就没了,而生产者觉得已经发送成功了。

若是Broker是集群部署,有多副本机制,即消息不只仅要写入当前Broker,还须要写入副本机中。那配置成至少写入两台机子后再给生产者响应。这样基本上就能保证存储的可靠了。一台挂了还有一台还在呢(假如怕两台都挂了..那就再多些)。

那假如来个地震机房机子都挂了呢?emmmmmm...大公司基本上都有异地多活。

那要是这几个地都地震了呢?emmmmmm...这时候仍是先关心关心人吧。

消费消息

这里常常会有同窗犯错,有些同窗当消费者拿到消息以后直接存入内存队列中就直接返回给Broker消费成功,这是不对的。

你须要考虑拿到消息放在内存以后消费者就宕机了怎么办。因此咱们应该在消费者真正执行完业务逻辑以后,再发送给Broker消费成功,这才是真正的消费了。

因此只要咱们在消息业务逻辑处理完成以后再给Broker响应,那么消费阶段消息就不会丢失。

小结一下

能够看出,保证消息的可靠性须要三方配合

生产者须要处理好Broker的响应,出错状况下利用重试、报警等手段。

Broker须要控制响应的时机,单机状况下是消息刷盘后返回响应,集群多副本状况下,即发送至两个副本及以上的状况下再返回响应。

消费者须要在执行完真正的业务逻辑以后再返回响应给Broker

可是要注意消息可靠性加强了,性能就降低了,等待消息刷盘、多副本同步后返回都会影响性能。所以仍是看业务,例如日志的传输可能丢那么一两条关系不大,所以不必等消息刷盘再响应。

若是处理重复消息

咱们先来看看能不能避免消息的重复。

假设咱们发送消息,就管发,无论Broker的响应,那么咱们发往Broker是不会重复的。

可是通常状况咱们是不容许这样的,这样消息就彻底不可靠了,咱们的基本需求是消息至少得发到Broker上,那就得等Broker的响应,那么就可能存在Broker已经写入了,当时响应因为网络缘由生产者没有收到,而后生产者又重发了一次,此时消息就重复了。

再看消费者消费的时候,假设咱们消费者拿到消息消费了,业务逻辑已经走完了,事务提交了,此时须要更新Consumer offset了,而后这个消费者挂了,另外一个消费者顶上,此时Consumer offset还没更新,因而又拿到刚才那条消息,业务又被执行了一遍。因而消息又重复了。

能够看到正常业务而言消息重复是不可避免的,所以咱们只能从另外一个角度来解决重复消息的问题。

关键点就是幂等。既然咱们不能防止重复消息的产生,那么咱们只能在业务上处理重复消息所带来的影响。

幂等处理重复消息

幂等是数学上的概念,咱们就理解为一样的参数屡次调用同一个接口和调用一次产生的结果是一致的。

例如这条 SQLupdate t1 set money = 150 where id = 1 and money = 100; 执行多少遍money都是150,这就叫幂等。

所以须要改造业务处理逻辑,使得在重复消息的状况下也不会影响最终的结果。

能够经过上面我那条 SQL 同样,作了个前置条件判断,即money = 100状况,而且直接修改,更通用的是作个version即版本号控制,对比消息中的版本号和数据库中的版本号。

或者经过数据库的约束例如惟一键,例如insert into update on duplicate key...

或者记录关键的key,好比处理订单这种,记录订单ID,假若有重复的消息过来,先判断下这个ID是否已经被处理过了,若是没处理再进行下一步。固然也能够用全局惟一ID等等。

基本上就这么几个套路,真正应用到实际中仍是得看具体业务细节

如何保证消息的有序性

有序性分:全局有序和部分有序

全局有序

若是要保证消息的全局有序,首先只能由一个生产者往Topic发送消息,而且一个Topic内部只能有一个队列(分区)。消费者也必须是单线程消费这个队列。这样的消息就是全局有序的!

不过通常状况下咱们都不须要全局有序,即便是同步MySQL Binlog也只须要保证单表消息有序便可。

部分有序

所以绝大部分的有序需求是部分有序,部分有序咱们就能够将Topic内部划分红咱们须要的队列数,把消息经过特定的策略发往固定的队列中,而后每一个队列对应一个单线程处理的消费者。这样即完成了部分有序的需求,又能够经过队列数量的并发来提升消息处理效率。

图中我画了多个生产者,一个生产者也能够,只要同类消息发往指定的队列便可。

若是处理消息堆积

消息的堆积每每是由于生产者的生产速度与消费者的消费速度不匹配。有多是由于消息消费失败反复重试形成的,也有可能就是消费者消费能力弱,渐渐地消息就积压了。

所以咱们须要先定位消费慢的缘由,若是是bug则处理 bug ,若是是由于自己消费能力较弱,咱们能够优化下消费逻辑,好比以前是一条一条消息消费处理的,此次咱们批量处理,好比数据库的插入,一条一条插和批量插效率是不同的。

假如逻辑咱们已经都优化了,但仍是慢,那就得考虑水平扩容了,增长Topic的队列数和消费者数量,注意队列数必定要增长,否则新增长的消费者是没东西消费的。一个Topic中,一个队列只会分配给一个消费者

固然你消费者内部是单线程仍是多线程消费那看具体场景。不过要注意上面提升的消息丢失的问题,若是你是将接受到的消息写入内存队列以后,而后就返回响应给Broker,而后多线程向内存队列消费消息,假设此时消费者宕机了,内存队列里面还未消费的消息也就丢了。

最后

上面的几个问题都是咱们在使用消息队列的时候常常能遇到的问题,而且也是面试关于消息队列方面的核心考点。今天没有深刻具体消息队列的细节,可是套路就是这么个套路,大方向上搞明白很关键。以后再接着写有关Kafka的源码分析文章,有兴趣的小伙伴请耐心等待。

往期推荐:

图解+代码|常见限流算法以及限流在单机分布式场景下的思考

Kafka请求处理全流程分析

Kafka索引设计的亮点:https://juejin.im/post/5efdeae7f265da22d017e58d

Kafka日志段读写分析:https://juejin.im/post/5ef6b94ae51d4534a1236cb0


我是 yes,从一点点到亿点点,咱们下篇见。


本文分享自微信公众号 - yes的练级攻略(yes_java)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索