RocketMQ基础知识

1.核心概念

rocketmq集群架构

Producer:类似发信人,消息发送者

包括:同步发送、异步发送、顺序发送、单向发送;
生产者组:同一类Producer组成一个集合,发送同一类消息且逻辑一致,用于在事务中给broker提供回查服务

Consumer:类似收信人,消息接收者

两种消费形式:式拉取式消费、推动式消费;
两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)
消费者组:同一类Consumer组成一个集合

  • 按照组的形式记录消费者位移;
  • 不同组会消费同一个topic数据,每组内一条消息只会被其中一个consumer消费;

Broker:类似邮局,代理服务器

除了暂存和传输消息,也存储消息相关的元数据,包括消费者组、消费偏移进度、主题、队列消息等

Broker Server是RocketMQ真正的业务核心,包含多个重要子模块:

  • Remoting Module:整个Broker的实体,负责处理来自clients端的请求
  • Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
  • Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能
  • HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能
  • Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询

架构模式

  • 普通集群:给每个节点分配一个固定角色

    • master负责响应客户端请求,并存储消息;
    • slave负责对master消息进行同步保存,并响应部分客户端的读请求;
    • 各个节点的角色无法进行切换,master挂了,这一组Broker就不可用。
  • Dledger高可用集群:RocketMQ4.5版本引入,集群会随机选出一个节点作为master,当master节点挂了后,会从slave自动选出一个节点升级为master(raft算法)。

Name Server:类似各个邮局的管理机构,管理Broker

  • Broker Server启动时向所有的Name Server注册自己的服务信息,并且后续通过心跳请求的方式保证这个服务信息的实时性
  • 生产者或消费者通过Name Server查找各主题相应的Broker IP列表
  • 多个Namesrver实例组成集群,但相互独立,没有信息交换
  • NameServer中任意的节点挂了,只要有一台服务节点正常,整个路由服务就不会有影响

Topic:一类消息的集合

  • 每个主题包含若干条消息,每条消息只能属于一个主题,RocketMQ消息订阅的基本单位;
  • Topic只是一个逻辑概念,同一个Topic下的消息,会分片保存到不同的Broker上,而每一个分片单位,称为MessageQueue

Message Queue:用于并行发送和接收消息,存储消息的物理地址

FIFO特性的队列结构,生产者发送消息与消费者消费消息的最小单位

Message:生产和消费数据的最小单位

  • 每条消息必须属于一个主题Topic
  • 每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key
  • 有一个Tag标签,用于区分同一主题下不同类型的消息

2.参数配置

1.RocketMQ的JVM内存配置

以runbroker.sh中G1GC配置为例

 JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
 JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"

-XX:+UseG1GC: 使用G1垃圾回收器,
-XX:G1HeapRegionSize=16m 将G1的region块大小设为16M
-XX:G1ReservePercent:在G1的老年代中预留25%空闲内存,默认值是10%,RocketMQ把这个参数调大了
-XX:InitiatingHeapOccupancyPercent=30:当堆内存使用率达到30%后就会启动G1垃圾回收器尝试回收垃圾,默认值是45%。RocketMQ把这个参数调小了,避免垃圾对象过多,一次垃圾回收时间太长的问题
gc*:file…filecount=5,filesize=30M:定制了GC的日志文件,确定GC日志文件的地址、打印的内容以及控制每个日志文件的大小为30M并且只保留5个文件

2.RocketMQ的其他一些核心参数

例如在conf/dleger/broker-n0.conf中有一个参数:sendMessageThreadPoolNums=16
表明RocketMQ内部用来发送消息的线程池的线程数量是16个,其实这个参数可以根据机器的CPU核心数进行适当调整,例如如果你的机器核心数超过16个,就可以把这个参数适当调大。

