消息队列算是十分重要的一个知识点,不管是在我们平时的日常工作中还是面试中。其中面试中总有一下几个核心问题:
上面这些问题都是我们会经常遇到的,在阐述这些知识前我们先来看一下消息队列基本术语和概念
消息队列其实就是一个使用队列来通信的组件,但是我们日常中所说的消息队列常常代指的是消息中间件。
主要是在业务不断扩张的情景下,需要有东西来解耦服务之间的共享、控制资源合理合时的使用以及缓冲流量洪峰。
为了面对这些问题,消息队列就应运而生了,常来实现:异步处理、服务解耦、流量控制。
针对调用链路太长,导致响应缓慢,使用消息队列可以减少请求的等待,还能让服务异步并发处理,提升系统总体性能。
一般会使用消息队列来解决系统之间耦合的问题,一个服务只需要把相关信息塞到消息队列中,下流系统谁需要就去订阅这个主题,就极大的解放了主服务。
核心思想就是削峰填谷,用消息队列做一个中间件来进行缓冲,防止大流量瞬间打崩,或者遇到一些不需要及时响应的后台任务,可以暂时放到消息队列中,依次处理。
有好处必然有坏处,多引入一个中间件系统的稳定性就下降一层,运维的难度抬高一层。因此要权衡利弊
消息队列有两种模型:队列模型和发布/订阅模型
生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者,但是消费者之间是竞争关系,即每条消息只能被一个消费者消费。
为了解决一条消息能被多个消费者消费的问题。该模型是将消息发往一个Topic即主题中,所有订阅了这个Topic的订阅者都能消费这条消息
队列模型每条消息只能被一个消费者消费,而发布/订阅模型就是为让一条消息可以被多个消费者消费而生的,当然队列模型也可以通过消息全量存储至多个队列来解决一条消息被多个消费者消费问题,但是会有数据的冗余。
发布/订阅模型兼容队列模型,即只有一个消费者的情况下和队列模型基本一致。RabbitMQ采用队列模型RocketMQ和Kafka 采用发布/订阅模型。
我们一般称发送消息方为生产者Producer,接收消息方为消费者Consumer,消息队列服务器为Broker
消息从Producer发往Broker,Broker将消息存储至本地,然后Consumer从Broker拉取消息,或者Broker推送消息至Consumer,最后消费。
为了提高并发度,往往发布/订阅模型还会引入队列或者分区的概念,即消息是发往一个主题下的某个队列或者某个分区中。在RocketMQ中叫做队列,在Kafka中叫做分区,但其本质是一样的
例如在某个主题下有五个队列,那么这个主题的并发度就提高为5,同时可以有5个消费者并行消费该主题的消息。一般可以采用轮询或者key hash取余等策略来将同一主题的消息分配到不同的队列中
与之对应的消费者一般都有组的感念,即消费者都是属于某个消费组的。一条消息会发往多个订阅了这个主题的消费组。
假设现在有两个消费组分别是Group1和Group2,它们都订阅了Topic-a。此时有一条消息发往Topic-a,那么这两个消费组都能接收这条消息。
然后这条消息实际是写入Topic某个队列中,消费组中的某个消费者对应消费一个队列的消息。
在物理上除了副本拷贝以外,一个消息在Broker中只会有一份,每个消费组会有自己的offset即消费点位来标识消费到的位置。在消费点位之前的消息表明已经消费过了。当然这个offset是队列级别的。每个消费组都会维护订阅的Topic下的每个队列的offset。
就我们市面上常见的消息队列而言,只要配置得当,我们的消息就不会丢失。
在消息传递中一共有三个阶段:生产消息、存储消息和消费消息。我们从这三个阶段分别入手来看看如何确保消息不会丢失。
生产者发送消息至Broker,需要处理Brock的响应,不论是同步还是异步发送消息,同步和异步回调都需要做好try-catch,妥善的处理响应,如果Brocker返回写入失败等错误消息,需要重试发送,当多次发送失败需要作报警,日志记录等。
这样就能保证在生产消息阶段消息不会丢失。
存储消息阶段需要在消息刷盘之后再给生产者响应,假设消息写入缓存中就返回响应,那么要是突然断电,就会导致生产者以为发送成功但是没有保存上的情况
如果Broker是集群部署,有多个副本机制,即消息不仅仅要写入当前Brock,还需要写入副本机中,那么配置成至少写入两个机子后再给生产者响应。
如果当消费者拿到消息后直接存入内存队列中就直接返回给Broker消费成功是不正确的。
如果需要拿到消息后放在内存中消费者宕机的情况,只有消费者真正执行完业务逻辑以后,再发送给Brocker消费成功,才是真正的消费了。
要保证消息的可靠性需要三方配合:
生产者需要处理好Brocker的响应,出错情况下利用重试、报警等手段
Broker需要控制响应的时机,单机情况下是消息刷盘后返回响应,集群多副本情况下,即发送至两个副本以上的情况下再返回响应。
消费者需要在执行完真正的业务逻辑之后再返回响应给Broker
当消息可靠性增强了,性能就会有所下降,等待消息刷盘,多副本同步后返回都会影响性能。因此还是有利有弊。
由于要保证消息的可靠性,所以我们生产者不能只发送消息,不去管Broker的响应,但是就存在Broker已经写入了,当时响应由于网络等原因没有收到,然后生产者又重发了一次,此时消息就重复了。
再看消费者消费的时候,假设我们消费者拿到消息消费了,业务逻辑已经走完了,事务也已经提交了,此时需要更新Consumer offset了,然后这个消费者挂掉了,另一个消费者顶上,此时由于Consumer offset还没更新,于是又拿到刚才那条消息,业务会又被执行一遍,于是消息又重复了。
我们可以看到对于正常业务而言消息重复是不可避免的,因此我们只能从另一个角度来解决重复消息的问题。
即相同的参数多次调用同一接口和调用一次产生的结果是一致的。常用的方法:
有序性分为:全局有序和部分有序
如果要保证消息的全局有序,首先只能由一个生产者往Topic发送消息,并且一个Tocip内部只能有一个队列(分区)。消费者也必须是单线程消费这个队列。这样的消息就是全局有序的。
不过一般情况下我们都不需要全局有序,即使是同步MySQL Binlog也只需要保证单表消息有序即可。
因此绝大部分的有序需求是部分有序,部分有序我们就可以将Topic内部划分成我们需要的队列数,把消息通过特定的策略发往固定的队列中,然后每个队列对应一个单线程处理的消费者。这样就完成了部分有序的需求,又可以通过队列数量的并发来提高消息处理效率。
消息堆积往往是因为生产者生产的速度与消费者的消费速度不匹配。有可能是因为消息消费失败反复重试造成的,也可能是消费者消费能力弱,渐渐的消息就积压了。
首先需要定位造成问题的原因:有bug则处理bug,如果是本身消费能力弱,就优化下消费逻辑,如批量插入等。
实在不行可以考虑水平扩容,增加Topic的队列数和消费者的数量。需要注意的是,队列数一点要增加,不然新增加的消费者是没东西消费的。一个Topic中一个队列只会分配给一个消费者