消息队列之RocketMQ(一)初步了解

294 阅读45分钟

RocketMQ简介

先来吹一波,哈哈哈

RocketMQ是一个基于自有的通信协议采用Java语言编写的成熟企业级消息产品,统一的处理消息引擎,轻量级的数据处理平台。

  • 低延迟:在高并发压力下,超过99.6%的响应延迟在1毫秒之内
  • 面向金融的:系统支撑的追踪和审计功能具备高可用性
  • 行业可发展性:保证了万亿级别的消息容量
  • 对大数据友好的:具备通用集成功能的批量传输支撑了海量吞吐
  • 大量的积累:只要给予足够的磁盘空间,就可以累积消息而不会造成性能损失
  • 提供多语言客户端SDK: Java、C++、Python、Go、.NET等语言

RocketMQ各部分角色介绍

话不多说,先上图:

image.png

image.png

NameServer

命名服务,更新和路由发现 broker服务。NameServer的作用是为消息生产者、消息消费者提供关于主题 Topic 的路由信息,NameServer除了要存储路由的基础信息,还要能够管理 Broker节点,包括路由注册、路由删除等功能。

Broker

它能接收producer和consumer的请求,并调用store层服务对消息进行处理。HA服务的基本单元,支持同步双写,异步双写等模式。

Producer/Consumer

java版本的MQ客户端实现,包括生产者和消费者。

Store

存储层实现,同时包括了索引服务,高可用HA服务实现。

Netty Remoting Server/Netty Remoting Client

基于netty的底层通信实现,所有服务间的交互都基于此模块。也区分服务端和客户端

RocketMQ基本概念

image.png

生产者

生产者是 Apache RocketMQ 系统中用来构建并传输消息到服务端的运行实体。

生产者通常被集成在业务系统中,将业务消息按照要求封装成 Apache RocketMQ 的消息(Message)并发送至服务端。

在消息生产者中,可以定义如下传输行为:

  • 发送方式:生产者可通过API接口设置消息发送的方式。Apache RocketMQ 支持同步传输和异步传输。
  • 批量发送:生产者可通过API接口设置消息批量传输的方式。例如,批量发送的消息条数或消息大小。
  • 事务行为:Apache RocketMQ 支持事务消息,对于事务消息需要生产者配合进行事务检查等行为保障事务的最终一致性。具体信息,请参见事务消息

生产者和主题的关系为多对多关系,即同一个生产者可以向多个主题发送消息,对于平台类场景如果需要发送消息到多个主题,并不需要创建多个生产者;同一个主题也可以接收多个生产者的消息,以此可以实现生产者性能的水平扩展和容灾。

image.png

模型关系

在 Apache RocketMQ 的领域模型中,生产者的位置和流程如下

image.png

  1. 消息由生产者初始化并发送到Apache RocketMQ 服务端。
  2. 消息按照到达Apache RocketMQ 服务端的顺序存储到主题的指定队列中。
  3. 消费者按照指定的订阅关系从Apache RocketMQ 服务端中获取消息并消费。

内部属性

客户端ID

  • 定义:生产者客户端的标识,用于区分不同的生产者。集群内全局唯一。
  • 取值:客户端ID由Apache RocketMQ 的SDK自动生成,主要用于日志查看、问题定位等运维场景,不支持修改。

通信参数

  • 接入点信息  (必选)  :连接服务端的接入地址,用于识别服务端集群。 接入点必须按格式配置,建议使用域名,避免使用IP地址,防止节点变更无法进行热点迁移。
  • 身份认证信息  (可选)  :客户端用于身份验证的凭证信息。 仅在服务端开启身份识别和认证时需要传输。
  • 请求超时时间  (可选)  :客户端网络请求调用的超时时间。取值范围和默认值,请参见参数限制

预绑定主题列表

  • 定义:Apache RocketMQ 的生产者需要将消息发送到的目标主题列表,主要作用如下:

    • 事务消息  (必须设置)  :事务消息场景下,生产者在故障、重启恢复时,需要检查事务消息的主题中是否有未提交的事务消息。避免生产者发送新消息后,主题中的旧事务消息一直处于未提交状态,造成业务延迟。

    • 非事务消息  (建议设置)  :服务端会在生产者初始化时根据预绑定主题列表,检查目标主题的访问权限和合法性,而不需要等到应用启动后再检查。

      若未设置,或后续消息发送的目标主题动态变更, Apache RocketMQ 会对目标主题进行动态补充检验。

  • 约束:对于事务消息,预绑定列表必须设置,且需要和事务检查器一起配合使用。

事务检查器

  • 定义:Apache RocketMQ 的事务消息机制中,为保证异常场景下事务的最终一致性,生产者需要主动实现事务检查器的接口。具体信息,请参见事务消息
  • 发送事务消息时,事务检查器必须设置,且需要和预绑定主题列表一起配合使用。

发送重试策略

消息(Message)

消息是 Apache RocketMQ 中的最小数据传输单元。生产者将业务数据的负载和拓展属性包装成消息发送到 Apache RocketMQ 服务端,服务端按照相关语义将消息投递到消费端进行消费。

Apache RocketMQ 的消息模型具备如下特点:

  • 消息不可变性

    消息本质上是已经产生并确定的事件,一旦产生后,消息的内容不会发生改变。即使经过传输链路的控制也不会发生变化,消费端获取的消息都是只读消息视图。

  • 消息持久化

    Apache RocketMQ 会默认对消息进行持久化,即将接收到的消息存储到 Apache RocketMQ 服务端的存储文件中,保证消息的可回溯性和系统故障场景下的可恢复性。

