Rocket的特性有那些?
支持发布/订阅(Pub/Sub)模式和点对点(P2P)消息模型。
在一个队列中可靠的先进先出(FIFO)和严格的顺序传递。
kafka推荐使用单线程,所以默认情况下一个队列中也是可靠的先进先出和严格有序。但需要提高消费能力的时候推荐使用增加消费者和分区的方法,不能通过增加线程的方式提高消费能力。
RabbitMQ是使用推的模式,所以默认情况下也是严格有序的。
Rocket支持使用多线程,可以通过增加线程数来提供消费能力。默认是20线程。
Rocket为了保证一个队列先进先出和严格的顺序传递,是在消费者获取数据的时候加锁。只有一个线程可以消费数据,其他线程阻塞等待。
支持推模式和拉模式。
单一队列百万级消息的堆积能力。
就是可以把消息储存在中间件中,将消费的压力放在中间件上而不是消费端。
kafka同样支持该特性,但kafka的问题是当主题和分区变多以后性能有明显的下降,RocketMQ通过单文件记录的方式优化了该功能性能下降不明显。
Rabbit默认不支持消息持久化,基本不需要考虑这个特性。
支持多种协议,比如JMS、MQTT等。
支持分布式架构。
RabbitMQ分布式支持能力一般。
kafka分布式支持较为复杂,达到了在保证正确性的前提下可以保证较高的一致性。
Rocket分布式支持方式较为简便,放弃了一定的一致性要求降低了系统维护的难度。
有仪表盘Dashboard。
kafka的听说不太好用。
Rabbit有且相对好用的。
Rocket的优势有那些?
支持事务型消息。
事务性消息是分布式事务实现中的一种。
先说明一下什么是分布式事务,这个是单机事务衍生出来的。当我们一个业务需要在多个不同的微服务上实现各自的数据提交的时候,我们需要保证他们之间的数据一致性,这个就是分布式事务。
分布式事务实现的思路一种是就是需要记录每一个微服务中的事务提交情况,并将其存放在一个消息中心中。然后保证一个业务操作中所有微服务中的操作是不是一致的,如果某一个操作出现不一致则所有的其他操作都需要回滚,比如二阶段提交、三阶段提交,本地消息表。一种是补偿性性事务,提前准备好回滚方式,当其他相关微服务操作出现异常后调用补偿操作保证一致性,比如TCC、Saga。
Seata等框架是具体实现并不属于实现方式。
二阶段、三阶段,核心原理都是将事务信息交由统一的事务处理服务来控制的。各微服务将自己的操作通知控制服务,如果所有都正常则总控制服务通知各微服务完成提交操作;如果某些出现异常,则总控制服务通知其他微服务回滚。三阶段是对二阶段的优化。
本地消息表,就是将事务消息存放到统一的数据库表中。不同的微服务循环查询数据库表,如果所有的相关操作都成功怎自动完成提交操作,如果有一个操作出现异常则各组回滚。
TCC,是为每一个操作提供部分执行操作,一部分是业务操作,一部分是服务调用出现异常的操作,一部分是服务调用完成后的提交操作。当服务调用过程中某微服务中出现异常后,回调回滚操作恢复数据,如果所有业务都没有出现异常,则回调提交操作。
Saga,是为每一个操作提供提交操作和回滚操作。将业务操作重构成业务流,然后在sage提供的引擎的支持下按照业务流逐个服务完成提交操作,如果其中某一个操作出现异常,则按照业务流按执行顺序逆序执行指定的回退操作。相对TCC来说不需要锁定数据,可以异步执行。
事务性消息其实也是同步数据数据的一种实现。具体的实现逻辑可以表述为:
先假设参与节点包括:业务系统一,MQ,业务系统二。业务系统一完成部分业务,然后通过MQ推送消息给业务系统二完成另一部分操作。
可能出现的问题是:业务系统一完成了一部分操作,推动消息之后出现异常回滚时,业务系统二已经消费了消息,出现了数据不一致。
RocketMQ的处理方式是:业务系统一先发送消息(half)给MQ,但该消息不能被消费。之后等业务系统成功完成了自己的操作后通知MQ,MQ在经消息转为正常可以消费。当MQ找过一段时间没有接到业务系统一发送的确认通知后可以发送确认请求。
但无法处理业务系统二出现异常后,通知业务系统一完成事务回滚。
支持结合RocketMQ的多个系统之间的数据一致性。
首先RocketMQ通过事务型消息保证生产者和中间件数据一致。
然后如果消费者消费失败可以重试,直到成功保证一致性。
即使消费者宕机,由于数据储存在中间件,重启服务后重新调动消费就可以,最终也可以保证一致性。
但中间可能出现部分时间的不一致。
支持18个级别的延迟消息。
kafka需要自己通过业务处理解决。
Rabbit可以通过设置队列的超时时间并搭配死信队列解决。
支持指定次数和时间间隔的失败消息重发。
这里说的是延迟级别(delay level),就是第一次重试是在1s之后,第二次重试在5s之后等等。
目的是通过加长延长时间缓解对方的处理压力。如果继续按照同样的延迟时间发送消息,当接收方已经压力爆表了,还给他发送消息更有可能造成宕机。
支持broker端的tag过滤。
就是发送给broker的消息中包含一个tag属性,然后broker可以通过tag判断是否将消息推送给消费者。
相当业务,可以减少网络传输。
支持重复消费。
一定是基于消息的持久化。
rabbit默认不支持,kafka支持。
RocketMQ的架构是什么?
![](https://i-blog.csdnimg.cn/blog_migrate/8ece0b665d18971c463f26997ce1b552.png)
由四个角色组成:producer(生产者)、broker(中间件)、nameServer(注册服务)、consumer(消费者)。
其中注册服务摒弃了zookeeper中强一致性业务实现,采取各自独立存储但互不交互的方式。将一致性的保证交由业务方来负责处理。
也就是说各个中间件在启动后会将自己的调用信息发送到每一个注册服务节点上。
生产者会从其中一个注册服务节点获取中间件信息,如果连接失败则从另一个注册服务查看。
中间件和注册服务之间通过心跳请求保持连接。
注册服务节点之间不互相交互。
中间件包括两种,master(主)和slave(从)。
其中正常情况下主节点会提供服务,包括保存生产者消息和向消费者推送消息。
主节点宕机后,对应的从节点可以提供向消费者推送消息的服务。
从节点会从主同步数据。
集群部署的方式有四种:单master,多master,多master多slave(同步刷盘),多master多slave(异步刷盘)。
多个主节点之间相互独立,互不影响,也就是说没有数据交互。相对于kafka多节点,多副本备份,rocket没有该功能,即使有多个中间件节点提供相同的服务,服务之间也不会通信。如果其中一个中间件节点失效,在没有从节点备份的情况下主节点中的数据就丢失了。
生产者有几种发送数据方式?生产者组的概念是什么?
发送方式有3种:同步发送、异步发送和单向发送。
生产者组,业务逻辑上不太明白,为生产者标识了一个组属性。
消息消费有几种模式?消费者组的概念是什么?
有两种,集群模式和广播模式。
集群就是一个消费者节点处理消费。
广播是所有消费者都会消费同一条消息。
这个是broker启动的时候配置的。
消费者组,当是点对点模式时保证同一个消费者组中的只有一个消费者可以消费消息。
Rocket是如何保证高可用的?
支持集群。
生产者和消费者都是从注册中心拿中间件的信息。
这里需要注意三点:
中间件上下线一般可能需要30s时间,注册中心才能完成数据更新。
中间件的主题变更也可能需要30s,才能完成注册中心中的信息变更。
主题的变更一定要在控制台完成,这个是官方极力建议的。
各组件的启动流程是什么?
第一步启动注册中心服务。
第二步启动中间件服务,并完成服务在注册中心的注册既服务注册。
第三步生产者从注册中心获取中间件地址,可以推送消息。同时,消费者也开始和中间件建立连接,准备消息消费。
第四步生产者开始生产消息,消费者消费消息。
注:当一个中间件节点挂机后,如果有从节点,消费者可以从从节点消费消息,但生产者无法将生产的消息推送到中间件。
Rocket中的Topic中是否和kafka有分片(partition)概念类似的组件?
有,rocket中是消息队列。
kafka中主题下默认有1个分片,rocket中主题下默认有4个消息队列。
rocket中有几种消息类型?
相对于kafka,rocket中提供了多线程的优化。所以消费者具备了并行消费的可能。
同时,rocket提供了配置可以实现和kafka类似的效果,叫顺序消费。
总结来说,就是两种并行消费和顺序消费。
Rocket是如何提高数据存储性能的?
用了零拷贝技术,具体来说就是mmap(内存映射)技术。
内存映射技术:就是将用户空间的内存地址和内核空间的内存地址建立映射关系。在用户操作用户空间的数据的时候其实就是处理内核空间中的数据,进而避免了数据在用户空间和内核空间之间的拷贝。
零拷贝技术还有一种sendfile,这个的前提条件是用户不需要完成数据操作,可以将数据在不同硬件对应的内存空间之间传输而不需要将数据copy到用户空间。
操作系统会给每一个进程分配一片内存空间存储进程数据。为了防止进程执行危险操作导致系统奔溃,操作系统不允许进程直接调用一些资源而采用系统提供的方法,比如从硬盘读数据,或者通过socket发送数据。为了匹配这个实现,操作系统在为进程分配内存的时候将内存分成了两个部分,一部分是操作系统提供方法需要的内存成为内核空间,一部分是用户自己使用使用的空间叫用户空间。系统要求用户进程不能使用内核空间。向对应的进程在操作内核空间的时候会进入内核态,不操作的时候会进入用户态。
Rocket为什么可能出现数据重复的问题?
Rocket为提高消费者的消费能力,在消费端使用了多线程。这样就出现了一个问题,不同线程中消费的消息的偏移量不同,导致提交的消费偏移就不同,确定以那个偏移量作为中间件中存储的消费偏移就是个大问题。所以Rocket选择记录最小的偏移量为消费偏移量,但这样就容易造成重复消费。
这个也是Rocket用户需要容忍的设计缺陷,所以Rocket建议用户做幂等性操作,而Rocket完全放弃了这部分工作。
相比较而言,kafka通过消费端建议单线程处理避免了这个问题,同时也可以在一定程度上保证消息的幂等性。当然消息体本身的幂等性还是需要适用方处理。
Rocket是否可以实现严格的消息有序?
Rocket的如果采用单队列模式是可以实现严格有序的。
相对于kafka来说,kafka可能出现片区的重分配导致消息出现乱序,所以只能有一个消费者才能包含消息有序。
而Rocket由于没有重分区的概念,正常情况下消费者只关联一个中间件节点不会出现变更,所以可以保证严格有序。
Rocket如何保证消息的写入效率的?
首先,使用了mmap技术优化了写入效率。
其次,将所有的写入都放在了一个文件中,避免kafka由于片区太多导致的写入效率下降。
之后,消费的时候使用异步线程读写入的消息数据放到对应的队列中让消费者消费。
保证写入效率是因为消息来的过程对Rocket来说是不可控的,但向消费者发消息是Rocket可控的,毕竟削峰就是中间件中的一个功能。也就是说MQ中间件不需要保证0延迟。
Rocket在消息安全方面做了那些工作?
Rocket提供了什么方式可以保证数据在中间件中的安全的?
首先是单节点的数据持久化,在保证单节点服务器不出现问题只是Rocket中间件进程出现问题的情况下。
Rocket提供了同步刷盘模式,保证消息一定可以完成持久化,避免程序出问题导致的消息丢失。
Rocket也提供了异步刷盘的模式,通过主动接受一部分的数据丢失的风险,提高了消息写入效率。
其次是如果单节点的服务器出现了问题,那么Rocket提供了从节点保证数据冗余,但这里可能在数据复制过程中出现丢失的风险。
Rocket提供了同步复制,可以保证数据的高一致性。
同样,也提供异步复制,在可以接受一定数据丢失的风险下,提高了写入效率。
发送过程中如何保证消息不丢失?
如何保证消息一定会被消费的?
采用了ack机制,当然这种也导致了可能出现消息重复,需要业务方提供消息幂等性保证。
可以回溯消息。
基于延迟级别(delay level),提供了消息重试机制。
相对于kafka和rabbit,提供了16个级别延时队列。
如何使用docker完成RocketMQ的安装?
我自己遇到的问题:
启动Broker的时候,docker logs查看日志,提示如下信息:
![](https://i-blog.csdnimg.cn/blog_migrate/9cc6bdf5c9dec5eff35a3bd989f8753f.png)
原因是broker.conf文件位置写错了,然后docker启动的时候,因为没有broker.conf文件,就创建了对应的文件,但创建的是目录不是文件。
启动Broker服务命令如下:
docker run -d -p 10911:10911 -p 10909:10909 \
-v /tmp/data/rocketmq/broker/logs:/root/logs \
-v /tmp/data/rocketmq/broker/store:/root/store
-v /tmp/etc/rocketmq/broker/broker.conf:/opt/rocketmq/conf/broker.conf \
--name rmqbroker \
--link rmqnamesrv:namesrv \
-e "NAMESRV_ADDR=namesrv:9876" \
-e "MAX_POSSIBLE_HEAP=200000000" \
rocketmqinc/rocketmq sh mqbroker \
-c /opt/rocketmq/conf/broker.conf
注意最后一行”-c /opt/rocketmq/conf/broker.conf“,这个地址是docker容器中的地址,不是宿主机的地址。我以为这个是宿主机地址,所以在这里创建了broker.conf文件。
宿主机对应的地址在 -v第三行中的指定的:/tmp/etc/rocketmq/broker/broker.conf
启动Dashboard(仪表盘)的时候没有修改对应的地址信息。
Rocket如何发送数据?
引入依赖
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.1</version>
</dependency>
</dependencies>
发送方代码
DefaultMQProducer producer = new DefaultMQProducer("first");
producer.setNamesrvAddr("192.168.239.132:9876");
producer.start();
String topic = "study_topic";
String msg = "这是第%s条消息,时间是:%s",
successLog = "Broker:%s,Topic:%s,Queue:%s,offset:%s",
errorLog = "Exception message: %s";
for (int i = 1; i< 2; i++) {
LocalDateTime date = LocalDateTime.now();
String dateStr = date.get(ChronoField.HOUR_OF_DAY) + ":" + date.get(ChronoField.MINUTE_OF_HOUR) + ":" + date.get(ChronoField.SECOND_OF_MINUTE);
Message message = new Message(topic, "tag", String.format(msg, i, dateStr).getBytes(Charset.defaultCharset()));
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(String.format(successLog,sendResult.getMessageQueue().getBrokerName(),
sendResult.getMessageQueue().getTopic(), sendResult.getMessageQueue().getQueueId(), sendResult.getQueueOffset()));
}
@Override
public void onException(Throwable e) {
System.out.println(String.format(errorLog, e.getMessage()));
}
});
}
//A
TimeUnit.SECONDS.sleep(100);
producer.shutdown();
遇到两个错误:
一个是producer竟然有nameSpace,应该配nameServer addr,调用setNamesrvAddr方法,掉秤setNamespace方法。
第二个是标志A处,使用异步发送导致还没有发送生产者就shutdown了,所以消息发送失败。但是谁能告诉我提示为啥是连不上nameServer,提示如下:
org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to [192.168.239.132:9876] failed
Rocket如何消费数据?
代码如下:
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("first-consumer");
consumer.setNamesrvAddr("192.168.239.132:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
//第二个是tag
consumer.subscribe("study_topic", "*");
String message = "thread:%s, broker:%s, topic: %s, queue: %s, offset: %s, body: %s, time: %s";
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
LocalDateTime time = LocalDateTime.now();
String timeStr = time.get(ChronoField.HOUR_OF_DAY) + ":" + time.get(ChronoField.MINUTE_OF_HOUR) + ":" + time.get(ChronoField.SECOND_OF_MINUTE);
System.out.println(String.format(message, Thread.currentThread().getId(), msg.getBrokerName(), msg.getTopic(), msg.getQueueId()
, msg.getQueueOffset(), new String(msg.getBody(), Charset.defaultCharset()), timeStr));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("消费者已启动!");
}
依据输出的结果中的线程信息,可以看出是多线程消费。