RocketMQ二 之深入消息发送/深入消息消费

深入消息发送 

 消息生产者流程

  • 消息发送的主要流程:验证消息、查找路由、消息发送(包含异常机制) 
  • 验证消息:主要是要求主题名称、消息体不能为空、消息长度不能等于0,且不能超过消息的最大的长度4M(生产者对象中配置maxMessageSize=1024*1024*4)
  • 查找路由:客户端(生产者)会缓存topic 路由信息(如果是第一次发送消息,本地没有缓存,查询NameServer 尝试获取),路由信息主要包含了消息队列(queue 相关信息),
  • 消息发送:选择消息队列,发送消息,发送成功则返回。选择消息队列两种方式(一般有两种,这里不做详细讲解,后续做详细讲解) 

 深入消息模式

 拉模式


代码上使用

  • 1)获取MessageQueues 并遍历(一个Topic 包括多个MessageQueue),如果是特殊情况,也可以选择指定的MessageQueue 来读取消息
  • 2)维护Offsetstore,从一个MessageQueue 里拉取消息时,要传入Offset 参数,随着不断的读取消息,Offset 会不断增长。这个时候就需要用户把Offset存储起来,根据实际的情况存入内存、写入磁盘或者数据库中。
  • 3)根据不同的消息状态做不同的处理。
  • 拉取消息的请求后,会返回:FOUND(获取到消息),NO_MATCHED_MSG(没有匹配的消息),NO_NEW_MSG(没有新消息),OFFSET_ILLEGAL(非法偏移量)四种状态,其中必要重要的是FOUND(获取到消息)和NO_NEW_MSG(没有新消息)。
  • 总结:这种模式下用户需要自己处理Queue,并且自己保存偏移量,所以这种方式太过灵活,往往我们业务的关注重点不在内部消息的处理上,所以一般情况下我们会使用推模式,

 推模式


代码上使用

  • Push 方式是Server 端接收到消息后,主动把消息推给Client 端,实时性高,但是使用Push 方式主动推送也存在一些问题:比如加大Server 端的工作量,其次Client 端的处理能力各不相同,如果Client 不能及时处理Server 推过来的消息,会造成各种潜在的问题。

长轮询

  • 所以RocketMQ 使用“长轮询”的方式来解决以上问题,核心思想是这样,客户端还是拉取消息,Broker 端HOLD 住客户端发过来的请求一小段时间,在这个时间内(5s)有新消息达到,就利用现有的连接立刻返回消息给Consunmer。“长轮询”的主动权还是掌握在Consumer 手中,Broker 即使有大量消息积压,也不会主动推送给Consumer。因为长轮询方式的有局限性,是在HOLD 住Comsumer 请求的时候需要占用资源,所以它适合在消息队列这种客户端连接数可控的场景中。 

流量控制 

  • Push 模式基于拉取,消费者会判断获取但还未处理的消息个数、消息总大小、Offset 的跨度3 个维度来控制,如果任一值超过设定的大小就隔一段时间再拉取消息,从而达到流量控制的目的。
  • 两种情况会限流,限流的做法是放弃本次拉取消息的动作,并且这个队列的下一次拉取任务将在50 毫秒后才加入到拉取任务队列。
  • 1:当前的ProcessQueue(一个主题有多个队列,每一个队列会对应有一个ProcessQueue 来处理消息)正在处理的消息数量>1000
  • 2:队列中最大最小偏移量差距>2000,这个是为了避免一条消息堵塞,消息进度无法向前推进,可能造成大量消息重复消费。 

消息队列负载与重新分布机制 

  •  在集群消费模式中,往往会有很多个消费者,对应消费一个主题(topic),一个主题中有很多个消费者队列(queue),我们要考虑的问题是,集群内多个消费者是如何负载主题下的多个消费者队列,并且如果有新的消费者加入是,消息队列又会如何重新分布。
  • 从源码的角度上看,RocketMQ 消息队列重新分布是由RebalanceService 线程来实现的,一个MQClientInstance 持有一个RebalanceService 实现,并且随着MQClientInstance 的启动而启动。
  • 备注:(MQClientInstance 是生产者和消费者中最大的一个实例,作为生产者或者消费者引用RocketMQ 客户端,在一个JVM 中所有消费者,生产者都持有同一个MQClientInstance,MQClientInstance 只会启动一次)

RocketMQ 默认提供5 中分配算法

如果有8 个消息队列(q1,q2,q3,q4,q5,q6,q7,q8),有3 个消费者(c1,c2,c3)

1) 平均分配(AllocateMessageQueueAveragely)
c1:q1,q2,q3
c2:q4,q5,q6
c3:q7,q8,


2) 平均轮询分配(AllocateMessageQueueAveragelyByCircle)
c1:q1,q4,q7
c2:q2,q5,q8
c3:q3,q6


3) 一直性Hash(AllocateMessageQueueConsistentHash)
不推荐使用,因为消息队列负载均衡信息不容易跟踪


4) 根据配置(AllocateMessageQueueByConfig)
为每一个消费者配置固定的消费队列