模型关系

在整个 Apache RocketMQ 的领域模型中,消息所处的流程和位置如下:

image.png

  1. 消息由生产者初始化并发送到Apache RocketMQ 服务端。
  2. 消息按照到达Apache RocketMQ 服务端的顺序存储到队列中。
  3. 消费者按照指定的订阅关系从Apache RocketMQ 服务端中获取消息并消费。

消息内部属性

系统保留属性

主题名称

  • 定义:当前消息所属的主题的名称。集群内全局唯一。更多信息,请参见主题(Topic)
  • 取值:从客户端SDK接口获取。

消息类型

  • 定义:当前消息的类型。

  • 取值:从客户端SDK接口获取。Apache RocketMQ 支持的消息类型如下:

    • Normal:普通消息,消息本身无特殊语义,消息之间也没有任何关联。
    • FIFO:顺序消息,Apache RocketMQ 通过消息分组MessageGroup标记一组特定消息的先后顺序,可以保证消息的投递顺序严格按照消息发送时的顺序。
    • Delay:定时/延时消息,通过指定延时时间控制消息生产后不要立即投递,而是在延时间隔后才对消费者可见。
    • Transaction:事务消息,Apache RocketMQ 支持分布式事务消息,支持应用数据库更新和消息调用的事务一致性保障。

消息队列

  • 定义:实际存储当前消息的队列。更多信息,请参见队列(MessageQueue)
  • 取值:由服务端指定并填充。

消息位点

  • 定义:当前消息存储在队列中的位置。更多信息,请参见消费进度原理
  • 取值:由服务端指定并填充。取值范围:0~long.Max。

消息ID

  • 定义:消息的唯一标识,集群内每条消息的ID全局唯一。
  • 取值:生产者客户端系统自动生成。固定为数字和大写字母组成的32位字符串。

索引Key列表(可选)

  • 定义:消息的索引键,可通过设置不同的Key区分消息和快速查找消息。
  • 取值:由生产者客户端定义。

过滤标签Tag(可选)

  • 定义:消息的过滤标签。消费者可通过Tag对消息进行过滤,仅接收指定标签的消息。
  • 取值:由生产者客户端定义。
  • 约束:一条消息仅支持设置一个标签。

定时时间(可选)

  • 定义:定时场景下,消息触发延时投递的毫秒级时间戳。更多信息,请参见定时/延时消息
  • 取值:由消息生产者定义。
  • 约束:最大可设置定时时长为40天。

消息发送时间

  • 定义:消息发送时,生产者客户端系统的本地毫秒级时间戳。
  • 取值:由生产者客户端系统填充。
  • 说明:客户端系统时钟和服务端系统时钟可能存在偏差,消息发送时间是以客户端系统时钟为准。

消息保存时间戳

  • 定义:消息在Apache RocketMQ 服务端完成存储时,服务端系统的本地毫秒级时间戳。 对于定时消息和事务消息,消息保存时间指的是消息生效对消费方可见的服务端系统时间。
  • 取值:由服务端系统填充。
  • 说明:客户端系统时钟和服务端系统时钟可能存在偏差,消息保留时间是以服务端系统时钟为准。

消费重试次数

  • 定义:消息消费失败后,Apache RocketMQ 服务端重新投递的次数。每次重试后,重试次数加1。更多信息,请参见消费重试
  • 取值:由服务端系统标记。首次消费,重试次数为0;消费失败首次重试时,重试次数为1。

业务自定义属性

  • 定义:生产者可以自定义设置的扩展信息。
  • 取值:由消息生产者自定义,按照字符串键值对设置。

消息负载

  • 定义:业务消息的实际报文数据。
  • 取值:由生产者负责序列化编码,按照二进制字节传输。
  • 约束:请参见参数限制

行为约束

消息大小不得超过其类型所对应的限制,否则消息会发送失败。

系统默认的消息最大限制如下:

  • 普通和顺序消息:4 MB
  • 事务和定时或延时消息:64 KB

主题(Topic)

主题是 Apache RocketMQ 中消息传输和存储的顶层容器,用于标识同一类业务逻辑的消息。

  • 定义数据的分类隔离:  在 Apache RocketMQ 的方案设计中,建议将不同业务类型的数据拆分到不同的主题中管理,通过主题实现存储的隔离性和订阅隔离性。
  • 定义数据的身份和权限:  Apache RocketMQ 的消息本身是匿名无身份的,同一分类的消息使用相同的主题来做身份识别和权限管理。

模型关系

在整个 Apache RocketMQ 的领域模型中,主题所处的流程和位置如下:

image.png 主题是 Apache RocketMQ 的顶层存储,所有消息资源的定义都在主题内部完成,但主题是一个逻辑概念,并不是实际的消息容器。

主题内部由多个队列组成,消息的存储和水平扩展能力最终是由队列实现的;并且针对主题的所有约束和属性设置,最终也是通过主题内部的队列来实现。

内部属性

主题名称

  • 定义:主题的名称,用于标识主题,主题名称集群内全局唯一。
  • 取值:由用户创建主题时定义。
  • 约束:请参见参数限制