3.Linux内核参数定制

  • ulimit,需要进行大量的网络通信和磁盘IO。
  • vm.extra_free_kbytes,告诉VM在后台回收(kswapd)启动的阈值与直接回收(通过分配进程)的阈值之间保留额外的可用内存。RocketMQ使用此参数来避免内存分配中的长延迟。(与具体内核版本相关)
  • vm.min_free_kbytes,如果将其设置为低于1024KB,将会巧妙的将系统破坏,并且系统在高负载下容易出现死锁。
  • vm.max_map_count,限制一个进程可能具有的最大内存映射区域数。RocketMQ将使用mmap加载CommitLog和ConsumeQueue,因此建议将为此参数设置较大的值。
  • vm.swappiness,定义内核交换内存页面的积极程度。较高的值会增加攻击性,较低的值会减少交换量。建议将值设置为10来避免交换延迟。
  • File descriptor limits,RocketMQ需要为文件(CommitLog和ConsumeQueue)和网络连接打开文件描述符。建议设置文件描述符的值为655350。

这些参数在CentOS7中的配置文件都在 /proc/sys/vm目录下。
另外,RocketMQ的bin目录下有个os.sh里面设置了RocketMQ建议的系统内核参数,可以根据情况进行调整

3.核心原理

1.读写队列

  • 写队列会创建对应的存储文件,负责消息写入。读队列会记录Consumer的Offset,负责消息读取。
  • Topic权限: 2:禁写禁订阅(死信队列);4:可订阅,不能写;6:可写可订阅。
  • 通常都需要设置读队列等于写队列。只有在MessageQueue进行缩减的时候,例如原来四个队列,现在缩减成两个。可以先缩减写队列,待空出来的读队列上的消息都被消费完后,再缩减读队列,保证平稳的缩减队列。

2.消息持久化

