目录
一、消息队列
1、定义
消息 + 队列 (Message + Queue) 简称MQ。消息队列本质就是个队列,FIFO先入先出,只不过队列中存放的内容是Message,从而叫消息队列。消息队列的主要用途就是在不同的服务、进程、线程之间进行通信。
2、消息队列的场景
1、消息队列-异步处理
在上面的工作流程中,我们可以看到同步和异步的过程中,是在库存后面不同,异步后面是一个消息队列存储所有的消息,然后分配给不同的线程进行异步处理。而同步方法中是将这三个任务一个一个进行处理。显而易见的就是异步方法是很快的。那么我们就可以更快的返回结果,减少其中的等待,实现并发处理,提升系统总体的性能。
2、消息队列-流量控制(削峰)
我们生活中常见的有秒杀活动,这种活动,会吸引大量的用户参与进来,比如规定好一个时间点,那么在这个时间点内会有大量的访问请求。那么我们的服务器会压力剧增。对于这种情况我们可以使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。
比如上面的流程中,我们使用消息队列,设置最大的限制数量,把所有的访问请求按着先后顺序进行存储起来,然后让后端服务只提取需要提取的数据就可以了。这样就保护了后端服务不被压垮。
3、消息队列-服务解耦
4、消息队列-发布订阅
在一些游戏中,我们经常能看到,一些玩家对全服进行喊话,或者系统进行播报的操作。这些展现出来的消息就是通过消息队列,按着先进先出的规则播报出来的。
3、基本概念和原理
1、Broker(代理)
消息队列中有个叫Broker,Broker(代理)是消息队列系统中的一个核心组件,扮演着消息中间件的角色。它负责接收、存储、路由和转发消息,确保消息从生产者(Producer)传递到消费者(Consumer)。当然可以包含多个Broker,可以以集群的方式放置。 通俗来讲就是MQ的服务器。
2、生产消费者
这个消息队列的两边肯定是有生产者(Producer)生产数据,传递给消息队列,然后消费者(Consumer)进行消费其中的数据。对于生产者就是生产完数据,主动将数据存放在消息队列中,但是对于消费者来说,是消息队列主动将数据传递给消费者,还是生产者主动去拉取数据呢?
我们假设是主动推送给消费者数据,根据我们上面所讲的,一个消息队列可以连接多个系统(消费者),当系统足够多的时候,我们的消息队列需要向多个系统发送数据,如果说消息队列的处理能力跟不上,那么消费者拿到数据的时间就会变长。但如果是消费者主动拉取消息队列中的数据的话,就没有这个问题。
3、点对点消息队列-线程池
线程池我之前的文章也讲过并且还有详细的代码讲解。生产者生产的消息固定传送给一个消息队列,然后消费者从这个队列中拉取数据,一条消息只能被消费一次,也就是一条消息只能被一个消费者拿到。
4、发布订阅消息-ToPic
发布订阅消息模型中,支持向一个特定的主题Topic发布消息,0个或多个订阅者接收来自这个消息主题的消息。 在这种模型下,发布者和订阅者彼此不知道对方。当发布者向这个主题发布消息,然后所有的订阅者会接收这个消息。
5、消息的ACK确认机制
为了保证消息的不丢失,消息队列中提供了消息的ACknowledge机制,即ACK机制。当消费者确认这个消息已经消费掉了,那么会向消息队列发送一个ACK,消息队列收到后会将这个消息进行删除。但是当系统宕机,消息队列并未收到ACK的话,消息队列会认为这个消息并未被消费掉,便会将这个消息继续发送给其他的消费者重新处理。这样ACK的实时性会牺牲一定的吞吐量。
4、消息队列产品
市面上最常用的消息队列就是RabbitMQ、RocketMQ、KafKa、ZeroMQ。
二、KafKa
1、介绍
2、架构
我们看Topic(主题)A,我们发现它有两个leader,并且分布在不同的Broker中,并且他们的副本也要分布在不同的Broker中,这样做就是为了高可用性。我们看图可以发现,生产者生产的数据只进入到了leader中,对于他们的副本来说,就相当于数据库的主从复制一样,会进行同步数据。当leader挂掉后,会选举新的leader,但是不会在同一个broker中(高可用性)。并且对于消费者读取数据也是从leader中读取数据。
3、分区和主题的关系
4、生产者
5、分区分配策略
一个消费者可以订阅多个主题,可以去消费多个分区,一个分区不支持多个消费者(同一个消费组)读取。 一个消费者组中有多个 consumer,一个 topic 有多个 partition,所以必然会涉及到 partition 的分 配问题,即确定那个 partition 由哪个 consumer 来消费。当消费者组里面的消费者个数发生改变的时 候,也会触发再平衡。
Kafka 有四种分配策略,Range、RoundRobin、Sticky、CooperativeSticky,可以通过参数 partition.assignment.strategy 来配置,默认 Range + CooperativeSticky。
1、RangeAssignor
假设n= 分区数/消费者数量,m= 分区数%消费者数量,那么前m个消费者每个分配n+1个分区,后面的(消费者数量-m)个消费者每个分配n个分区。现在我们假设消费组内有2个消费者C0和C1都订阅了主题t0和t1, 并且每个主题都有4个分区,那 么订阅的所有分区可以标识为: t0p0、t0p1、t0p2、t0p3、t1p0、t1p1、t1p2、t1p3。那么消费者拿到的就是。
但是当2个主题都只有3个分区,那么订阅的所有分区可以标识为:t0p0、 t0p1、t0p2、 t1p0、t1p1、t1p2最终的分配结果为:
可以明显地看到这样的分配并不均匀,如果将类似的情形扩大,则有可能出现部分消费者过载的情况。
2、RoundRobinAssignor
它的分配策略的原理是将消费组内所有消费者及消费者订阅的所有主题的分区按照字典序排序,然后通过轮询方式逐个将分区依次分配给每个消费者。
假设消费组中有2个消费者C0 和C1都订阅了主题 t0和t1, 并且每个主题都有3个分区,那么订阅的 所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、 t1p1、t1p2。最终的分配结果为:
假设消费组内有3个消费者(C0、 C1和C2), 它们共订阅了3个主题(t0、t1、 t2) , 这 3个主题分别有 1、2、3个分区, 即整个消费组订阅了t0p0、 t1p0、 t1p1、 t2p0、 t2p1、 t2p2这6个分区。 具体而言, 消费者 C0 订阅的是主题t0, 消费者C1 订阅的是主题t0和t1, 消费者C2 订阅的是主题t0、t1和t2, 那 么最终的分配结果为:
可以看到RoundRobinAssignor策略也不是十分完美, 这样分配其实并不是最优解, 因为完全可以将分区t1p1 分配给消费者C1。
3、StickyAssignor
它主要有两个目的: (1)分区的分配要尽可能均匀。 (2)分区的分配尽可能与上次分配的保待相同。 当两者发生冲突时, 第一个目标优先于第二个目标。
假设消费组内有3个消费者(C0、C1和C2),它们都订阅了4个主题(t0、t1、t2、t3),并且每个主题有2个 分区。 也就是说,整个消费组订阅了t0p0、 t0p1、 t1p0、 t1p1、 t2p0、 t2p1、 t3p0、 t3p1这8个分 区。 最终的分配结果如下:
这样初看上去似乎与采用RoundRobinAssignor分配策略所分配的结果相同, 但事实是否真的如此呢? 再假设此时消费者 C1脱离了消费组, 那么消费组就会执行再均衡操作,进而消费分区会重新分配。 如果采用RoundRobinAssignor 分配策略, 那么此时的分配结果如下:
如分配结果所示,RoundRobinAssignor分配策略会按照消费者C0 和C2进行重新轮询分配。 如果此时使用的是StickyAssignor分配策略,那么分配结果为:
可以看到分配结果中保留了上一次分配中对消费者 C0 和C2的所有分配结果,并将原来消费者C1的 “ 负 担 “ 分配给了剩余的两个消费者 C0 和C2, 最终 C0 和C2的分配还保持了均衡。 如果发生分区重分配,那么对于同一个分区而言,有可能之前的消费者和新指派的消费者不是同一个, 之前消费者进行到一半的处理还要在新指派的消费者中再次复现一遍,这显然很浪费系统资源。 StickyAssignor 分配策略如同其名称中的"st1cky" 一样,让分配策略具备一定 的 “ 黏性 ” ,尽可能地让 前后两次分配相同,进而减少系统资源的损耗及其他异常情况的发生。