队列列表

  • 定义:队列作为主题的组成单元,是消息存储的实际容器,一个主题内包含一个或多个队列,消息实际存储在主题的各队列内。更多信息,请参见队列(MessageQueue)
  • 取值:系统根据队列数量给主题分配队列,队列数量创建主题时定义。
  • 约束:一个主题内至少包含一个队列。

消息类型

  • 定义:主题所支持的消息类型。

  • 取值:创建主题时选择消息类型。Apache RocketMQ 支持的主题类型如下:

    • Normal:普通消息,消息本身无特殊语义,消息之间也没有任何关联。
    • FIFO:顺序消息,Apache RocketMQ 通过消息分组MessageGroup标记一组特定消息的先后顺序,可以保证消息的投递顺序严格按照消息发送时的顺序。
    • Delay:定时/延时消息,通过指定延时时间控制消息生产后不要立即投递,而是在延时间隔后才对消费者可见。
    • Transaction:事务消息,Apache RocketMQ 支持分布式事务消息,支持应用数据库更新和消息调用的事务一致性保障。
  • 约束:Apache RocketMQ 从5.0版本开始,支持强制校验消息类型,即每个主题只允许发送一种消息类型的消息,这样可以更好的运维和管理生产系统,避免混乱。为保证向下兼容4.x版本行为,强制校验功能默认关闭,推荐通过服务端参数 enableTopicMessageTypeCheck 开启校验。

行为约束

消息类型强制校验

Apache RocketMQ 5.x版本支持将消息类型拆分到主题中进行独立运维和处理,因此系统会对发送的消息类型和主题定的消息类型进行强制校验,若校验不通过,则消息发送请求会被拒绝,并返回类型不匹配异常。校验原则如下:

  • 消息类型必须一致:发送的消息的类型,必须和目标主题定义的消息类型一致。

  • 主题类型必须单一:每个主题只支持一种消息类型,不允许将多种类型的消息发送到同一个主题中。 常见错误使用场景

  • 发送的消息类型不匹配例如,创建主题时消息类型定义为顺序消息,发送消息时发送事务消息到该主题中,此时消息发送请求会被拒绝,并返回类型不匹配异常。

  • 单一消息主题混用例如,创建主题时消息类型定义为普通消息,发送消息时同时发送普通消息和顺序消息到该主题中,则顺序消息的发送请求会被拒绝,并返回类型不匹配异常。

消息队列

队列是 Apache RocketMQ 中消息存储和传输的实际容器,也是 Apache RocketMQ 消息的最小存储单元。 Apache RocketMQ 的所有主题都是由多个队列组成,以此实现队列数量的水平拆分和队列内部的流式存储。

队列的主要作用如下:

  • 存储顺序性

    队列天然具备顺序性,即消息按照进入队列的顺序写入存储,同一队列间的消息天然存在顺序关系,队列头部为最早写入的消息,队列尾部为最新写入的消息。消息在队列中的位置和消息之间的顺序通过位点(Offset)进行标记管理。

  • 流式操作语义

    Apache RocketMQ 基于队列的存储模型可确保消息从任意位点读取任意数量的消息,以此实现类似聚合读取、回溯读取等特性,这些特性是RabbitMQ、ActiveMQ等非队列存储模型不具备的。

模型关系

在整个 Apache RocketMQ 的领域模型中,队列所处的流程和位置如下:

image.png

Apache RocketMQ 默认提供消息可靠存储机制,所有发送成功的消息都被持久化存储到队列中,配合生产者和消费者客户端的调用可实现至少投递一次的可靠性语义。

Apache RocketMQ 队列模型和Kafka的分区(Partition)模型类似。在 Apache RocketMQ 消息收发模型中,队列属于主题的一部分,虽然所有的消息资源以主题粒度管理,但实际的操作实现是面向队列。例如,生产者指定某个主题,向主题内发送消息,但实际消息发送到该主题下的某个队列中。

Apache RocketMQ 中通过修改队列数量,以此实现横向的水平扩容和缩容。

内部属性

读写权限 - 一般的业务不会涉及这部分

  • 定义:当前队列是否可以读写数据。

  • 取值:由服务端定义,枚举值如下

    • 6:读写状态,当前队列允许读取消息和写入消息。
    • 4:只读状态,当前队列只允许读取消息,不允许写入消息。
    • 2:只写状态,当前队列只允许写入消息,不允许读取消息。
    • 0:不可读写状态,当前队列不允许读取消息和写入消息。
  • 约束:队列的读写权限属于运维侧操作,不建议频繁修改。

行为约束

每个主题下会由一到多个队列来存储消息,每个主题对应的队列数与消息类型以及实例所处地域(Region)相关,队列数暂不支持修改。

使用建议

按照实际业务消耗设置队列数

Apache RocketMQ 的队列数可在创建主题或变更主题时设置修改,队列数量的设置应遵循少用够用原则,避免随意增加队列数量。

主题内队列数过多可能对导致如下问题:

  • 集群元数据膨胀

    Apache RocketMQ 会以队列粒度采集指标和监控数据,队列过多容易造成管控元数据膨胀。

  • 客户端压力过大

    Apache RocketMQ 的消息读写都是针对队列进行操作,队列过多对应更多的轮询请求,增加系统负荷。

常见队列增加场景

  • 需要增加队列实现物理节点负载均衡

    Apache RocketMQ 每个主题的多个队列可以分布在不同的服务节点上,在集群水平扩容增加节点后,为了保证集群流量的负载均衡,建议在新的服务节点上新增队列,或将旧的队列迁移到新的服务节点上。

  • 需要增加队列实现顺序消息性能扩展

    在 Apache RocketMQ 服务端4.x版本中,顺序消息的顺序性在队列内生效的,因此顺序消息的并发度会在一定程度上受队列数量的影响,因此建议仅在系统性能瓶颈时再增加队列。

消费者分组

消费者分组是 Apache RocketMQ 系统中承载多个消费行为一致的消费者的负载均衡分组。

和消费者不同,消费者分组并不是运行实体,而是一个逻辑资源。在 Apache RocketMQ 中,通过消费者分组内初始化多个消费者实现消费性能的水平扩展以及高可用容灾。

在消费者分组中,统一定义以下消费行为,同一分组下的多个消费者将按照分组内统一的消费行为和负载均衡策略消费消息。

  • 订阅关系:Apache RocketMQ 以消费者分组的粒度管理订阅关系,实现订阅关系的管理和追溯。具体信息,请参见订阅关系(Subscription)
  • 投递顺序性:Apache RocketMQ 的服务端将消息投递给消费者消费时,支持顺序投递和并发投递,投递方式在消费者分组中统一配置。具体信息,请参见顺序消息
  • 消费重试策略: 消费者消费消息失败时的重试策略,包括重试次数、死信队列设置等。具体信息,请参见消费重试

模型关系

在 Apache RocketMQ 的领域模型中,消费者分组的位置和流程如下:

image.png

  1. 消息由生产者初始化并发送到Apache RocketMQ 服务端。
  2. 消息按照到达Apache RocketMQ 服务端的顺序存储到主题的指定队列中。
  3. 消费者按照指定的订阅关系从Apache RocketMQ 服务端中获取消息并消费。

内部属性

消费者分组名称

  • 定义:消费者分组的名称,用于区分不同的消费者分组。集群内全局唯一。
  • 取值:消费者分组由用户设置并创建。具体命名规范,请参见参数限制

投递顺序性

  • 定义:消费者消费消息时,Apache RocketMQ 向消费者客户端投递消息的顺序。

    根据不同的消费场景,Apache RocketMQ 提供顺序投递和并发投递两种方式。具体信息,请参见顺序消息

  • 取值:默认投递方式为并发投递。

消费重试策略

  • 定义:消费者消费消息失败时,系统的重试策略。消费者消费消息失败时,系统会按照重试策略,将指定消息投递给消费者重新消费。具体信息,请参见消费重试

  • 取值:重试策略包括:

    • 最大重试次数:表示消息可以重新被投递的最大次数,超过最大重试次数还没被成功消费,消息将被投递至死信队列或丢弃。
    • 重试间隔:Apache RocketMQ 服务端重新投递消息的间隔时间。 最大重试次数和重试间隔的取值范围及默认值,请参见参数限制
  • 约束:重试间隔仅在PushConsumer消费类型下有效。

订阅关系

  • 定义:当前消费者分组关联的订阅关系集合。包括消费者订阅的主题,以及消息的过滤规则等。订阅关系由消费者动态注册到消费者分组中,Apache RocketMQ 服务端会持久化订阅关系并匹配消息的消费进度。更多信息,请参见订阅关系(Subscription)

行为约束

在 Apache RocketMQ 领域模型中,消费者的管理通过消费者分组实现,同一分组内的消费者共同分摊消息进行消费。因此,为了保证分组内消息的正常负载和消费,

Apache RocketMQ 要求同一分组下的所有消费者以下消费行为保持一致:

  • 投递顺序
  • 消费重试策略

使用建议

按照业务合理拆分分组

Apache RocketMQ 的消费者和主题是多对多的关系,对于消费者分组的拆分设计,建议遵循以下原则:

  • 消费者的投递顺序一致:同一消费者分组下所有消费者的消费投递顺序是相同的,统一都是顺序投递或并发投递,不同业务场景不能混用消费者分组。
  • 消费者业务类型一致:一般消费者分组和主题对应,不同业务域对消息消费的要求不同,例如消息过滤属性、消费重试策略不同。因此,不同业务域主题的消费建议使用不同的消费者分组,避免一个消费者分组消费超过10个主题。

订阅关系(Subscription)

订阅关系是 Apache RocketMQ 系统中消费者获取消息、处理消息的规则和状态配置。

订阅关系由消费者分组动态注册到服务端系统,并在后续的消息传输中按照订阅关系定义的过滤规则进行消息匹配和消费进度维护。

通过配置订阅关系,可控制如下传输行为:

  • 消息过滤规则:用于控制消费者在消费消息时,选择主题内的哪些消息进行消费,设置消费过滤规则可以高效地过滤消费者需要的消息集合,灵活根据不同的业务场景设置不同的消息接收范围。具体信息,请参见消息过滤
  • 消费状态:Apache RocketMQ 服务端默认提供订阅关系持久化的能力,即消费者分组在服务端注册订阅关系后,当消费者离线并再次上线后,可以获取离线前的消费进度并继续消费。

订阅关系判断原则

Apache RocketMQ 的订阅关系按照消费者分组和主题粒度设计,因此,一个订阅关系指的是指定某个消费者分组对于某个主题的订阅,判断原则如下:

  • 不同消费者分组对于同一个主题的订阅相互独立如下图所示,消费者分组Group A和消费者分组Group B分别以不同的订阅关系订阅了同一个主题Topic A,这两个订阅关系互相独立,可以各自定义,不受影响。

