【读书笔记-消息队列-Java高级】

文章目录


前言

记录一些自己对消息队列的理解和看法,有不对的地方欢迎指正,有重要知识点遗漏之处欢迎补充。

一、什么是消息队列?

顾名思义它是一个存储消息队列。

二、为什么使用消息队列

1.为啥要用消息队列

很多人都在项目中使用到了消息队列,但是其实很多人都不知道为什么要用消息队列,或是说为什么这个业务场景下要用消息队列,很多时候我们都是为了用而用,但是我们应该去思考为什么要用消息队列,消息队列主要有三个核心用处:解耦、异步、削峰

解耦

看这么个场景。A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?就需要改A系统的代码增加对E系统的调用,如果D系统又不需要了呢,又需要删除A系统中对D系统的调用,新增和这样的代码显然耦合性太高。

在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办,调用失败咋办?头发都白了啊!

如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A系统就不用再调用其他系统,就不需要维护调用那一段代码,也不需要考虑人家是否调用成功、失败超时等情况,某个系统有改动只会影响到它那一个系统,对其它系统都没有影响,其实这就做到了A系统和其它系统的解耦合。

总结:通过一个MQ,发布订阅消息这么一个模型,A系统就与其它系统彻底解耦了。

面试技巧:你需要去考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的,你就需要去考虑在你的项目里,是不是可以运用这个 MQ 去进行系统的解耦。在简历中体现出来这块东西,用 MQ 作解耦。

异步

再来看一个场景,A系统接收一个请求,这个请求的处理中需要调用B系统进行短信发送,需要调用C系统的统计,需要调用D系统的某个功能,但是BCD系统这三个接口调用都很耗费时间,其实A系统的处理时间是很短的,如果按照这样的处理用户发起一个请求要等1s才能得到响应。这对用户来说几乎不可接受。

如果使用MQ ,A系统只需要向MQ发送一条消息,BCD系统都从MQ消费这个消息,B系统取到消息后去发送短信,C系统去做统计,D系统去做它的事,而A系统就只需要消耗3ms+发送消息的时间(发送消息的时间是很短的),对于用户而言,其实感觉上就是点个按钮,5ms 以后就直接返回了,爽!网站做得真好,真快!

总结:你可以想在你的系统中是否有类似,发送短信,做统计的一些事,这些其实可以利用MQ异步的特性,将消息发送到MQ,然后短信服务消费MQ消息发送短信,统计服务消费MQ消息进行统计。其实一些与业务相关性不大又比较耗时的接口调用完全可以使用MQ异步处理。

削峰

再看一个场景,一个接口的请求瞬间的并发请求量很大,特别是一些特惠活动,比如1s内有5k的请求访问接口,但是我们的接口1s能承受的最大请求量是1k,因为我们的系统也是基于mysql,瞬间承受不了这么大的并发量,这样很容易把我们的系统、数据库搞崩,这样的场景我们可以用将用户的请求延时处理,将请求消息发送到MQ,每秒将这5k的请求写入MQ,启动一个消费者来慢慢消费这些信息,这样可能会有一个问题,我们的消费速度比发送消息的速度慢,可能会导致消息积压,可能会有几十万甚至上百万的请求消息积压在MQ里,但是这些活动接口大部分都是特定时间并发量很大,过了这个特定时间平时几乎没有什么请求量,所以短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就只有比如 20 个请求进 MQ,但是系统依然会按照每秒 1k 个请求的速度在处理。所以说,只要高峰期一过,系统就会快速将积压的消息给解决掉。

总结:对于一些并发量很大的接口可以利用消息队列削峰的特性,将请求信息发到MQ后再以系统稳定的速度做处理,这样可以把瞬间的大量请求分摊下来,同时我们也可以思考,在我们的项目中是否存在这样高并发的接口,如果有我们能不能尝试去用MQ来优化它。

2.消息队列有啥优缺点

优点上面已经说了,就是在特殊场景下有其对应的好处解耦异步削峰

缺点有以下几个:

1.系统可用性降低:系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,ABCD 四个系统还好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整?MQ 一挂,整套系统崩溃,你不就完了?所以在我们引入MQ的时候一定要考虑它的可用性,其实在引用任何中间件的时候我们都应该考虑它的可用行,生产环境出不得岔子,引入的组件一定要保证高可用。

2.系统复杂度提高:硬生生加个 MQ 进来,你怎么保证消息不被重复消费?怎么处理消息丢失的情况,怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。

3.一致性问题:A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。

总结:所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。

2.Kafka、ActiveMQ、RabbitMQ、RocketMQ有什么优缺点?

特性ActiveMQRabbitMQRocketMQKafka
单机吞吐量万级,比 RocketMQ、Kafka 低一个数量级同 ActiveMQ10 万级,支撑高吞吐10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景
topic 数量对吞吐量的影响topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topictopic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源
时效性ms 级微秒级,这是 RabbitMQ 的一大特点,延迟最低ms 级延迟在 ms 级以内
可用性高,基于主从架构实现高可用同 ActiveMQ非常高,分布式架构非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性有较低的概率丢失数据基本不丢经过参数优化配置,可以做到 0 丢失同 RocketMQ
功能支持MQ 领域的功能极其完备基于 erlang 开发,并发能力很强,性能极好,延时很低MQ 功能较为完善,还是分布式的,扩展性好功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用