5) 根据Broker 部署机房名(AllocateMessageQueueByMachineRoom)
对每一个消费者负载不同Broker 上的队列

  • 一般尽量使用“平均分配”“平均轮询分配”,因为分配算法比较直观。无论哪种算法,遵循的原则是一个消费者可以分配多个消息队列,同一个消息队列只会分配一个消费者,所以如果消费者个数大于消息队列数量,则有些消费者无法消费消息。
  • RebalanceService 每隔20S 进行一次队列负载
  • 每次进行队列重新负载时会查询出当前所有的消费者,并且对消息队列、消费者列表进行排序。因为在一个JVM 中只会有一个pullRequestQueue 对象。具体可见源码中PullMessageService

消息确认(ACK)

  • PushConsumer 为了保证消息肯定消费成功,只有使用方明确表示消费成功,RocketMQ 才会认为消息消费成功。中途断电,抛出异常等都不会认为成功——即都会重新投递。
  • 业务实现消费回调的时候,当且仅当此回调函数返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS,RocketMQ 才会认为这批消息(默认是1 条)是消费完成的 

todo

消息ACK 机制

RocketMQ 是以consumer group+queue 为单位是管理消费进度的,以一个consumer offset 标记这个这个消费组在这条queue 上的消费进度。
如果某已存在的消费组出现了新消费实例的时候,依靠这个组的消费进度,就可以判断第一次是从哪里开始拉取的。
每次消息成功后,本地的消费进度会被更新,然后由定时器定时同步到broker,以此持久化消费进度。
但是每次记录消费进度的时候,只会把一批消息中最小的offset 值为消费进度值

todo

消息进度存储

广播模式

同一个消费组的所有消费者都需要消费主题下的所有消息,因为消费者的行为都是独立的,互不影响,固消息进度需要独立存储,所以这种模式下消息进度存储在消费者本地。

todo

集群模式

集群模式消息进度存储文件存放在服务器Broker 上。

顺序消息

顺序消息(FIFO 消息)是消息队列RocketMQ 提供的一种严格按照顺序来发布和消费的消息。顺序发布和顺序消费是指对于指定的一个Topic,生产者按照一定的先后顺序发布消息;消费者按照既定的先后顺序订阅消息,即先发布的消息一定会先被客户端接收到。顺序消息分为全局顺序消息分区顺序消息

全局顺序消息

RocketMQ 在默认情况下不保证顺序,要保证全局顺序,需要把Topic 的读写队列数设置为1,然后生产者和消费者的并发设置也是1。所以这样的话高并发,高吞吐量的功能完全用不上。

todo

适用场景

适用于性能要求不高,所有的消息严格按照FIFO( 全称First in, First out,先进先出) 原则来发布和消费的场景。

示例

要确保全局顺序消息,需要先把Topic 的读写队列数设置为1,然后生产者和消费者的并发设置也是1。
mqadmin update Topic -t AllOrder -c DefaultCluster -r 1 -w 1 -n 127.0.0.1:9876 在证券处理中,以人民币兑换美元为Topic,在价格相同的情况下,先出价者优先处理,则可以按照FIFO 的方式发布和消费全局顺序消息。

分区顺序消息

对于指定的一个Topic,所有消息根据Sharding Key 进行区块分区。同一个分区内的消息按照严格的FIFO 顺序进行发布和消费。Sharding Key 是顺序消息中用来区分不同分区的关键字段,和普通消息的Key 是完全不同的概念。

适用场景

适用于性能要求高,以 Sharding Key 作为分区字段,在同一个区块中严格地按照 FIFO 原则进行消息发布和消费的场景。

示例

  • 用户注册需要发送发验证码,以用户 ID 作为 Sharding Key,那么同一个用户发送的消息都会按照发布的先后顺序来消费。
  • 电商的订单创建,以订单 ID 作为 Sharding Key,那么同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息都会按照发布的先后顺序来消费。
  • 电商系统均使用分区顺序消息,既保证业务的顺序,同时又能保证业务的高性能。

全局顺序与分区顺序对比

在控制台创建顺序消息使用的不同类型 Topic 对比如下。

表 1. 消息类型对比
Topic 的消息类型 是否支持事务消息 是否支持定时/延时消息 性能
无序消息(普通、事务、定时/延时消息) 最高
分区顺序消息
全局顺序消息 一般
表 2. 发送方式对比
消息类型 是否支持可靠同步发送 是否支持可靠异步发送 是否支持 Oneway 发送
无序消息(普通、事务、定时/延时消息)
分区顺序消息
全局顺序消息

注意事项

使用顺序消息时,请注意以下几点:

  • 顺序消息暂不支持广播模式。
  • 建议同一个 Group ID 只对应一种类型的 Topic,即不同时用于顺序消息和无序消息的收发。
  • 顺序消息不支持异步发送方式,否则将无法严格保证顺序。
  • 对于全局顺序消息,建议至少创建 2 个 SDK 实例。同时运行多个实例,是为了防止工作实例意外退出而导致业务中断。当工作实例退出时,其他实例可以立即接手工作,不会导致业务中断,实际工作的只会有一个实例。

延时消息/定时消息