image.png

同一个消费者分组对于不同主题的订阅也相互独立如下图所示,消费者分组Group A订阅了两个主题Topic A和Topic B,对于Group A中的消费者来说,订阅的Topic A为一个订阅关系,订阅的Topic B为另外一个订阅关系,且这两个订阅关系互相独立,可以各自定义,不受影响。

模型关系

在 Apache RocketMQ 的领域模型中,订阅关系的位置和流程如下:

image.png

  1. 消息由生产者初始化并发送到Apache RocketMQ 服务端。
  2. 消息按照到达Apache RocketMQ 服务端的顺序存储到主题的指定队列中。
  3. 消费者按照指定的订阅关系从Apache RocketMQ 服务端中获取消息并消费。

内部属性

过滤类型

  • 定义:消息过滤规则的类型。订阅关系中设置消息过滤规则后,系统将按照过滤规则匹配主题中的消息,只将符合条件的消息投递给消费者消费,实现消息的再次分类。

  • 取值:

    • TAG过滤:按照Tag字符串进行全文过滤匹配。
    • SQL92过滤:按照SQL语法对消息属性进行过滤匹配。

过滤表达式

行为约束

订阅关系一致

Apache RocketMQ 是按照消费者分组粒度管理订阅关系,因此,同一消费者分组内的消费者在消费逻辑上必须保持一致,否则会出现消费冲突,导致部分消息消费异常。

  • 正确示例

    //Consumer c1
    Consumer c1 = ConsumerBuilder.build(groupA);
    c1.subscribe(topicA,"TagA");
    //Consumer c2
    Consumer c2 = ConsumerBuilder.build(groupA);
    c2.subscribe(topicA,"TagA");
    
  • 错误示例

    //Consumer c1
    Consumer c1 = ConsumerBuilder.build(groupA);
    c1.subscribe(topicA,"TagA");
    //Consumer c2Consumer 
    c2 = ConsumerBuilder.build(groupA);
    c2.subscribe(topicA,"TagB");
    

RocketMQ 简单使用

普通消息

本章节先会使用RocketMQ提供的原生客户端的API,当然除了原生客户端外,SpringBoot、SpringCloudStream也进行了集成,但本质上这些也是基于原生API的封装,所以只需掌握原生API,其他的也会水到渠成。

Java代码中使用普通消息的整体流程如下

导入MQ客户端依赖

org.apache.rocketmq

rocketmq-client

4.8.0

消息发送者步骤

1.创建消息生产者producer,并指定生产者组名

2.指定Nameserver地址

3.启动producer

4.创建消息对象,指定Topic、Tag和消息体

5.发送消息

6.关闭生产者producer

消息消费者步骤

1.创建消费者Consumer,指定消费者组名

2.指定Nameserver地址

3.订阅主题Topic和Tag

4.设置回调函数,处理消息

5.启动消费者consumer

三种消息发送方式

发送同步消息

同步发送是指消息发送方发出数据后,同步等待,直到收到接收方发回响应之后才发下一个请求。这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。

image.png

代码演示

image.png

发送结果分析

image.png

  • msgId

消息的全局唯一标识(RocketMQ的ID生成是使用机器IP和消息偏移量的组成),由消息队列 MQ 系统自动生成,唯一标识某条消息。

  • sendStatus

发送的标识:成功,失败等

  • queueId

queueId是Topic的分区;Producer发送具体一条消息的时,对应选择的该Topic下的某一个Queue的标识ID。

  • queueOffset

Message queue是无限长的数组。一条消息进来下标就会涨1,而这个数组的下标就是queueOffset,queueOffset是从0开始递增。

发送异步消息

异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。消息发送方在发送了一条消息后,不等接收方发回响应,接着进行第二条消息发送。发送方通过回调接口的方式接收服务器响应,并对响应结果进行处理。

image.png

代码演示

image.png

发送结果分析跟发送同步消息相同。

单向发送

image.png

这种方式主要用在不特别关心发送结果的场景,例如日志发送。单向(Oneway)发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。

代码演示

image.png

消息发送的权衡

image.png

两种消息消费方式

负载均衡模式(集群消费)

消费者采用负载均衡方式消费消息,一个分组(Group)下的多个消费者共同消费队列消息,每个消费者处理的消息不同。一个Consumer Group中的各个Consumer实例分摊去消费消息,即一条消息只会投递到一个Consumer Group下面的一个实例。例如某个Topic有3个队列,其中一个Consumer Group 有 3 个实例,那么每个实例只消费其中的1个队列。集群消费模式是消费者默认的消费方式。

image.png

代码演示

image.png

广播消费

image.png

广播消费模式中消息将对一个Consumer Group下的各个Consumer实例都投递一遍。即使这些 Consumer属于同一个Consumer Group,消息也会被Consumer Group 中的每个Consumer都消费一次。实际上,是一个消费组下的每个消费者实例都获取到了topic下面的每个Message Queue去拉取消费。所以消息会投递到每个消费者实例。

代码演示

image.png

消息消费时的权衡

负载均衡模式:适用场景&注意事项

Ø 消费端集群化部署,每条消息只需要被处理一次。

Ø 由于消费进度在服务端维护,可靠性更高。

Ø 集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。

Ø 集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。

