为什么使用消息队列?消息队列有什么优点和缺点?kafka,activemq,rabbitmq,rocketmq都有什么区别以及适合哪些场景?
面试官问这个问题,期望的一个回答是说:你们公司有个什么业务场景,这个业务场景有什么技术挑战,如果不用MQ可能会很麻烦,但是用了MQ之后带来了很多的好处.
先说一下消息队列常见的使用场景,其实场景很多,但是比较核心的有3个:解耦,异步,削峰
解耦
A系统发送个数据到BCD三个系统,接口调用发送,那如果E系统也要这个数据呢?那如果C系统现在不需要了呢?现在A系统又要发送第二种数据了呢?或者更崩溃的有:A系统要时时刻刻考虑BCDE四个系统如果挂了咋办?要不要重发?要不要把消息存起来?
面试技巧:需要去考虑一下负责的系统中是否类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦,但是其实这个调用是不需要直接同步调用接口的,如果用MQ给他异步化解耦,也是可以的,你就需要去考虑你的项目里,是不是可以运用这个MQ去进行系统的解耦.
异步
A系统接收一个请求,需要在自己本地写库,还需要在BCD三个系统写库,自己本地写库要3ms,BCD三个系统分别写库要300ms,450ms,200ms,最终请求总延时是3+300+450+200=953ms,接近1s,用户体验感就不好,觉得系统很慢.
这个时候就可以使用MQ的异步,A系统接到请求以后,发送三条数据到MQ,BCD系统分别监听一个MQ,然后执行业务操作,这样对于客户来说耗时就很少只需要A系统的耗费时间3ms+A系统发送消息到MQ的耗时,就可以完成.
削峰
每天0点到11点,A系统风平浪静,每秒并发请求数量就100个,结果每次一到11点~1点,每秒并发请求数量突然就会暴增到1万条,但系统最大的处理能力就只能是每秒钟处理1000个请求,这个时候,系统会死…
解决方法:
1.100万用户在中午高峰时候,大量的操作和请求,每秒有5000个请求;
2.每秒5000个请求写入MQ
3.系统A每秒钟最多只能处理2000个请求,因为MySQL每秒钟最多处理2000个请求.
4.系统A从MQ中慢啊慢拉取请求,每秒钟拉取2000个请求,不要超过自己每秒能处理的最大请求数量就ok了.
5.这样哪怕是最高峰的时候,系统A绝对不会挂掉
6.MQ,每秒钟5000个请求进来,结果就2000个请求出去,就会导致在中午高峰期(1个小时),可能有几十万甚至几百万的请求积压在MQ中
7.这个短暂的高峰期积压是ok的,因为高峰期之后,每秒钟就50个请求进MQ,但是系统A还是会按照每秒2000个该请求的速度在处理
8.所以说,只要高峰期一过,系统A就会很快速的将积压的消息给解决掉
缺点
常见的MQ对比
特性 | activemq | rabbitmq | rocketmq | kafka |
---|---|---|---|---|
单机吞吐量(一秒钟可以处理多少个请求) | 万级,吞吐量比 rocketmq 和Kafka要低了一个数量级 | 万级,吞吐量比rocketmq 和kafka要低了一个数量级 | 10万级,它也可以支撑高吞吐的一种MQ | 10万级,这是kafka最大的优点,就是吞吐量搞.一般配合大数据类的系统来进行实时数据计算\日志采集等场景 |
topic数量对吞吐量的影响 | topic可以达到几百,几千个的级别,吞吐量会有较小幅度的下降,这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic | topic从几十个到几百个的时候,吞吐量会大幅度下降,所以在同等机器下,kafka尽量保证topic数量不要过多,如果要支撑大规模topic,需要增加更多的机器资源 | ||
时效性 | ms级 | 微秒级,这个是rabbitMq的一大特点,延迟是最低的 | ms级 | 延迟在ms级以内 |
可用性 | 高,基于主从架构实现高可用性 | 高,基于主从架构实现高可用性 | 非常高,分布式架构 | 非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | 经过参数优化配置,可以做到0丢失 | 经过参数优化配置,消息可以做到0丢失 | |
功能支持 | MQ领域的功能及其完备 | 基于erlang开发,所以并发能力很强,性能及其好,延时很低 | MQ功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 |
优劣势总结 | 非常成熟,功能强大,在业内大量的公司记忆项目都有应用,偶尔会有较低概率丢失消息,而且现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ5.x维护越来越少,而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用 | erang语言开发,性能及其好,延时低,而且开源提供的管理界面非常好,用起来很好用,在国内一些互联网公司近几年用RabbitMq也比较多一些,但是问题也是显而易见的,rabbitmq确实吞吐量会低一些,这是因为他做的实现机制比较重,而且erlang开发,国内很少有实力做这块的开发 | 接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障,日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是ok的,还可以支撑大规模的topic数量,支持复杂MQ业务场景 | kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性及可靠性,而且分布式可以任意扩展,同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量,而且kafka唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微的影响可以忽略,这个特性天然时候大数据实时计算以及日志收集 |
面试题
如何保证消息队列的高可用?
2.kafka的高可用性
kafka一个最基本的架构认识:多个broker组成,每个broker是一个节点;你创建一个topic,这个topic可以划分为多个partition,每个partition可以存在于不同的broker上,每个partition就放一部分数据.
这就是天然的分布式消息队列,就是说一个topic的数据,是分散放在多个机器上的,每个机器就放一部分数据.
实际上rabbitmq之类的,并不是分布式消息队列,他就是传统的消息队列,只不过提供了一些集群,HA的机制而已,因为无论怎么设置,rabbitmq一个queue的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个queue的完整数据.
kafka0.8以前,是没有HA(高可用机制)机制的,就是任何一个broker宕机了,那个broker上的partition数据就废了,没法写也没法读,没有什么高可用性可言.
kafka0.8以后,提供了HA机制,就是replica副本机制.每个partition的数据都会同步到其他机器上,形成自己的多个replica副本,然后所有replica会选举一个leader出来,那么生产和消费都跟这个leader打交道,然后其他replica就是follower.写的时候,leader会负责把数据同步到所有follower上去,读的时候就直接读leader上数据即可.只能读写leader?很简单,要是你可以随意读写每个follower,那么就要care数据一致性的问题,系统复杂度太高,很容易出问题,kafka会均匀的将一个partition的所有replica分布在不同的机器上,这样才可以提高容错性.
这么搞,就有所谓的高可用性了,因为如果某个broker宕机了,那个broker上面的partition在其他机器上都有副本的,如果这上面有某个partition的leader.那么此时会重新选举一个新的leader出来.大家继续读写那个新的leader即可.这就有所谓的高可用性了.
写数据的时候,生产者就写leader,然后leader将数据落地写入本地磁盘,接着其他follower自己主动从leader来pull数据,一旦所有follower同步好数据了,就会发送ack给leader,leader收到所有follower的ack之后,就会返回写成功的消息给生产者.(当然,只是其中一种模式,还可以适当调整这个行为)
消费的时候,只会从leader去读,但是只有一个消息已经被所有follower都同步成功返回ack的时候,这个消息才会被消费者读到.
实际上这块机制.讲深了,是可以非常之深入的,后续有时间可以继续补充这点的知识
如何保证消息不被重复消费?(如何保证消息消费时的幂等性)
分析
其实还是要结合业务来思考,几个思路:
1)比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别查入了,update一下就好.
2)比如写入redis,那没问题了,反正每次都是set,天然的幂等性.
3)或者稍微复杂点,需要让生产者发送每条数据的时候,里面加一个全局唯一的id,类似订单id之类的东西,然后你这里消费到了之后,先根据这个id去比如redis里查一下,之前消费过吗?如果没有消费过,就处理,然后这个id写入redis,如果消费过了,就不处理了,保证别重复处理相同的消息即可.
还有比如基于数据库的唯一键来保证重复数据不会重复插入多条,我们之前线上系统就有这个问题,就是拿到数据的时候,每次重启可能会有重复,因为kafka消费者还没来得及提交offset,重复数据拿到了以后我们插入的时候,因为有唯一键约束了,所以重复数据只会插入报错,不会导致数据库中出现脏数据
如何保证MQ的消费是幂等性的,需要结合具体的业务来看.
如何保证消息的可靠性传输(如何处理消息丢失的问题)?
面试官心里分析
这个是肯定的,用mq有个基本原则,就是数据不能多一条,也不能少一条,不能多,就是指重复消费和幂等性问题.不能少,就是说这数据别搞丢了.那这个问题你必须的考虑一下.
如果说你这个是用哪个mq来传递非常核心的消息,比如说计费,扣费的一些消息,因为以前设计和研发过一个公司非常核心的广告平台,计费系统,计费系统是很重的一个业务,操作是很耗时的,所以说广告系统整体的架构里面,实际上是将计费做成异步化的,然后中间就加了一个MQ.
我们当时为了确保说这个MQ传递过程中绝对不会把计费消息给弄丢,花了很多精力,广告主投放了一个广告,明明说好了,用户点击一次扣费1块钱,结果要是用户动不动点击了一次,扣费的时候搞得消息丢了,我们公司就会不断的少几块钱,几块钱积少成多,这个对公司是一个很大的损失.
面试题剖析
这个丢数据,mq一般分为两种,要么是mq自己弄丢的,要么是我们消费的时候弄丢了.
分析:
rabbitmq,这种mq,一般来说都是承载公司的核心业务的,数据是绝对不能给弄丢的.
故障原因
1)rabbitmq
① 生产者弄丢了数据
生产者将数据发送到rabbitmq的时候,可能数据就在半路给搞丢了,因为网络啥的问题,都有可能.
此时可以选择用rabbitmq提供的事务功能,就是生产者发送数据之前开启rabbitmq事务(channel.txSelect),然后发送消息,如果消息没有成功就会被rabbitmq接收到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试发送消息;如果收到了消息,那么可以提交事务(channel.txCommit).但是问题是,rabbitmq事务机制一搞,基本上吞吐量会下来,因为太耗性能.
所以一般来说,如果你要确保说写rabbitmq的消息别丢,可以开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉你这个消息ok了,如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试.
生产者这块如果是要保证消息不丢失,一般是用confirm机制,异步的模式.你发送消息之后不会阻塞,直接你可以发送下一个消息,这个的吞吐量会比较高一些.
走一个回调的机制
1,先把channel设置成confirm的模式
2,发送一个消息
3,发送完消息之后你就不用管了
4,rabbitmq如果接收到了这条消息的话,就会回调你生产者本地的一个接口,通知你说这条消息我已经收到了
5,rabbitmq如果在接收消息的时候报错了,就会回调你的接口,告诉你这个消息接收失败了,你可以再次重发
channel.confirm
//发送一个消息
//然后就ok了,不用管了
你会在生产者那里提供一个供回调的一个回调接口的实现
public void ack(String messageId){
}
public void nack(String messageId){
//再次重发一次这个消息
}
②rabbitmq弄丢了数据
就是rabbitmq自己弄丢了数据,这个你必须开启rabbitmq的持久化,就是消息写入之后会持久化到磁盘,哪怕是rabbitmq自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢失,除非极其罕见是,rabbitmq还没有持久化,自己就挂了,可能导致少量数据会丢失的,但这个概率比较小.
设置持久化有两个步骤:
第一个是创建queue的时候将其设置为持久化的,这样就可以保证rabbitmq持久化queue的元数据,但是不会持久化queue里的数据.
第二个是发送消息的时候将消息的deliveryMode设置为2,就是讲消息设置为持久化的,此时rabbitmq就会将消息持久化到磁盘上去.必须要同时设置这两个持久化才行.
而且持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者都不到ack,你也是可以自己重发的.
哪怕是你给rabbitmq开启了持久化机制,也有一种可能,就是这个消息写到了rabbitmq中,但是还没来得及持久化到磁盘上,结果不巧,此时rabbitmq挂了,就会导致内存里的一点点数据丢失.
③消费端弄丢了数据
你开到了消费者的autoAck机制
你消费到了数据之后,消费者会自动通知rabbitmq说,ok,我已经消费完这条消息了
如果你消费到了一条消息,还在处理中,还没处理完,此时消费者就自动autoAck了,tongzhirabbitmq说这条消息已经消费了
此时不巧,消费者系统宕机了,就会那条消息丢失,还没处理完,而且rabbitmq还以为这个消息已经处理掉了.
解决方法
消费者这个层面,你需要将autoAck给他关闭,然后每次自己确定已经处理完了一条消息之后,你在发送ack给rabbitmq.
如果你还没有处理完就宕机了,此时rabbitmq没收到你发的ack消息.
然后rabbitmq就会将这天消息重新分配给其他的消费者去处理.
kafka数据丢失
①消费端弄丢了数据
唯一可能导致消费者弄丢数据的情况,就是说,你那个消费到了这个消息,然后消费者那边自动提交了offset,让kafka以为你已经消费好了这个消息,其实你刚准备处理这个消息,你还没有处理,你自己就挂了,此时这条消息就丢了.
这不是一样么.大家都知道kafka会自动提交offset,那么只要关闭自动提交offset,在处理完之后自己手动提交offset,就可以保证数据不会丢,但是此时确实还是回重复消费,比如你刚处理完,还没有提交offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了.
②kafka弄丢了数据
这块比较常见的一个场景,级是kafka某个broker宕机,然后重新选举partition的leader时.大家想想,要是此时其他的follower刚好还有些数据没有同步,结果此时leader挂了,然后选举某个follower成leader之后,他不就少了一些数据,这就丢了一些数据.
所以此时一般是要求起码设置如下4个参数:
给这个topic设置replication.factor参数:这个值必须大于1,要求每个partition必须有至少2个副本
在kafka服务端设置min.insync.replicas参数:这个值必须大于1,这个是要一个leader至少感知到有至少一个follower还跟自己保持联系,没掉队,这样才能确保leader挂了还有一个follower吧.
在producer端设置 acks=all:这个是要求每条数据,必须是写入所有replica之后,才能认为是写成功了.
在producer端设置retries:MAX:这个是要求一旦写入是不,就无限重试,卡在这里了
我们生产环境就是按照上述要求配置的,这样配置之后,至少在kafka broker端就可以保证在leader所在的broker发生故障,进行leader切换时,数据不会丢失.
如何保证消息的顺序性
剖析
我们以前做过一个mysql binlog同步的系统,压力还是非常大的,日同步数据要达到上亿,mysql->mysql,
你在mysql里增删改一条数据,对应出来的增删改3条binlog,接着这三条binlog发送到MQ里面,到消费出来一次执行,起码的保证人家是按照顺序来的吧?不然本来是:新增,修改,删除;改了顺序就会错.
先看看顺序会错乱的俩场景
①rabbitmq:一个queue,多个consumer,这不明显乱了
②kafka:一个topic,一个partition,一个consumer,内部多线程,这不也明显乱了
那如何保证消息的顺序性呢?
1,rabbitmq:拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点,或者就一个queue但是对于一个consumer,然后这个consumer内部用内存队列排队,然后分发给底层不同的worker来处理
2,kafka:一个topic,一个partition,一个consumer,内部单线程消费,写N个内存queue,然后N个线程分别消费一个内存queue即可.
kafka:
activemq:
rabbitmq:
rocketmq: