本文参考 https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/why-mq.md
https://www.yuque.com/liangxinjiang/powiyk/akyt35
一. 消息队列的组成
1. Broker 消息服务器,作为server提供消息核心服务
2. Producer 消息生产者,业务的发起方,负责生产消息传输给broker
3. Consumer 消息消费者,业务的处理方,负责从broker获取消息并进行业务逻辑处理
4. Topic 队列,PTP(Point-to-Point点对点)模式下,特定生产者向特定queue发送消息,消费者订阅特定的Queue完成指定消息的接收
5. Message 消息体,根据不同通信协议定义的固定格式进行编码的数据包,来封装业务数据,实现消息的传输
二:模式分类
1. 点对点 Point-to-Point 使用queue作为通信载体
生产者发送消息到queue中,然后消费者从queue中拉取并进行消费。消息被消费后,queue中不再存储,所以消费者不可能消费到已经被消费的消息,Queue支持存在多个消费者,但是对一个消息而言,只有一个消费者可以消费。
2. 发布/订阅模式
Pub/Sub 发布订阅(广播):使用topic作为通信载体
消息生产者将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。
queue实现了负载均衡,将producer生产的消息发送到消息队列中,由多个消费者进行消费,但一个消费者只能被一个消费者接受,当没有消费者可用时,这个消息会被保存知道有一个可用的消费者。
topic 实现了发布和订阅,当你发布一个消息,所有订阅这个topic的服务都能得到这个消息,所以从1到N个订阅者都能得到一个消息的拷贝。
二. 为什么使用消息队列
根据具体的业务场景来说。解耦,冗余,异步通信,扩展性,过载保护,可恢复性,顺序保证,缓冲,数据流处理,比较重要的有:解耦、异步、削峰(过载保护)
1. 解耦
场景1:ABCD四个系统(模块),A系统发送数据到BCD三个系统,系统之间通过接口调用发送,这时候C系统,不需要A系统发送数据了,而新增了一个E系统需要A系统发送数据。
这个场景下。A系统与其他系统严重耦合,A系统发送数据时,要时刻考虑其他系统是否正常、要不要进行重发、发送的数据是否要保存起来避免出错。而这些问题在使用MQ之后都可以进行更好的处理。
A系统发送一条数据,发送到MQ中去,哪个系统需要就自己去MQ里消费,如果添加新系统,新系统也去MQ消费即可,哪个系统不需要数据了,就取消该系统对MQ消息的消费。通过使用一个MQ,Pub/Sub发布订阅消息模型,A系统就不需要考虑给谁发送,失败超时等问题,与其他系统彻底解耦。
2. 异步
场景2:A 系统接受一个请求,需要在自己本地写库,还需要BCD三个系统写库,自己本地写库需要3ms,BCD三个系统写库分别需要300ms,450ms,200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近1s,这会导致用户体验很差,无法接受。
如果使用MQ,那么A系统连续发送3条消息到MQ队列中,假如耗时5ms,A系统从接受一个请求到返回响应给用户,总时长是 3+5=8ms,用户几乎感知不到,这就不会影响到用户体验。
3. 削峰 过载保护
场景3:A系统每天1:00~2:00,每秒并发请求数量就会暴增到5k+条,其他时间正常,每秒并发请求数量就50~100条,A系统是直接基于MySQL的,大量的请求涌入MySQL。每秒钟需要对MySQL执行约5k+条SQL,但一般的MySQL无法实现每秒5k+的操作,如果每秒请求数量到5k的话,可能会直接把MySQL给干死,导致系统game over。而且高峰期一过,每秒钟的请求数量就降下来了,对整个系统无任何压力。
使用MQ,每秒5k个请求写入MQ,A系统每秒钟最多处理2k个请求,因为MySQL的性能限制它每秒钟只能处理这么多,那么A系统从MQ中拉取请求,每秒钟拉取2k个请求进行处理,不超过自己每秒能处理的最大请求数量就OK,这样下来,即便是高峰期的时候,A系统也不会挂掉,这样的结果是MQ中会积压大量的请求,这个短暂的高峰期是没问题的,因为高峰期过了之后,每秒钟就50~100个请求进入MQ,但A系统依旧会按照每秒2k个请求的速度处理,所以高峰期过后,MQ中积压的消息A系统会快速的给解决掉。
可恢复性:系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
数据流处理:分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量数据采集,然后进行大数据分析是当前互联网的必备集数,通过消息队列完成此类数据收集是最好的选择。
三. 消息队列有什么优缺点
消息队列MQ是一种非常复杂得架构,引入它有很多好处,但是也得针对它带来的坏处做各种额外的处理来进行规避,这样就使得系统复杂度直线上升,但有些情况下你还不得不用。
优点:特殊场景下的应用,解耦、异步(提高系统响应时间)、削峰、为大数据处理框架提供服务
缺点:
1. 系统可用性降低
系统引入的外部依赖越多,越容易出现问题,加入MQ后,MQ一旦出现问题,整套系统全部崩溃,只能gg,这时就要考虑MQ的高可用。
2. 系统复杂度提高
加入MQ后,你得考虑如何保证消息不被重复消费、如何处理消息丢失、如何保证消息传递的顺序性等一堆问题,头大。
3. 一致性问题(****)
A系统处理完了直接返回成功了,客户以为这个请求就成功了,但问题是,你A系统处理完了,还需要BCD系统进行写库操作,然后BC系统写库成功,D系统写库失败了,咋整,数据不一致了。
四. MQ高可用(RabbitMQ、Kafka):
RabbitMQ 是基于主从(非分布式)做高可用性的。有三种模式:单机模式,普通集群模式,镜像集群模式。
单机模式:自己玩玩儿。
普通集群模式:在多台机器上启动多个RabbitMQ实例,每个机器启动一个。你创建的queue,只会放在一个RabbitMQ实例上,但是每个实例都同步queue的元数据(元数据:queue的一些配置信息,通过元数据,可以找到queue所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从queue所在实例上拉取数据过来。这种方式很麻烦,也没做到所谓的分布式,就是个普通集群。这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个queue所在实例消费数据,前者有数据拉取的开销,后者导致单例性能瓶颈。而且如果那个放queue的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你开启了消息持久化,让RabbitMQ落地存储消息的话,消息不一定会丢,得等到这个实例恢复了,才可以继续从这个实例拉取数据。所以这个方案没有高可用性,主要是用来提高吞吐量的,让集群中多个节点来服务某个queue的读写操作。
镜像集群模式(HA 高可用性):该模式下,创建的queue会存放于多个实例上,即每个RabbitMQ节点都有这个queue的一个完整镜像,包含queue的全部数据。每次写消息到queue的时候,都会自动把消息同步到多个实例的queue上,读的时候任意一个节点都可以。
优点:任何一个机器宕机,不影响其他机器与业务。
缺点在于网络带宽压力 和消耗很重,并且扩展性差(某个queue负载很重,添加机器,新机器也要同步这个queue所包含的所有数据,无法线性扩展)。
Kafka 结构图:
Kafka:有多个broker组成,每个broker可以看成一个节点;创建一个topic,这个topic可以划分为多个partition(分区),每个partition存放一部分数据,然后每个partition存放在不同broker上。(分布式消息队列,一个topic的数据分散放在多个机器上,每个机器上放一部分数据)
kafka 0.8版本之后提供了HA(高可用)机制 即副本机制,每个partition的数据会同步到其他机器上,形成自己的多个replica副本。然后所有的副本选举出一个leader出来,其他的副本就是follower,写的时候 leader 负责把数据同步到所有的follower上,读的时候直接读取 leader 上的数据即可,kafka会均匀的将一个partition的所有副本分布在不同的机器上,提高容错性。(高可用)
某个broker宕机了,这个broker上的partition数据在其他机器上都有副本,如果这个broker上有某个partition的 leader ,那么此时会从follower中重新选举出一个 leader ,继续读取这个新 leader 就行。
写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)
消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。
生产者如何保证数据的完整性?
设置发送数据是否需要服务端的反馈,有三个值0,1,-1
0: producer不会等待broker发送ack
1: 当leader接收到消息之后发送ack
-1: 当所有的follower都同步消息成功后发送ack request.required.acks=0
kafka只能保证一个partition中的消息被某个consumer消费时是顺序的;事实上,从Topic角度来说,当有多个partitions时,消息仍不是全局有序的。