广播模式:适用场景&注意事项

Ø 每条消息都需要被相同逻辑的多台机器处理。

Ø 消费进度在客户端维护,出现重复的概率稍大于集群模式。

Ø 广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。

Ø 广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。

Ø 广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。

Ø 目前仅 Java 客户端支持广播模式。

Ø 广播消费模式下不支持顺序消息。

Ø 广播消费模式下不支持重置消费位点。

Ø 广播模式下服务端不维护消费进度,所以消息队列 RocketMQ 控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。

顺序消息

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。区别如下:

生产消息时在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

全局有序

image.png

分区有序

image.png

全局有序

全局有序比较简单,主要控制在于创建Topic指定只有一个队列,同步确保生产者与消费者都只有一个实例进行即可。

分区有序

在电商业务场景中,一个订单的流程是:创建、付款、推送、完成。在加入RocketMQ后,一个订单会分别产生对于这个订单的创建、付款、推送、完成等消息,如果我们把所有消息全部送入到RocketMQ中的一个主题中,这里该如何实现针对一个订单的消息顺序性呢!如下图:

image.png

要完成分区有序性,在生产者环节使用自定义的消息队列选择策略,确保订单号尾数相同的消息会被先后发送到同一个队列中(案例中主题有3个队列,生产环境中可设定成10个满足全部尾数的需求),然后再消费端开启负载均衡模式,最终确保一个消费者拿到的消息对于一个订单来说是有序的。

代码案例

生产者代码

image.png

image.png

image.png

发送日志:

image.png

消费者代码

消费时,同一个OrderId获取到的肯定是同一个队列。从而确保一个订单中处理的顺序。

image.png

消费者日志:

image.png

注意事项

使用顺序消息:首先要保证消息是有序进入MQ的,消息放入MQ之前,对id等关键字进行取模,放入指定messageQueue,同时consume消费消息失败时,不能返回reconsume——later,这样会导致乱序,所以应该返回suspend_current_queue_a_moment,意思是先等一会,一会儿再处理这批消息,而不是放到重试队列里。

延时消息

概念

延时消息: Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递(被消费者消费),而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

image.png

适用场景

消息生产和消费有时间窗口要求:比如在电商交易中超时未支付关闭订单的场景,在订单创建时向RocketMQ发送一条延时消息。这条消息将会在 30 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略。

使用案例

Apache RocketMQ目前只支持固定精度的定时消息,因为如果要支持任意的时间精度,在 Broker 层面,必须要做消息排序,如果再涉及到持久化,那么消息排序要不可避免的产生巨大性能开销。(RocketMQ的商业版本Aliware MQ提供了任意时刻的定时消息功能,Apache的RocketMQ并没有,阿里并没有开源)

Apache RocketMQ发送延时消息是设置在每一个消息体上的,在创建消息时设定一个延时时间长度,消息将从当前发送时间点开始延迟固定时间之后才开始投递。

延迟消息的level,区分18个等级:level为1,表示延迟1秒后消费;level为2表示延迟5秒后消费;level为3表示延迟10秒后消费;以此类推;最大level为18表示延迟2个小时消费。具体标识如下:

level123456789
延迟1s5s10s30s1m2m3m4m5m
level101112131415161718
延迟6m7m8m9m10m20m30m1h2h

是这生产消息跟普通的生产消息类似,只需要在消息上设置延迟队列的level即可。消费消息跟普通的消费消息一致。

生产者

image.png

消费者

image.png

查看消费者消息信息,打印消费延迟与生产时设定符合。

image.png

批量消息

在高并发场景中,批量发送消息能显著提高传递消息发送时的性能(减少网络连接及IO的开销)。使用批量消息时的限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK(集群时会细讲),且不能是延时消息。

在发送批量消息时先构建一个消息对象集合,然后调用send(Collection msg)系列的方法即可。由于批量消息的4MB限制,所以一般情况下在集合中添加消息需要先计算当前集合中消息对象的大小是否超过限制,如果超过限制也可以使用分割消息的方式进行多次批量发送。

使用案例

一般批量发送(不考虑消息分割)****

因为批量消息是一个Collection,所以送入消息可以是List,也可以是Set,这里为方便起见,使用List进行批量组装发送。

image.png

批量切分发送

如果消息的总长度可能大于4MB时,这时候最好把消息进行分割,案例中以1M大小进行消息分割。

我们需要发送10万元素的数组,这个量很大,怎么快速发送完。使用批量发送,同时每一批控制在1M左右确保不超过消息大小限制。

image.png

image.png

消息的过滤

在实际的开发应用中,对于一类消息尽可能使用一个Topic进行存储,但在消费时需要选择您想要的消息,这时可以使用RocketMQ的消息过滤功能,具体实现是利用消息的Tag和Key。

Key一般用于消息在业务层面的唯一标识。对发送的消息设置好 Key,以后可以根据这个 Key 来查找消息。比如消息异常,消息丢失,进行查找会很方便。RocketMQ 会创建专门的索引文件,用来存储 Key与消息的映射,由于底层实现是 Hash 索引,应尽量使 Key唯一,避免潜在的哈希冲突。

Tag可以理解为是二级分类。以淘宝交易平台为例,订单消息和支付消息属于不同业务类型的消息,分别创建OrderTopic 和PayTopic,其中订单消息根据不同的商品品类以不同的 Tag 再进行细分,如手机类、家电类、男装类、女装类、化妆品类,最后它们都被各个不同的系统所接收。通过合理的使用 Topic 和 Tag,可以让业务结构清晰,更可以提高效率。