综上,各种对比之后,有如下建议:

一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,人不推荐用这个。

后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高。

不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。

如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。

三、保证消息队列的高可用

因为Kafak、RabbitMQ、ActiveMQ很少用到,现在我用的比较多的是RocketMQ,所以我在网上拿了一张RocketMQ的分布式高可用架构图。

 引入消息队列后,我们要考虑消息队列的可用性,在生产中,没人使用单机模式的消息队列。因此作为一个合格的程序员,应该对消息队列的高可用有很深刻的了解。

简单描述一下,Broker是存放消息的角色,NameServer可以看做一个注册中心这样一个角色,它存放了各个Broker的信息,不管是生产者还是消费者都会和NameServer保持一个长链接,从里面获取Broker的路由信息,生产者只能将消息发送到Broker maseter(主节点),而消费者可以从Broker master和Broker slave(从节点)订阅消息。其实这幅图中我们可以看到RocketMq的高可用是采用的多个主节点多个从节点的架构,其实这是很多中间普遍使用的分布式架构,这种多主多从的架构有很多优点,比如数据量太大了目前服务器承载不下,则需要增加服务器节点,那么增加一个主节点到集群中,相当于又多了一个可以存储数据的节点,所以横向扩展很方便,只不过要处理好数据分片规则以及数据迁移等事项,这样不但可以将大量数据保存下来,还能提高效率(因为提供服务的节点又多了,相对与一个节点来说平摊下来的访问量就少了),从节点是主节点的一个副本,当主节点挂了从节点就顶替主节点,让系统能够继续运行,这样的架构就保证了系统的可用性。

四、保证消息的可靠性

RocketMQ来说,如何保证消息的可靠性传递?RocketMQ的消息存储结构如下图所示:


消息队列存储的最小单位是消息Message。同一个Topic下的消息映射成多个逻辑队列。不同Topic的消息按照到达broker的先后顺序以Append的方式添加至CommitLog,顺序写,随机读。顺序写是可以大大提升写入效率。此外采用了ConsumeQueue中间结构来存储偏移量信息,实现消息的分发。由于ConsumeQueue结构固定且大小有限,在实际情况中,大部分的ConsumeQueue 能够被全部读入内存,可以达到内存读取的速度。此外为了保证CommitLog和ConsumeQueue的一致性, CommitLog里存储了Consume Queues 、Message Key、Tag等所有信息,即使ConsumeQueue丢失,也可以通过 commitLog完全恢复出来,这样只要保证commitLog数据的可靠性,就可以保证Consume Queue的可靠性。

简单说一下,在RocketMQ中我们如何保证消息的可靠性传递:

首先,要保证发送的消息一定要保存到磁盘上,首先保证消息的持久化配置是开启的,其次RocketMQ有一个刷盘的机制,同步刷盘和异步刷盘,在它的配置文件中我们可以将其配置为同步刷盘,也就是说我们发送消息到RocketMQ,MQ将消息保存到磁盘(而不仅仅是内存)后才返回给我们消息已经发送成功,这样其实是稳妥的(同步刷盘性能会受到影响,没有两全之法)。

其次,要保证消费消息成功才舍弃此消息,消费者消费成功返回CONSUME_SUCCESS消费队列才会删除此消息。

总结:要保证消息的可靠性传递,简单说就是发送消息一定要成功,消费消息一定要成功。RocketMQ的数据存储是放在磁盘上的,一个消息的持久化存储简单说三个步骤,生产者发送消息->内存->磁盘,所以发送消息成功就是一定要成功把消息保存到磁盘里,不然服务器突然断电或出现故障内存中的消息就会丢失,只有保存在磁盘里才是稳当的,那么如何保证消费一定成功呢,试想一下如果消费者正在消费一个消息突然出现故障(断电等异常因素),那么这个消息等同于消费失败了,然而消息队列认为你已经消费了此消息,这其实就没能保证消费成功,所以我们自己也可以思考一下,如何才能保证消费一定能成功呢?我们可以给消息队列一个消费消息的响应,比如消息正常消费完成,我们告诉消息队列这个消息我消费了,你可以删了,消息队列收到消息自然就知道这个消息是消费成功了,如果我们没有给MQ任何响应或者是异常响应,那么MQ将继续保留这个消息,等待下一次消费,这样就保证了消息一定能消费成功,其实这些解决方案就类似于我们解决生活中的一些问题,只要你去思考其实答案就在我们身边,先不管自己的解决方案是否最优,能解决问题就已经很棒了,最优方案大多是经过千锤百炼才出得来的。

五、保证消息不被重复消费

举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。

其实重复消费不可怕,可怕的是你没考虑到重复消费之后,怎么保证幂等性

幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错

其实还是得结合业务来思考,我这里给几个思路:

业务中自己判断去重:比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。

Set去重:比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。

全局ID:比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。