概念介绍

  • 定时消息:Producer 将消息发送到消息队列 RocketMQ 版服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息。
  • 延时消息:Producer 将消息发送到消息队列 RocketMQ 版服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

定时消息与延时消息在代码配置上存在一些差异,但是最终达到的效果相同:消息在发送到消息队列 RocketMQ 版服务端后并不会立马投递,而是根据消息中的属性延迟固定时间后才投递给消费者。

适用场景

定时消息和延时消息适用于以下一些场景:

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

使用方式

定时消息和延时消息的使用在代码编写上存在略微的区别:

  • 发送定时消息需要明确指定消息发送时间点之后的某一时间点作为消息投递的时间点。
  • 发送延时消息时需要设定一个延时时间长度,消息将从当前发送时间点开始延迟固定时间之后才开始投递。
  • Apache RocketMQ 目前只支持固定精度的定时消息,因为如果要支持任意的时间精度,在Broker 层面,必须要做消息排序,如果再涉及到持久化,那么消息排序要不可避免的产生巨大性能开销。发送延时消息时需要设定一个延时时间长度,消息将从当前发送时间点开始延迟固定时间之后才开始投递。延迟消息是根据延迟队列的level 来的,延迟队列默认是msg.setDelayTimeLevel(5) 代表延迟一分钟"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"是这18 个等级(秒(s)、分(m)、小时(h)),level 为1,表示延迟1 秒后消费,level 为5 表示延迟1 分钟后消费,level 为18 表示延迟2 个小时消费。生产消息跟普通的生产消息类似,只需要在消息上设置延迟队列的level 即可。消费消息跟普通的消费消息一致。

注意事项

  • 定时和延时消息的 msg.setStartDeliverTime 参数需要设置成当前时间戳之后的某个时刻(单位毫秒)。如果被设置成当前时间戳之前的某个时刻,消息将立刻投递给消费者。

  • 定时和延时消息的 msg.setStartDeliverTime 参数可设置 40 天内的任何时刻(单位毫秒),超过 40 天消息发送将失败。
  • StartDeliverTime 是服务端开始向消费端投递的时间。 如果消费者当前有消息堆积,那么定时和延时消息会排在堆积消息后面,将不能严格按照配置的时间进行投递。
  • 由于客户端和服务端可能存在时间差,消息的实际投递时间与客户端设置的投递时间之间可能存在偏差。
  • 设置定时和延时消息的投递时间后,依然受 3 天的消息保存时长限制。

    例如,设置定时消息 5 天后才能被消费,如果第 5 天后一直没被消费,那么这条消息将在第 8 天被删除。

消息过滤

概念介绍

RocketMQ 分布式消息队列的消息过滤方式有别于其它MQ 中间件,是可以实现服务端的过滤。

描述消息队列 RocketMQ 版的消费者如何根据 Tag 在消息队列 RocketMQ 版服务端完成消息过滤,以确保消费者最终只消费到其关注的消息类型。

  • Tag,即消息标签,用于对某个 Topic 下的消息进行分类。消息队列 RocketMQ 版的生产者在发送消息时,已经指定消息的 Tag,消费者需根据已经指定的 Tag 来进行订阅。

场景示例

以下图电商交易场景为例,从客户下单到收到商品这一过程会生产一系列消息,以以下消息为例:

  • 订单创建消息
  • 支付消息
  • 物流消息

这些消息会发送到 Trade_Topic Topic 中,被各个不同的系统所订阅,以以下系统为例:

  • 支付系统:只需订阅支付消息
  • 物流系统:只需订阅物流消息
  • 交易成功率分析系统:需订阅订单和支付消息
  • 实时计算系统:需要订阅所有和交易相关的消息

过滤示意图如下所示。RocketMQ二 之深入消息发送/深入消息消费_第1张图片

示例代码

  • 发送消息

    发送消息时,每条消息必须指明 Tag:

        Message msg = new Message("MQ_TOPIC","TagA","Hello MQ".getBytes());                
  • 订阅所有 Tag

    消费者如需订阅某 Topic 下所有类型的消息,Tag 用符号 * 表示:

        consumer.subscribe("MQ_TOPIC", "*", new MessageListener() {
            public Action consume(Message message, ConsumeContext context) {
                System.out.println(message.getMsgID());
                return Action.CommitMessage;
            }
        });                
  • 订阅单个 Tag

    消费者如需订阅某 Topic 下某一种类型的消息,请明确标明 Tag:

        consumer.subscribe("MQ_TOPIC", "TagA", new MessageListener() {
            public Action consume(Message message, ConsumeContext context) {
                System.out.println(message.getMsgID());
                return Action.CommitMessage;
            }
        });                
  • 订阅多个 Tag

    消费者如需订阅某 Topic 下多种类型的消息,请在多个 Tag 之间用 || 分隔:

        consumer.subscribe("MQ_TOPIC", "TagA||TagB", new MessageListener() {
            public Action consume(Message message, ConsumeContext context) {
                System.out.println(message.getMsgID());
                return Action.CommitMessage;
            }
        });                

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(RocketMQ二 之深入消息发送/深入消息消费)