Key和Tag的主要差别是使用场景不同,Key主要用于通过命令行命令查询消息,而Tag用于在消息端的代码中,用来进行服务端消息过滤。

使用Key一般使用mqadmin管理工具,具体位置在RocketMQ/bin目录下。

Tag过滤

使用Tag过滤的方式是在消息生产时传入感兴趣的Tag标签,然后在消费端就可以根据Tag来选择您想要的消息。具体的操作是在创建Message的时候添加,一个Message只能有一个Tag。

使用案例

生产者发送60条消息,分别打上三种tag标签。

image.png

消费者消费时只选择TagA和TagB的消息。

image.png

注意事项

Tag过滤的形式非常简单,||代表或、*代表所有,所以使用Tag过滤这对于复杂的场景可能不起作用。在这种情况下,可以使用SQL表达式筛选消息。

Sql过滤

SQL特性可以通过发送消息时的属性来进行消息的过滤计算。具体的操作是使用SQL92标准的sql语句,前提是只有使用push模式的消费者才能用(消费的模式就是push)

SQL基本语法

数值比较: 比如:>,>=,<,<=,BETWEEN,=;

字符比较: 比如:=,<>,IN;

IS NULL 或者 IS NOT NULL;

逻辑符号: AND,OR,NOT;

常量支持类型为:

数值,比如:123,3.1415;

字符,比如:'abc',必须用单引号包裹起来;

NULL,特殊的常量

布尔值,TRUE 或 FALSE

注意事项

Sql过滤需要Broker开启这项功能(如果消费时使用SQL过滤抛出异常错误,说明Sql92功能没有开启),需要修改Broker.conf配置文件。加入enablePropertyFilter=true 然后重启Broker服务。

使用案例

消息生产者,发送消息时加入消息属性,你能通过putUserProperty来设置消息的属性,以下案例中生产者发送10条消息,除了设置Tag之外,另外设置属性a的值。

image.png

用MessageSelector.bySql来使用sql筛选消息

image.png

消费结果:按照Tag和SQL过滤消费3条消息。

image.png

分布式事务消息

分布式事务的来龙去脉

image.png

业务场景:用户A转账100元给用户B,这个业务比较简单,具体的步骤:

1、用户A的账户先扣除100元

2、再把用户B的账户加100元

如果在同一个数据库中进行,事务可以保证这两步操作,要么同时成功,要么同时不成功。这样就保证了转账的数据一致性。

但是在微服务架构中因为各个服务都是独立的模块,都是远程调用,没法在同一个事务中,所以就会遇到分布式事务问题。

RocketMQ中的处理方案

image.png

RocketMQ分布式事务方式:把扣款业务和加钱业务异步化,扣款成功后,发送“扣款成功消息”到RocketMQ,加钱业务订阅“扣款成功消息”,再对用户B加钱(扣款消息中包含了源账户和目标账户ID,以及钱数)

对于扣款业务来说,需规定是先扣款还是先向MQ发消息

场景一:先扣款后向MQ发消息

先扣款再发送消息,万一发送消息失败了,那用户B就没法加钱

场景二:先向MQ发像消息,后扣款

扣款成功消息发送成功,但用户A扣款失败,可加钱业务订阅到了消息,用户B加了钱

问题所在,也就是没法保证扣款和发送消息,同时成功,或同时失败;导致数据不一致。

为了解决以上问题,RocketMq把消息分为两个阶段:半事务阶段和确认阶段

半事务阶段:

该阶段主要发一个消息到rocketmq,但该消息只储存在commitlog中,但consumeQueue中不可见,也就是消费端(订阅端)无法看到此消息

确认阶段(commit/rollback):

该阶段主要是把半事务消息保存到consumeQueue中,即让消费端可以看到此消息,也就是可以消费此消息。如果是rollback就不保存。 image.png

整个流程:

1、A在扣款之前,先发送半事务消息
2、发送预备消息成功后,执行本地扣款事务
3、扣款成功后,再发送确认消息
4、B消息端(加钱业务)可以看到确认消息,消费此消息,进行加钱

注意:上面的确认消息可以为commit消息,可以被订阅者消费;也可以是Rollback消息,即执行本地扣款事务失败后,提交rollback消息,即删除那个半事务消息,订阅者无法消费。这样就可以解决以下异常问题:

异常1:如果发送半事务消息失败,下面的流程不会走下去,这个是正常的。
异常2:如果发送半事务消息成功,但执行本地事务失败。这个也没有问题,因为此半事务消息不会被消费端订阅到,消费端不会执行业务。
异常3:如果发送半事务消息成功,执行本地事务成功,但发送确认消息失败;这个就有问题了,因为用户A扣款成功了,但加钱业务没有订阅到确认消息,无法加钱。这里出现了数据不一致。

image.png

RocketMq如何解决上面的问题,核心思路就是【事务回查】,也就是RocketMq会定时遍历commitlog中的半事务消息。

对于异常3,发送半事务消息成功,本地扣款事务成功,但发送确认消息失败;因为RocketMq会进行回查半事务消息,在回查后发现业务已经扣款成功了,就补发“发送commit确认消息”;这样加钱业务就可以订阅此消息了。