数据库唯一键:比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。

当然还有其它的办法,方便可靠就行。

六、保证消息的顺序性

举个例子,往MQ里发送增加、修改、删除三条信息,消费的时候你愣是换了顺序给执行成删除、修改、增加,不全错了么。本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。

先看看顺序会错乱的俩场景:

RabbitMQ:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者 2 先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。


Kafka:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。
消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞多个线程来并发处理消息。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。

有的消息队列中的消息是实现了有序性的,有的消息队列中的消息是无序的,比如Kafka中的消息就是有序的。

解决方案:其实我们的目的很简单,就是想发送消息的顺序和消费顺序是一致的。就拿RabbitMQ来说它队列中的消息是无序的,我们可以一个队列对应一个消费者,同一业务需要保证顺序的消息我们发往一个队列,消费的时候只让一个消费者去消费此队列,这个消费者内部用内存队列对消息做排序(排序规则可以是发送消息时的当前时间戳),然后分发给不同的线程来处理。注意,这里消费者不直接消费消息,而是将消息根据关键值(比如:订单 id)进行哈希,哈希值相同的消息保存到相同的内存队列里。也就是说,需要保证顺序的消息存到了相同的内存队列,然后由一个唯一的 worker 去处理(分给多个队列多个线程取处理也是为了提高消费速度)。

七、消息堆积问题解决及消息堆积引发的消息过期问题解决

如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时怎么办?

大量消息在mq堆积了几个小时还没解决

几千万条数据在 MQ 里积压了几小时,这是我在项目中真实遇到的问题,因为消费者服务出现了问题,导致大量数据积压在MQ中,不过因为我们消费者的消费处理逻辑比较简单不耗时,所以我们重启消费者后很快就将这几千万数据消耗掉。但是如果消费这的处理逻辑并不简单很耗时该怎么办,这个时候修复consumer重新消费也要等待几个小时才能消费完毕,那有没有什么好的办法可以加快消费速度呢?

解决方案:一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:

先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。

新建一个 topic是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。

然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。

接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。

等快速消费完积压数据之后,得恢复原先部署的架构重新用原先的 consumer 机器来消费消息。

mq中的消息过期失效了

消息如果设置了过期时间,消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢

解决方案:面对这个问题,我们可以写一个消费者快速消费并记录下mq中的消息,先将mq中积压的信息消费掉避免消息过期被清理掉,然后我们可以等到系统压力不大的时候,比如凌晨我们将这些消息重新导入mq重新消费,将数据补回来。

mq快写满了

如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?

解决方案:面对mq快写满的情况,我们首先要做的就是先,处理掉积压的消息,给mq腾出空间,我们可以写一个消费者,对于不重要的消息我们直接丢弃,对于不能丢弃的消息我们也采用消费记录的方式,等到mq压力不大系统空闲的时候,将消息重新导入mq重新消费。

对于 RocketMQ,官方针对消息积压问题,提供了解决方案。

1.提高消费并行度

绝大部分消息消费行为都属于 IO 密集型,即可能是操作数据库,或者调用 RPC,这类消费行为的消费速度在于后端数据库或者外系统的吞吐量,通过增加消费并行度,可以提高总的消费吞吐量,但是并行度增加到一定程度,反而会下降。所以,应用必须要设置合理的并行度。 如下有几种修改消费并行度的方法:

同一个 ConsumerGroup 下,通过增加 Consumer 实例数量来提高并行度(需要注意的是超过订阅队列数的 Consumer 实例无效)。可以通过加机器,或者在已有机器启动多个进程的方式。 提高单个 Consumer 的消费并行线程,通过修改参数 consumeThreadMin、consumeThreadMax 实现。

2.批量方式消费

某些业务流程如果支持批量方式消费,则可以很大程度上提高消费吞吐量,例如订单扣款类应用,一次处理一个订单耗时 1 s,一次处理 10 个订单可能也只耗时 2 s,这样即可大幅度提高消费的吞吐量,通过设置 consumer 的 consumeMessageBatchMaxSize 返个参数,默认是 1,即一次只消费一条消息,例如设置为 N,那么每次消费的消息数小于等于 N。

3.跳过非重要信息

发生消息堆积时,如果消费速度一直追不上发送速度,如果业务对数据要求不高的话,可以选择丢弃不重要的消息。例如,当某个队列的消息数堆积到 100000 条以上,则尝试丢弃部分或全部消息,这样就可以快速追上发送消息的速度。

4.优化每条消息消费过程

优化消费者处理逻辑,减少耗时,提升消费速度。

八、如何设计一个消息队列

不需要我们去实现一个生产级别的消息队列(不需要重复造轮子,mq 肯定是很复杂的),现有的mq也有很多,完全没必要,其实只需要知道一个技术的基本原理、核心组成部分、基本架构构成,一个系统设计思路理清楚就ok。

比如说这个消息队列系统,我们从以下几个角度来考虑一下:

分布式可伸缩性:首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?

持久化:其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。

高可用:其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。

消息可靠性传递:能不能支持数据 0 丢失啊?可以的,就是前面我们说的消息可靠性传递。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值