RocketMQ消息采用磁盘文件保存消息,默认路径在${user_home}/store目
录。(可以在broker.conf中自行指定)
目录结构:
在这里插入图片描述

  • CommitLog:存储消息的元数据,所有消息都会顺序存入到CommitLog文件当中。CommitLog由多个文件组成,每个文件固定大小1G。以第一条消息的偏移量命名文件。
  • ConsumerQueue:存储消息在CommitLog的索引。一个MessageQueue一个文件,记录当前MessageQueue被哪些消费者组消费到了哪一条CommitLog。
  • IndexFile:为了消息查询提供了一种通过key或时间区间来查询消息的方法。这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程。
  • checkpoint:数据存盘检查点。主要记录commitlog文件、ConsumeQueue文件以及IndexFile文件最后一次刷盘的时间戳。
  • config/*.json:将RocketMQ的一些关键配置信息进行存盘保存。包括:Topic配置、消费者组配置、消费者组消息偏移量Offset等等一些信息。
  • abort:判断程序是否正常关闭的一个标识文件。正常情况:会在启动时创建,而关闭服务时删除。但如果遇到服务器宕机,或者kill -9这样一些非正常关闭服务的情况,abort文件就不会删除。因为RocketMQ就可以判断上一次服务是非正常关闭的,后续就会做一些数据恢复的操作
  • lock:文件夹已经被rocketmq占用,如果系统有多个rocketmq服务,提示已占用,启动失败

消息整体结构图
在这里插入图片描述
1、CommitLog文件存储所有消息实体

  • 所有生产者发过来的消息,都会依次存储到Commitlog文件中,可以减少查找目标文件的时间,让消息以最快的速度落盘。
  • CommitLog文件大小固定(1GB),但是其中存储的每个消息单元长度是不固定的,具体格式可参考org.apache.rocketmq.store.CommitLog。
    在这里插入图片描述
  • RocketMQ在每次存CommitLog文件时,都会去检查当前CommitLog文件空间是否足够,不够就创建一个新的CommitLog文件,文件名为当前消息的偏移量。

2、ConsumeQueue文件主要是加速消费者的消息索引

  • 每个文件夹对应RocketMQ中的一个MessageQueue,文件夹下的文件记录每个MessageQueue中消息在CommitLog文件当中的偏移量。
  • 消费者通过ComsumeQueue文件,可以快速找到CommitLog文件中的消息记录。消费者在ConsumeQueue文件当中的消费进度,会保存在config/consumerOffset.json文件中。
  • 每个ConsumeQueue文件固定由30万个固定大小20byte的数据块组成(大约6M)。 数据块包括:msgPhyOffset(8byte,消息在文件中的起始位置) + msgSize(4byte,消息在文件中占用的长度) + msgTagCode(8byte,消息的tag的Hash值)。
  • 在ConsumeQueue.java有一个常量CQ_STORE_UNIT_SIZE=20,这个常量表示一个数据块的大小。

3、IndexFile文件主要是辅助消息检索

  • 消息消费按照MeessageId或者MessageKey来检索文件,比如RocketMQ管理控制台的消息轨迹功能,ConsumeQueue文件就不够用了。
  • 文件名比较特殊,用时间命名的一个固定大小的文件。
  • indexHeader(固定40byte) + slot(固定500W个,每个固定20byte) + index(最多500W*4个,每个固定20byte) 三部分组成。

3.删除过期文件

  • 判断过期文件
    RocketMQ判断文件是否过期的唯一标准就是非当前写文件的保留时间,
    并不关心文件的消息是否被消费。所以,RocketMQ的消息堆积是有时间限度的,配置在broker.conf中fileReservedTime属性(文件保留时间,默认48小时)。
    在这里插入图片描述
  • 删除过期文件
    RocketMQ内部有一个定时任务,对文件进行扫描,并触发文件删除操作。配置在broker.conf的deleteWhen属性,默认凌晨四点
  • 另外,RocketMQ还会检查服务器磁盘空间是否足够,如果磁盘空间的使用率达到阈值,也会触发过期文件删除。RocketMQ官方建议 broker的磁盘空间不要少于4G

4.高效文件写

4.1 零拷贝

1.CPU拷贝和DMA拷贝

  操作系统的内存空间分为用户态和内核态。用户态的应用程序无法直接操作硬件,需要转换成内核空间,才能操作硬件(为了保护操作系统安全)。因此,应用程序与网卡、磁盘等硬件进行数据交互时,就需要在用户态和内核态之间来回的复制数据。CPU进行数据复制任务的分配、调度等管理步骤,早先这些IO接口都是由CPU独立负责,所以当发生大规模的数据读写操作时,CPU的占用率会非常高。
Alt
  之后,操作系统为了避免CPU被各种IO调用占满,引入了DMA(直接存储器存储),由DMA负责频繁的IO操作。DMA是一套独立指令集,不占用CPU计算资源。CPU不参数据复制工作,只管理DMA权限。
在这里插入图片描述
  但是,数据复制过程中需要借助数据总线。当系统IO操作频繁时,会造成总线冲突,影响数据读写性能。所以,又引入了Channel通道,一个完全独立的处理器,专门负责IO操作。Channel有自己的IO指令,与CPU无关,更适合大型的IO操作,性能更高。Java应用层与零拷贝相关的操作都是通过Channel的子类实现,其实借鉴了操作系统中的概念。
在这里插入图片描述
  所以,零拷贝技术,并不是不拷贝,而是尽量减少CPU拷贝,提高性能。

2.mmap文件映射机制

在这里插入图片描述
  在一次普通读写操作中,需要进行四次数据拷贝:内核态与用户态间的状态切换数据拷贝(性能较慢的CPU拷贝两次),磁盘与内核态间的数据拷贝(优化后的DMA拷贝两次)。mmap文件映射方式是在用户态不再保存文件真实数据,只保存文件映射,包括文件内存起始地址,文件大小等。真实的数据,直接通过操作映射,在内核态完成数据复制。
在这里插入图片描述
  在JDK的NIO包中,java.nio.HeapByteBuffer映射的是JVM的一块堆内内存,由一个byte数组缓存数据内容,所有的读写操作需要先操作这个byte数组。这是没有使用零拷贝的普通文件读写机制。
  而NIO包中的java.nio.DirectByteBuffer实现类则映射的是一块堆外内存。没有任何数据结构保存数据内容,只保存一个内存地址。所有对数据的读写操作,都通过unsafe魔法类直接交由内核完成,这其实就是mmap的读写机制。
  在java中大量使用了mmap文件映射,在Linux机器上通过jps查看运行的进程ID,再使用lsof -p {PID}的方式查看文件的映射情况
在这里插入图片描述

cwd 表示程序的工作目录。rtd 表示用户的根目录。 txt表示运行程序的指令。
下面的1u表示Java应用的标准输出,2u表示Java应用的标准错误输出,默认的/dev/pts/1是linux当中的伪终端。通常服务器上会写 java xxx 1>text.txt 2>&1 这样的脚本,就是指定这里的1u,2u。

  总之,mmap机制适合操作小文件,如果文件太大,映射信息也会过大。建议的映射文件大小不超过2G 。RocketMQ的CommitLog文件固定保持在1G,也是为了方便文件映射

3.sendFile机制

在这里插入图片描述
  以磁盘复制数据到网卡为例,早期的sendfile实现机制还是依靠CPU进行页缓存与socket缓存区间的数据拷贝。后期在拷贝过程中,不拷贝文件内容,只拷贝一个带有文件位置和长度等信息的文件描述符FD,大大减少了传递的数据。真实的数据内容,由DMA控制器从页缓存打包异步发送到socket中。
  sendfile机制在内核态直接完成数据复制,不需要用户态参与,所以这种机制的传输效率是非常稳定的。sendfile机制非常适合大数据的复制转移。

4.2 顺序写

  如果应用程序往磁盘写文件时磁盘空间不连续,会产生很多碎片,在磁盘多个扇区间进行大量的随机写。大量的寻址操作,严重影响写数据性能。顺序写机制在磁盘中提前申请一块连续的磁盘空间,每次写数据时直接在之前写入的地址后面接着写,避免寻址操作。
  Kafka官方曾说明,顺序写的性能基本能够达到内存级别。如果配备固态硬盘,顺序写的性能甚至有可能超过写内存。
  org.apache.rocketmq.store.CommitLog#DefaultAppendMessageCallback中的doAppend方法。在这个方法中,会以追加的方式将消息先写入到一个堆外内存byteBuffer中,然后再通过fileChannel写入到磁盘。

4.3 刷盘机制

  在操作系统层面,当应用程序写入一个文件时,文件内容并不直接写入到硬件,而是先写入操作系统的一个缓存PageCache中。PageCache缓存以4K大小为单位,缓存文件的具体内容。这些写入到PageCache中的文件,在应用程序看来,是已经完全落盘了的,可以正常修改、复制等等。但是,PageCache依然是内存状态,所以一断电就会丢失。因此,需要将内存状态的数据写入到磁盘当中,这样数据才能真正完成持久化,断电也不会丢失。这个过程称为刷盘
  操作系统只会在某些特定的时刻将PageCache写入到磁盘,例如正常关机时,就会完成PageCache刷盘。另外,在Linux中,对于有数据修改的PageCache,会标记为Dirty(脏页)状态。当Dirty Page的比例达到一定的阈值,就会触发一次刷盘操作。
  但是,只要操作系统的刷盘操作不是即时执行,那就避免不了非正常宕机时的数据丢失问题。因此,操作系统也提供了一个系统调用,应用程序可以自行调用这个系统调用,完成PageCache的强制刷盘(在Linux中是fsync)。
  RocketMQ也设计了两种刷盘机制,同步刷盘和异步刷盘
在这里插入图片描述

  • 同步刷盘:在返回写成功状态时,消息已经被写入磁盘。消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。同步刷盘机制会更频繁的调用fsync,所以吞吐量相比异步刷盘会降低,但是数据的安全性会得到提高。
  • 异步刷盘:在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
  • 配置方式:通过Broker配置文件里的flushDiskType 参数设的,这个参数被配置成SYNC_FLUSH、ASYNC_FLUSH(默认)中的一个。

5.消息主从复制

消息复制的方式分为同步复制和异步复制

  • 同步复制:
    同步复制是等Master和Slave都写入消息成功后才反馈给客户端写入成功的状态。
    在同步复制下,如果Master节点故障,Slave上有全部的数据备份,这样容易恢复数据。但是同步复制会增大数据写入的延迟,降低系统的吞吐量。
  • 异步复制:
    异步复制是只要master写入消息成功,就反馈给客户端写入成功的状态。然后再异步的将消息复制给Slave节点。
    在异步复制下,系统拥有较低的延迟和较高的吞吐量。但是如果master节点故障,而有些数据没有完成复制,就会造成数据丢失。
  • 配置方式:
    消息复制方式是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成ASYNC_MASTER(默认)、 SYNC_MASTER、SLAVE三个值中的一个。

6.负载均衡

1.Producer负载均衡

  Producer发消息时,默认会采用递增取模的方式轮询往目标Topic下所有不同broker的MessageQueue发消息。
  生产者在发消息时,可以指定一个MessageQueueSelector,通过这个对象将消息发送到自己指定的MessageQueue上,保证消息局部有序。

2.Consumer负载均衡

  Consumer也是以MessageQueue为单位进行负载均衡。分为集群模式和广播模式

  1. 集群模式
      在集群模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue。
      每当实例的数量有变更,都会触发一次所有实例的负载均衡,按照queue的数量和实例的数量平均分配queue给每个实例。
      每次分配时,都会将MessageQueue和消费者ID进行排序后,再用不同的分配算法进行分配。内置的分配的算法共有六种,分别对应AllocateMessageQueueStrategy下的六种实现类,可以在consumer中直接set来指定。默认情况下使用的是最简单的平均分配策略。
  • AllocateMachineRoomNearby: 将同机房的Consumer和Broker优先分配在一起。
      通过一个machineRoomResolver对象定制Consumer和Broker的机房解析规则。然后引入另外一个分配策略对同机房的Broker和Consumer进行分配。一般用简单的平均分配策略或者轮询分配策略。
      源码中有测试代码AllocateMachineRoomNearByTest。
      在示例中:
    在这里插入图片描述
        Broker的机房指定方式:messageQueue.getBrokerName().split(“-”)[0],
        Consumer的机房指定方式:clientID.split(“-”)[0]
        clinetID的构建方式:见ClientConfig.buildMQClientId方法。按他的测试代码应该是要把clientIP指定为IDC1-CID-0这样的形式。
    平均分配示例
  • AllocateMessageQueueAveragely:平均分配。将所有MessageQueue平均分给每一个消费者
  • AllocateMessageQueueAveragelyByCircle: 轮询分配。轮流的给一个消费者分配一个MessageQueue。
  • AllocateMessageQueueByConfig: 不分配,直接指定一个messageQueue列表。类似于广播模式,直接指定所有队列。
  • AllocateMessageQueueByMachineRoom:按逻辑机房的概念进行分配。又是对BrokerName和ConsumerIdc有定制化的配置。
  • AllocateMessageQueueConsistentHash。源码中有测试代码
  • AllocateMessageQueueConsitentHashTest。这个一致性哈希策略只需要指定一个虚拟节点数,是用的一个哈希环的算法,虚拟节点是为了让Hash数据在换上分布更为均匀。
  1. 广播模式
      广播模式下,每一条消息都会投递给订阅了Topic的所有消费者实例,也就没有消息分配这一说。而在实现上,就是在Consumer分配Queue时,所有Consumer都分到所有的Queue。
      广播模式实现关键是将消费者的消费偏移量不再保存到broker,而是保存到客户端,由客户端自行维护自己的消费偏移量。

7.消息重试

  广播模式不存在消息重试,只有通过设置返回状态达到普通消息重试的结果。

1.如何让消息进行重试

  集群消费模式下消费失败后,需要在消息监听器接口的实现中进行配置实现消息重试。有三种配置方式:

  • 返回ConsumeConcurrentlyStatus.RECONSUME_LATER(推荐)
  • 返回null
  • 抛出异常
 @Override
 public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
    //处理消息
    doConsumeMessage(msgs);
    //方式1:返回 ConsumeConcurrentlyStatus.RECONSUME_LATER,消息将重试
    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
    //方式2:返回 null,消息将重试
    return null;
    //方式3:直接抛出异常, 消息将重试
    throw new RuntimeException("Consumer Message exceotion");
}

如果希望消费失败后不重试,可以直接返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS。

2.重试消息如何处理

重试的消息会进入一个 “%RETRY%”+ConsumeGroup 的队列。
RocketMQ默认允许每条消息最多重试16次,每次重试的间隔时间如下:

重试次数与上次重试的间隔时间重试次数与上次重试的间隔时间
110 秒97 分钟
230 秒108 分钟
31 分钟119 分钟
42 分钟1210 分钟
53 分钟1320 分钟
64 分钟1430 分钟
75 分钟151 小时
86 分钟162 小时

重试时间与延迟消息的延迟级别对应,取的是延迟级别的后16级别(共18级)
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
测试重试时间可以将源码中的org.apache.rocketmq.example.quickstart.Consumer里的消息监听器返回状态改为ConsumeConcurrentlyStatus.RECONSUME_LATER测试。

重试次数:
如果消息重试16次后仍然失败,消息将不再投递,进入死信队列。
一条消息无论重试多少次,这些重试消息的MessageId始终都是一样的。
RocketMQ可以定制重试次数。例如通过consumer.setMaxReconsumeTimes(20);将重试次数设定为20次。当定制的重试次数超过16次后,消息的重试时间间隔永远为最大的2小时。

关于MessageId:
在老版RocketMQ,一条消息无论重试多少次,MessageId始终都一样。在4.9.1版本中,每次重试MessageId都会重建。

配置覆盖:
设置消息最大重试次数对相同GroupID下所有Consumer实例有效,最后启动的Consumer会覆盖之前启动的Consumer配置。

8.死信队列

当一条消息消费失败,RocketMQ自动重试消息。如果消息超过最大重试次数,RocketMQ不会立刻将这个消息丢弃,而会将其发送到这个消费者组对应的特殊队列:死信队列。

RocketMQ默认重试次数是16次。见源码org.apache.rocketmq.common.subscription.SubscriptionGroupConfig中的retryMaxTimes属性。
>
这个重试次数可以在消费者端进行配置。 例如DefaultMQPushConsumer实例中有个setMaxReconsumeTimes方法指定重试次数。

死信队列的名称是%DLQ%+ConsumGroup
在这里插入图片描述

死信队列特征

  • 一个死信队列对应一个ConsumGroup,而不是对应某个消费者实例。
  • 如果一个ConsumeGroup没有产生死信队列,RocketMQ就不会为其创建相应的死信队列。
  • 一个死信队列包含了这个ConsumeGroup里的所有死信消息,而不区分该消息属于哪个Topic。
  • 死信队列中的消息不会再被消费者正常消费。
  • 死信队列的有效期跟正常消息相同,默认3天,对应broker.conf中的fileReservedTime属性。超过这个最长时间的消息都会被删除,而不管消息是否消费过。

  通常,一条消息进入死信队列,意味着该消息在处理过程中出现了比较严重的错误,无法自行恢复。需要人工查看死信队列中的消息,排查错误原因,处理死信消息,比如转发到正常的Topic重新进行消费,或者丢弃。

  默认创建的死信队列中的消息在控制台和消费者中都无法读取。这是因为这些默认的死信队列,他们的权限perm被设置成了2:禁读(这个权限有三种 2:禁读,4:禁写,6:可读可写)。需要手动将死信队列的权限配置成6,才能被消费(可以通过mqadmin指定或者web控制台)。

9.消息幂等

幂等概念

在MQ系统中,对于消息幂等有三种实现语义:

  • at most once 最多一次:每条消息最多只会被消费一次
  • at least once 至少一次:每条消息至少会被消费一次
  • exactly once 刚刚好一次:每条消息都只会确定的消费一次

  其中,at most once最好保证,RocketMQ中通过异步发送、sendOneWay等方式保证最多一次。
  而at least once这个语义,RocketMQ也有同步发送、事务消息等很多方式能够保证。
  而exactly once是MQ中最理想也是最难保证的一种语义,需要有非常精细的设计才行。RocketMQ只能保证at least once,保证不了exactly once。所以,使用RocketMQ时,需要由业务系统自行保证消息的幂等性。但是,对于exactly once语义,阿里云的商业版RocketMQ明确有API支持,至于如何实现的,就不得而知了。

幂等必要性

  • 发送时消息重复
    当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。
  • 投递时消息重复
    消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。
  • 负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启)
    当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到重复消息。

处理方式

  在RocketMQ中是无法保证每个消息只被投递一次,所以要在业务上自行保证消息消费的幂等性。
  而要处理这个问题,RocketMQ的每条消息都有一个唯一的MessageId,这个参数在多次投递的过程中是不会改变的,所以业务上可以用这个MessageId来作为判断幂等的关键依据。
  但是,这个MessageId是无法保证全局唯一的,也会有冲突的情况。所以在一些对幂等性要求严格的场景,最好是使用业务上唯一的一个标识比较靠谱,例如订单ID。而这个业务标识可以使用Message的Key来进行传递。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

paopaodog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值