这个思路其实把异常2也解决了,如果本地事务没有执行成功,RocketMQ回查业务,发现没有执行成功,就会发送RollBack确认消息,把消息进行删除。

同时还要注意的点是,RocketMQ不能保障消息的重复,所以在消费端一定要做幂等性处理。

除此之外,如果消费端发生消费失败,同时也需要做重试,如果重试多次,消息会进入死信队列,这个时候也需要进行特殊的处理。(一般就是把A已经处理完的业务进行回退)

image.png

如果本地事务执行了很多张表,那是不是我们要把那些表都要进行判断是否执行成功呢?这样是不是太麻烦了,而且和业务很耦合。

好的方案是设计一张Transaction表,将业务表和Transaction绑定在同一个本地事务中,如果扣款本地事务成功时,Transaction中应当已经记录该TransactionId的状态为「已完成」。当RocketMq事务回查时,只需要检查对应的TransactionId的状态是否是「已完成」就好,而不用关心具体的业务数据。

如果是银行业务,对数据要求性极高,一般A与B需要进行手动对账,手动补偿。

分布式事务使用案例

本案例简化整体流程,使用A系统向B系统转100块钱为例进行讲解。

案例中消息发送方是A系统,消费订阅方是B系统。

image.png

生产者案例代码:

image.png

代码解释如下:

Ø 启动线程池进行定时的任务回查

image.png

Ø 设置生产者事务回查监听器

image.png

Ø 开启本地事务,同时发送半事务消息

image.png

image.png

Ø 如果半事务消息失败,则整个事务回滚。

事务回查案例代码:

事务回查是非常有必要的,因为生产者在发送完半事务消息后不能立马确认事务的执行状态,所以事务回查有两个方法,一个是本地事务方法,一个是事务回查方法,目的都是为了确保事务的提交或者回滚。

image.png

image.png

消费者案例代码:

消费者使用的尽最大可能性确保成功消费(重试机制+死信队列特殊处理),所以B系统的处理比较简单,开启事务确保消费成功即可。

image.png

使用限制

Ø 事务消息不支持延时消息和批量消息。

Ø 事务回查的间隔时间:BrokerConfig. transactionCheckInterval  通过Broker的配置文件设置好。

Ø 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。、

Ø 事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionMsgTimeout 参数。

Ø 事务性消息可能不止一次被检查或消费。

Ø 事务性消息中用到了生产者群组,这种就是一种高可用机制,用来确保事务消息的可靠性。

Ø 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。

Ø 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。

什么是Request-Reply?

RocketMQ 中"Request-Reply"模式允许Producer发出消息后,以同步或异步的形式等Consumer消费并返回一个响应消息,达到类似RPC的调用过程。

RocketMQ从4.6.0版本开始支持这种模式。这种模式的流程如下图:

image.png

与RPC的不同

RocketMQ的这种调用方式跟dubbo之类的RPC调用非常类似,那为什么不使用dubbo?而要使用RocketMQ的这种RPC调用呢?原因如下:

Ø 基于RocketMQ来实现RPC可以快速搭建服务的消息总线,实现自己的RPC框架。

Ø 基于RocketMQ来实现RPC可以方便的收集调用的相关信息,能够实现调用链路追踪和分析。

Ø 基于RocketMQ来实现RPC既可以解耦两个系统之间的依赖,也可以实现跨网络区域实现系统间的同步调用,这里RocketMQ扮演的是一个类似于网关的角色。

Request-Reply的实现逻辑

image.png

在以上图中,可以看到,使用RPC模式还是三方:Producer、Broker、Consumer。

在Producer中进行消息的发送时,可以随便指定Topic,但是需要送入reply_to_client、correlation_id两个关键信息,reply_to_client记录着请求方的clientlD(用于Broker响应时确定client端)。而correlation_id是标识每次请求的,用于响应消息与请求的配对。而在进行发送消息时,也有两种模式,一种同步阻塞,另外一种异步非阻塞,这些跟之前普通消息的三种发送方式类似。

Broker端除了Producer发送时指定的Topic之外,还有一个Reply_Topic,这个以集群名_REPLY_TOPIC命名(不管RPC生产者主题有多少,这个在一个集群中只有一个),主要用于Consumer响应RPC消息的路由发现。

Consumer端除了消费监听之外,还需要加入一个消息的生产(用于RPC的响应消息),必须使用客户端提供的MessageUtil进行消息的包装,防止关键信息丢失从而导致Producer不能收到RPC消息响应。

代码案例

生产者向RequestTopic主题发送RPC消息,使用同步阻塞方式。发送方法也不是send方法,而是request方法(该方法会封装reply_to_client、correlation_id等关键信息),同时方法也提供了Message的返回值。

image.png

消费者接受主题消息发送RPC响应。收到响应后需要再做一次生产,使用工具类MessageUtil封装消息后进行响应消息发送。

image.png

RPC案例消息:

生产者打印如下,对比普通消息多了reply_to_client、correlation_id两个关键信息,reply_to_client记录着请求方的clientlD(用于Broker响应时确定client端)。而correlation_id是标识每次请求的,用于响应消息与请求的配对。

image.png

消费打如下,消费者同时需要响应RPC,对应的主题是DefaultCluster_REPLY_TOPIC。

image.png

在PRC方式中,生产者也可以使用异步方式发起,代码如下:

image.png

RocketMQ源码学习

# 消息队列之RocketMQ(三)源码学习之NameServer源码分析