多个MQ如何选择
RabbitMQ
erlang开发,对消息堆积的支持并不友好,当大量消息积压的时候,会导致RabbitMQ的性能急剧下降。每秒钟可以处理几万到几十万消息。
RokectMQ
java开发,面向互联网集群,功能丰富,对在线业务的响应时延做了很多优化,大多数情况下可以做到毫秒级响应,每秒钟大概能处理几十万条消息。
Kafka
Scala开发,面向日志,功能丰富,性能最高。当你的业务场景中,每秒钟消息数量没有那么多的时候,kafka的时延反而会比较高,所以,kafka不太适合在线业务场景。
ActiveMQ
java开发,简单,稳定,性能不如前面三个。不推荐。
RocketMQ组成部分有哪些
Nameserver
无状态,动态列表;这也是和zk的重要区别之一。zk是有状态的。
Producer
消息生产者,负责发消息到Broker
Consumer
消息消费者,负责用Broker上拉取消息进行消费,消费完进行ack。
RokectMQ消费模式有几种
集群消费
- 一条消息只会被同Group中的一个consumer消息
- 多个Group同时消费一个Topic时,每个Group都会有一个consumer消费到数据(一条消息,消息-group.consumer:一对一)
广播消费
- 消息将对一个Consumer Group下的各个consumer实例都消费一遍。即:即使这些consumer属于同一个Consumer Group,消息也会被Consumer Group中的每一个consumer都消费一次(一条消息,消息-group.consumer:一对多)
消息重复消费如何解决
出现原因
正常情况下,在consumer真正消费完消息后应该发送ack,通知broker该消息已正常消费,从队列queue中剔除。当ack因为网络原因无法发送到broker,broker会认为这条消息没有被消费,此后会开启消息重投机制把消息再次投递到consumer。
消费模式:在clustering(集群)模式下,消息在broker中会保证相关group的consumer消费一次,但是针对不同group的consumer会推送多次
解决方案
- 数据库表:处理消息前,使用消息主键在表中带有约束的字段中insert
- Map:单机时可以使用map做限制,消费时查询当前消息id是不是已经存在;
- Redis:使用分布式锁
RocketMQ如何保证消息的顺序消费
首先多个queue只能保证单个queue里的顺序,queue是一个典型的FIFO,天然顺序,多个queue同时消费是无法绝对保证消息的有序性的。
可以使用同一topic,同一个queue,发消息的时候一个线程去发送消息,消费的时候,一个线程去消费queue里的消息;
消息的顺序消费对业务系统来说非常重要,一笔订单产生了三条消息,分别是创建订单、订单付款、订单完成。消费时,必须按照顺序消费才有意义,与此同时多比订单直接又是可以进行并行消费的。
接下来,通过订单例子来展示RocketMQ如何保证消息顺序消费的:
我们最容易想到的应该是如图这样的,必须M1先消费后通知S2,M2才能够被消费。问题是:M1、M2分别发送到S1/S2,这样无法保证M1先到到集群MQ,也不能保证M1先被消费。
如果把多个需要顺序消费的消息都发送到同一MQ Server呢?
这样看起来生产者到消费者的顺序绝对能保证,先发送M1后发送M2;根据先到先消费的原则,M1会先于M2被消费,这样就能保证M1、M2的消息顺序性。问题是:途中可以看到有多个消费者,M1虽先于M2被发送,但是如果S1到消费者1的网络慢于S2到消费者2,这个时候情况就是如图下所示:
要解决这样的问题,可以采用生产者到MQ Server中同样的思路,让S1的消息都发送到同一个消费者
MQ Server到消费者都是一比一,这样就能保证消息的顺序消费,但也会有问题:MQ Server没有消费者1响应时,有两种情况:
- M1确实没有到达消费者1(数据可能在网络传输中丢失)
- 消费者发回了响应消息,但MQ Server没有收到,如果是这种情况会导致M1被重复消费
源码解析
private SendResult send() {
// 获取topic路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
MessageQueue mq = null;
// 根据我们的算法,选择一个发送队列
// 这里的arg = orderId
mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg);
if (mq != null) {
return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, timeout);
}
}
}
获取到路由信息后,会根据MessageQueueSelector实现的算法来选择一个队列,同一个订单号获取到的肯定是同一个队列
// RocketMQ通过MessageQueueSelector中实现的算法来确定消息发送到哪一个队列上
// RocketMQ默认提供了两种MessageQueueSelector实现:随机/Hash
// 当然你可以根据业务实现自己的MessageQueueSelector来决定消息按照何种策略发送到消息队列中
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg)
{
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
获取到路由信息后,会根据MessageQueueSelector实现的算法来选择一个队列,同一个订单号获取到的肯定是同一个队列
// RocketMQ通过MessageQueueSelector中实现的算法来确定消息发送到哪一个队列上
// RocketMQ默认提供了两种MessageQueueSelector实现:随机/Hash
// 当然你可以根据业务实现自己的MessageQueueSelector来决定消息按照何种策略发送到消息队列中
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg)
{
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
这里就是我们实现的select算法,最后类似于这样
消费顺序消费总结
通过以上分析,RocketMQ实现严格的顺序消费采用的方法是:
生产者 -> MQ Server -> 消费者 是一对一的关系,保证同一个id的消息发送到同一个队列
优点:简单易行,容易理解
缺点:
- 并行度会成为消息系统的瓶颈(由于都是一比一导致吞吐量不足)
- 只要消费端出现问题,会导致整个系统流程阻塞(因为消息直接都相互依赖)
RocketMQ如何保证消息不丢失
Producer端
草去send()同步发消息,发送结果是同步感知的。发送失败后可重试,设置重试次数。默认三次
Broker端
修改刷盘策略为同步刷盘。默认情况下是异步刷盘的。集群部署
Consumer端
消费完全正常后,再进行手动ack确认
RokectMQ如何实现分布式事务
- 生产者向MQ服务器发送half消息
- half消息发送成功后,MQ服务器返回确认消息给到生产者。
- 生产者开始执行本地事务;
- 根据本地事务执行结果(UNKNOW,COMMIT,ROLLBACK)向MQ Server发送提交或回滚消息
- 如果错过了(可能因为网络异常、生产者突然宕机等导致异常的情况)提交/回滚消息,则MQ服务器将向同一组中的每个生产者发送回查消息以获取事务状态。
- 回查生产者本地事务状态
- 生产者根据本地事务状态发送提交/回滚消息。
- MQ服务器将丢弃恢复的消息,但已提交(进行过二次确认的half消息)的消息将投递给消费者进行消费。
Half Message:预处理消息,当broker收到此类消息后,会存储到RMQ_SYS_TRANS_HALF_TOPIC的消息消费队列中
检查事务状态:Broker会开启一个定时任务,消费RMQ_SYS_TRANS_HALF_TOPIC队列中的消息,每次执行任务会向消息发送者确认事务执行状态(提交、回滚、未知),如果是未知,Broker会定时去毁掉在重新检查。
超时:如果超过回查次数,默认消息回滚。也就是消息并未真正进入Topic的queue,而是用了临时queue来放所谓的half Message,等提交事务后,才会真正将half message转移到topic下的queue。
RokectMQ的消息堆积如何处理
- 如果可以添加消费者解决,就添加消费者的数据量
- 如果出现queue,但消费者多的情况。可以准备一个临时topic,同时创建一些queue,在临时创建一个消费者来把这些消息转移到topic中,让消费者消费。
RokectMQ消息持久化处理
RokectMQ是一款高性能、高可靠的分布式消息中间件,高性能和高可靠是很难兼得的。因为要保证高可靠,那么数据必须持久化到磁盘上,将数据持久化到磁盘,那么可能就不能保证高性能了。
RokectMQ在兼容这两方面做的不错,先从磁盘说起,现代的磁盘都是高性能的,些速度并不一定比网络的数据传输速度慢。
RokectMQ在持久化的设计上,采取的是消息顺序写、随机读的策略,利用次哦按顺序写的速度,让磁盘的些速度不会成为系统的瓶颈。并且采用的是MMPP这种“零拷贝”技术,提高消息存盘和网络发送的速度。极力满足RokectMQ的高性能、高可靠要求。
在RokectMQ持久化机制中,涉及到了三个角色:
- CommitLog:消息真正的存储文件,所有消息都存储再CommitLog文件中
- CosumeQueue:消息消息逻辑队列,类似数据库的索引文件
- IndexFile:消息索引文件,主要存储消息key与offset对应关系,提升消息检索速度
CmmitLog文件是存放消息数据的地方,所有消息都将存入到commitlog中。生产者将消息发送到Rocket的broker后,broker服务器会将消息顺序写入到Commitlog文件中,这也就是RocketMQ高性能的原因。
但是消费者消费消息的时候,可能就会遇到麻烦,每一个消费者只能订阅一个主题,消费者关心的是订阅主题下的所有消息,但是同一主题的消息再commitLog文件中可能是不连续的,那么消费者消费消息的时候,需要将CommitLog文件加载到内存中,遍历查找订阅主题下的消息,频繁的IO操作,性能就会急速下降。
为了解决上述问题,RocketMQ引入了comsumequeue文件。Comsumequeue文件可以看作是索引文件,类似于MySQL中的二级索引。在存放了同一主题下的所有消息,消费者消费的时候只需要去对应的consumeQueue组中取消息即可,consumequeue文件不会存储消息的全量信息,具体存储的字段,已在上图中标注。这样做也可以带来以下两个好处:
- 由于consumequeue文件内容较小,可以尽可能的保证consumequeue文件全部读入到内存,提高消费效率
- consumequeue文件也是会持久化的,不存全量信息可以节约磁盘空间
IndexFile是RocketMQ为消息订阅构建的索引文件,用来提高根据主题与消息队列检索消息的速度