RocketMQ详解

2 阅读6分钟

参考:javaguide

引入消息队列的目的

异步、消费、解耦。核心就是这六个字。

RocketMQ的架构

如何保证顺序消费?

如何解决重复消费问题?

参考:(99+ 封私信 / 30 条消息) kafka如何解决重复消费? - 知乎

RocketMQ如何实现分布式事务?

RocketMQ使用的是事务消息 + 事务反查机制。

image.png

事务消息处理流程:

  • 发送半消息:生产者将消息发送至RocketMQ服务端,服务端将消息持久化之后,向生产者返回ACK,此时消息被标记为“暂不能投递”,这种消息对于消费者是不可见的,这种状态的消息称为半消息。
  • 执行本地事务:生产者执行本地事务
  • 提交本地事务执行结果:生产者根据本地事务执行结果,向服务端提交Commit或者Rollback。服务端如果收到Commit,就会将该消息投递出去;如果收到Rollback,就会丢弃该消息
  • 事务回查:如果服务端因为网络或者其他原因没有收到本地事务执行结果,经过一段时间后,或者收到Unknown,服务端就会对生产者发起消息回查。生产者此时就会进行本地事务检查,再次提交本地事务执行结果

注意,在消息队列的分布式事务中,本地事务和消息存储才是同一个事务,而不是本地事务和其他服务的事务是同一个事务,不保证强一致性,只保证最终一致性,需要下游系统保证自己那部分事务。

消费者分类

如何解决消息堆积?

RocketMQ对比Kafka

RocketMQ优缺点:

  • 优点:功能相对Kafka来说丰富很多,比如延迟消息、事务消息等,这些都是Kafka没有的。吞吐量也高,支持集群部署,并且是Java语言开发。
  • 缺点:性能相比Kafka弱,因为kafka用的是sendfile,RocketMQ使用的是mmap + write

Kafka的优缺点:

  • 优点:高吞吐量,支持集群部署
  • 缺点:可能会丢数据,因为它在收到消息时不是立刻写到物理磁盘的,而是先写入磁盘缓冲区。功能单一,基本只支持收发消息,几乎没有高级功能

如何选择?

  • 如果我们需要极致的吞吐量和性能,又能够容许丢失部分消息,业务比较简单只有收发消息的需求,就直接选择Kafka。比如需要传输日志这种。
  • 如果有一些业务需求,比如需要事务消息、延迟消息、消息过滤等功能的,团队技术栈主要是Java相关的,就直接选择RocketMQ。

RocketMQ为什么高性能

主要是因为采用了零拷贝技术——mmap。

传统的IO方式

image.png

当我们要读取磁盘数据并将其发送到网络时,流程如上图所示:

  • 用户调用read()方法从磁盘读取数据,此时由用户态切换到内核态,也就是图中的切换1。
  • 将磁盘中的数据通过DMA拷贝到内核缓冲区,再拷贝到用户缓冲区
  • read()方法返回,内核态切换到用户态,也就是图中的切换2
  • 拿到数据后,用户调用write()方法,从用户态切换到内核态,也就是图中的切换3
  • CPU将用户缓冲区的数据拷贝到Socket缓冲区,再通过DMA拷贝到网卡
  • wrtie()方法返回,从内核态切换回用户态,也就是图中的切换4

整个过程发生了4次上下文切换和4次数据拷贝,在高并发场景下,这会严重影响读写性能。

零拷贝技术

零拷贝技术就是为了解决这个问题而产生的,分为mmap和sendfile两种。

mmap

mmap(memory map)是一种内存映射文件的方法,即将一个文件映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中的一段虚拟地址一一映射的关系。

简单点来说,就是内核缓冲区和用户缓冲区共享,减少了内核缓冲区到用户缓冲区的一次CPU拷贝。流程图如下:

image.png

其实就是把read() + write()换为mmap() + write()。

当用户调用mmap时,由用户态切换到内核态,数据通过DMA拷贝由磁盘拷贝到内核缓冲区,mmap返回,由内核态切换为用户态。随后用户调用write,由用户态切换到内核态,数据直接从内核缓冲区通过CPU拷贝到Socket缓冲区,再通过DMA拷贝到网卡,write返回,由内核态切换到用户态。

比起传统的IO方式,mmap减少了一次拷贝操作。

sendfile

sendfile相对于传统IO方式,减少了一次拷贝操作,同时减少了两次上下文切换。

image.png

用户在发起 sendfile()调用时由用户态切换到内核态,之后数据通过 DMA 拷贝到内核缓冲区,之后再将内核缓冲区的数据 CPU 拷贝到 Socket 缓冲区,最后拷贝到网卡,sendfile()返回,由内核态切换到用户态。

和mmap不一样,sendfile时无法知道读取的文件数据的,也无法对其进行操作,但是mmap时可以修改内核缓冲区的数据的。所以,如果需要对文件的内容进行修改之后再传输,只有mmap可以满足。

和Kafka的极致性能不一样,RocketMQ 的设计目标是为复杂的业务场景服务,因此它内置了许多需要读取或是修改消息内容的高级特性。如果数据不经过用户缓冲区,比如下面这些功能就无法实现:

  1. 消息过滤 (Message Filtering)
    RocketMQ 支持消费者通过 Tag 或 SQL92 属性来订阅特定消息。这意味着当消费者拉取消息时,Broker 必须先读取消息内容,判断它是否符合消费者的订阅规则,然后再决定是否发送。如果使用 sendfile,Broker 根本没机会读取消息内容,也就无法进行过滤。
  2. 延迟消息 (Delayed Message)
    延迟消息需要 Broker 在接收到消息后,先将其存储起来,并在指定的延迟时间到达后再投递给消费者。这个过程要求 Broker 必须能够解析消息,获取其延迟时间属性,并将其放入一个定时任务中。这同样需要数据进入应用层进行处理。
  3. 死信队列 (Dead Letter Queue)
    当一条消息消费失败并达到最大重试次数后,Broker 需要将其转发到死信队列。这个“转发”操作不是简单的文件拷贝,而是需要 Broker 重新构造一条新的消息(包含原消息内容),并发布到新的主题中。这要求 Broker 必须先获取到原始消息的完整内容。

可能你会想,kafka也支持死信队列,为什么kafka用的是sendfile呢?其实是因为投递死信队列这个动作,kafka是交给客户端实现的。

RocketMQ存储机制