RocketMQ理论

MQ(Message Queue)

MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。指把要传输的数据(消息)放在队列中,用队列机制来实现消息传递——生产者产生消息并把消息放入队列,然后由消费者去处理。消费者可以到指定队列拉取消息,或者订阅相应的队列,由MQ服务端给其推送消息。

作用

消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。

解耦:一个业务需要多个模块共同实现,或者一条消息有多个系统需要对应处理,只需要主业务完成以后,发送一条MQ,其余模块消费MQ消息,即可实现业务,降低模块之间的耦合。

异步:主业务执行结束后从属业务通过MQ,异步执行,减低业务的响应时间,提高用户体验。

削峰:高并发情况下,业务异步处理,提供高峰期业务处理能力,避免系统瘫痪。

缺点

系统可用性降低。依赖服务也多,服务越容易挂掉。需要考虑MQ瘫痪的情况

系统复杂性提高。需要考虑消息丢失、消息重复消费、消息传递的顺序性

业务一致性。主业务和从属业务一致性的处理

组成

NameServer:RocketMQ的注册中心,它管理两部分数据:集群的Topic-Queue的路由配置;Broker的实时配置信息。其它模块通过Nameservr提供的接口获取最新的Topic配置和路由信息。

Broker:RocketMQ的核心模块,负责接收并存储消息。同时提供 Push/Pull 接口来将消息发送给Consumer。Consumer可选择从Master或者Slave读取数据。多个主/从组成Broker集群,集群内的Master节点之间不做数据交互。Borker会将自己的Topic配置信息实时同步到NameServer。

Producer:消息的生产者,通过 NameServer获取所有Broker的路由信息,根据负载均衡策略选择将消息发到哪个Broker,然后调用Broker接口提交消息。

Consumer:消息的消费者,通过 NameServer获取所有broker的路由信息后,向Broker发送Pull请求来获取消息数据。Consumer可以以两种模式启动,广播(Broadcast)和集群(Cluster),广播模式下,一条消息会发送给所有Consumer,集群模式下消息只会发送给一个Consumer。

Message:消息,使用MessageId唯一识别,用户在发送时可以设置messageKey,便于之后查询和跟踪。一个 Message 必须指定 Topic,相当于寄信的地址。Message 还有一个可选的 Tag 设置,以便消费端可以基于 Tag 进行过滤消息。也可以添加额外的键值对,例如你需要一个业务 key 来查找 Broker 上的消息,方便在开发过程中诊断问题。

Topic:用于将消息按主题做划分,Producer将消息发往指定的Topic,Consumer订阅该Topic就可以收到这条消息。Topic跟发送方和消费方都没有强关联关系,发送方可以同时往多个Topic投放消息,消费方也可以订阅多个Topic的消息。在RocketMQ中,Topic是一个上逻辑概念。消息存储不会按Topic分开。

Tag:标签可以被认为是对 Topic 进一步细化。一般在相同业务模块中通过引入标签来标记不同用途的消息。

 原理

 Broker会向所有的NameServer上注册自己的信息,而不是某一个,是每一个,全部!

 Consumer消费消息没有真正意义的push,都是pull,虽然有push类,但实际底层实现采用的是长轮询机制,即拉取方式!

发送消息

三种消息发送方式-同步、异步、单向

同步:消息完全发送完成之后才返回结果。适用于发送的消息很重要,但对响应时间不敏感时。

// send 默认同步发送
mqTemplate.send("topic-test:TagA", MessageBuilder.withPayload(paramMap).build());
// 转换并同步发送
mqTemplate.convertAndSend("topic-test:TagA", paramMap);
// 同步发送
mqTemplate.syncSend("topic-test:TagA", "send sync message !");
// 同步顺序消息
mqTemplate.syncSendOrderly("topic-test", "1,创建");
mqTemplate.syncSendOrderly("topic-test", "2,支付");
mqTemplate.syncSendOrderly("topic-test", "3,完成");

异步:消息发送后立刻返回,当消息完全完成发送后,会调用回调函数sendCallback来告知发送者本次发送是成功或者失败。适用于发送的消息很重要,且对响应时间很敏感时。

// 异步消息
mqTemplate.asyncSend("topic-test:TagA", resultMap, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        System.out.println("发送成功");
    }
    @Override
    public void onException(Throwable throwable) {
        System.out.println("发送失败");
    }
});

// 异步顺序消息
mqTemplate.asyncSendOrderly("topic-test:TagA", "1,创建", "1", new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        System.out.println("发送成功");
    }
    @Override
    public void onException(Throwable e) {
        System.out.println("发送失败");
    }
});
mqTemplate.asyncSendOrderly("topic-test:TagA", "2,支付", "2", new SendCallback() {...});
mqTemplate.asyncSendOrderly("topic-test:TagA", "3,完成", "3", new SendCallback() {...});

单向:发送完消息后立即返回,不会等待来自broker的ack来告知本次消息发送是否完全完成发送。适用于发送的消息不重要,提高吞吐量。

// 单向消息
mqTemplate.sendOneWay("topic-test", "send one-way message");
// 单向顺序
mqTemplate.sendOneWayOrderly("topic-test", "1,创建","1");
mqTemplate.sendOneWayOrderly("topic-test", "2,支付","2");
mqTemplate.sendOneWayOrderly("topic-test", "3,完成","3");

延时消息: 消息临时存储在一个内部主题中,不支持任意时间精度,支持特定的 level,例如定时 5s,10s,1m 等。

 步骤:

  • 修改消息Topic名称和队列信息
  • 转发消息到延迟主题的CosumeQueue中
  • 延迟服务消费SCHEDULE_TOPIC_XXXX消息
  • 将信息重新存储到CommitLog中
  • 将消息投递到目标Topic中
  • 消费者消费目标topic中的数据

 RocketMQ支持延迟消息,但是不支持秒级精度。默认支持18个level,如果设置的延迟level超过最大值,那么将会重置最最大值

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

Broker在启动时,内部会创建一个内部主题:SCHEDULE_TOPIC_XXXX,根据延迟level的个数,创建对应数量的队列,也就是说18个level对应了18个队列。这并不是说这个内部主题只会有18个队列,因为Broker通常是集群模式部署的,因此每个节点都有18个队列。

// 延时消息,level为5对应1m(一分钟)
Message<String> message = MessageBuilder.withPayload("send Delay message").build();
mqTemplate.syncSend("topic-test",message,1000,5);

 事务消息:

实现应用之间的解耦,同时保证数据最终一致性。主要分为事务消息发送和事务消息回查。

生产者: 

// 发送事务消息
Message<String> message = MessageBuilder.withPayload("测试事务消息").build();
TransactionSendResult sendResult = mqTemplate.sendMessageInTransaction("topic-transaction", message, null);
// 监听事务消息
// 一个项目有且仅有一个@RocketMQTransactionListener注解
@RocketMQTransactionListener
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {
    // 执行本地事务
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            // 业务处理
            System.out.println(msg.getPayload());
            return RocketMQLocalTransactionState.COMMIT;
        }catch (Exception e){
            System.out.println(e.getMessage());
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
    // 回查
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        return RocketMQLocalTransactionState.COMMIT;
    }
}

消费者:

@Component
@RocketMQMessageListener(topic = "topic-transaction", consumerGroup = "my-consumer_test-topic")
public class Consumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("接受到消息:"+message.toString());
    }
}

事务消息发送步骤如下:

  • 发送方将半事务消息发送至消息队列RocketMQ版服务端。

  • 消息队列RocketMQ版服务端将消息持久化成功之后,向发送方返回 Ack 确认消息已经发送成功,此时消息为半事务消息。

  • 发送方开始执行本地事务逻辑。

  • 发送方根据本地事务执行结果向服务端提交二次确认(Commit或是Rollback),服务端收到Commit状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息。

事务消息回查步骤如下:

  • 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。

  • 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。

  • 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行操作。

注意事项

  • 事务消息发送完成本地事务后,可在execute方法中返回以下三种状态:

    • TransactionStatus.CommitTransaction:提交事务,允许订阅方消费该消息。

    • TransactionStatus.RollbackTransaction:回滚事务,消息将被丢弃不允许消费。

    • TransactionStatus.Unknow:暂时无法判断状态,等待固定时间以后消息队列RocketMQ版服务端向发送方进行消息回查。

  • 可通过以下方式给每条消息设定第一次消息回查的最快时间:
Message message = new Message();
// 在消息属性中添加第一次消息回查的最快时间,单位秒。
// 例如,以下设置实际第一次回查时间为120秒~125秒之间
message.putUserProperties(PropertyKeyConst.CheckImmunityTimeInSeconds,"120");
// 以上方式只确定事务消息的第一次回查的最快时间,实际回查时间向后浮动0秒~5秒;如第一次回查后事务仍未提交,后续每隔5秒回查一次.

存储

MQ存储主要分为以下三类:

文件系统:RocketMQ/Kafka/RabbitMQ

关系型数据库DBActiveMQ(默认采用的KahaDB做消息存储)可选用JDBC的方式来做消息持久化

分布式KV存储:ZeroMQ

对比:

存储效率, 文件系统>分布式KV存储>关系型数据库DB

易于实现和快速集成,关系型数据库DB>分布式KV存储>文件系统,但是性能会下降很多

 

RocketMQ文件存储在rocketmq文件夹下的store文件夹内,里面包含commitlog、config、consumerqueue、index这四个文件夹和abort、checkpoint两个文件

  • commitlog:存储消息内容的日志数据文件,每个文件默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量;比如00000000000000000000代表了第一个文件,起始偏移量为0。

  • config:存储配置信息

  • consumerqueue:存储消息在CommitLog中的offset,一个ConsumeQueue文件对应topic下的一个队列。

  • index:存储消息队列的索引文件,根据消息ID来查找消息。

  • abort文件:标记mq是正常退出还是异常退出

  • checkpoint文件:文件检查点,存储是commitlog、consumerqueue、index文件的最后刷盘时间

 刷盘:消息持久化

同步刷盘:

消息写入系统的PageCache后,立刻通知刷盘线程刷盘,消息真正持久化至磁盘后RocketMQ的Broker端才会真正返回给Producer端一个成功的ACK响应。同步刷盘保障消息可靠性,但是性能上会有较大影响,一般适用于金融业务应用该模式较多。

异步刷盘:

只要消息写入PageCache即将成功的ACK返回给Producer端。当内存里的消息量积累到一定程度时,触发刷盘操作,采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ的性能和吞吐量。

复制方式优点缺点适应场景
同步刷盘 保障消息可靠性 吞吐率相对于异步刷盘要低 消息可靠性要求较高的场景
异步刷盘 系统的吞吐量提高 系统断电等异常时会有部分丢失 对应吞吐量要求较高的场景

主从复制:

如果一个broker组有Master和Slave,消息需要从Master复制到Slave上,有同步和异步两种复制方式。

同步复制

也叫 “同步双写”,只有Master和Slave都将消息写入成功才返回返回成功状态。

异步复制:

消息写入Master节点之后就返回写入成功 。

复制方式优点缺点适应场景
同步复制 slave保证了与master一致的数据副本,如果master宕机,数据依然在slave中找到其数据和master的数据一致 由于需要slave确认效率上会有一定的损失 数据可靠性要求很高的场景
异步复制 无需等待slave确认消息是否存储成功效率上要高于同步复制 如果master宕机,由于数据同步有延迟导致slave和master存在一定程度的数据不一致问题 数据可靠性要求一般的场景

集群消费/广播消费

集群消费:

一个消费者分组中的消费者平均分摊消费消息,一个消息只能被一个消费者消费,不保证每一次失败重投的消息路由到同一台机器上。

@RocketMQMessageListener(topic = "topic-test", consumerGroup = "my-consumer", messageModel = MessageModel.CLUSTERING)

广播消费:

一个消息可以被一个消费者分组中的所有消费者消费。

@RocketMQMessageListener(topic = "topic-test", consumerGroup = "my-consumer", messageModel = MessageModel.BROADCASTING)

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

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

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

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

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

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

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

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

顺序消息/顺序消费

rocketmq单机情况下,创建一个topic,默认会将该topic分成4个queue进行存放。消息发送默认是会采用轮询的方式发送到不同的queue;消费端消费的时候,默认会分配多个queue,异步拉取多个queue消费消息。

例如:默认情况下,一个Topic默认创建 4个queue;当只有1个消费者时,对应4个queue(1 : 4),2个消费者时则平分4个queue(2 : 2)以此类推。

全局顺序:

一个Topic内所有的消息都发布到同一个queue,按照先进先出的顺序进行发布和消费;如:一个Topic只有一个queue,那么消费实例也只有一个,以此来保证全局顺序。

分区顺序:

对于指定的一个Topic,所有消息根据sharding key进行(queue)分区推送,同一个queue内的消息按照严格的FIFO顺序进行发布和消费Sharding key是顺序消息中用来区分不同分区的关键字段。

/**
 * Same to {@link #syncSend(String, Message)} with send orderly with hashKey by specified.
 *
 * @param destination formats: `topicName:tags`
 * @param message {@link org.springframework.messaging.Message}
 * @param hashKey use this key to select queue. for example: orderId, productId ...
 * @return {@link SendResult}
 */
public SendResult syncSendOrderly(String destination, Message<?> message, String hashKey) {
    return syncSendOrderly(destination, message, hashKey, producer.getSendMsgTimeout());
}
//生产者
for(int i=0;i<100;i++){
    Message<String> message = MessageBuilder.withPayload("send Delay message - "+ i).build();
    mqTemplate.syncSendOrderly("topic-test",message,"1");
}

//消费者
@Component
@RocketMQMessageListener(topic = "topic-test", consumerGroup = "my-consumer_test-topic", consumeMode= ConsumeMode.ORDERLY)
public class Consumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println("接受到消息:"+message.toString());
    }
}

 全局顺序消息与分区顺序消息对比:

消息类型对比

Topic的消息类型是否支持事务消息是否支持定时和延时消息性能
无序消息(普通、事务、定时和延时消息) 最高
分区顺序消息
全局顺序消息 一般

 

 

 

 

 

发送方式对比

消息类型是否支持可靠同步发送是否支持可靠异步发送是否支持Oneway发送
无序消息(普通、事务、定时和延时消息)
分区顺序消息
全局顺序消息

 

 

 

 

 

 

  • 同一条消息不可以既是顺序消息,又是定时消息和事务消息,三者是互斥关系,不能叠加在一起使用。

  • 顺序消息只支持可靠同步发送方式,不支持异步发送方式,否则将无法严格保证顺序。

  • 顺序消息暂时仅支持集群消费模式,不支持广播消费模式。

重复消费 

  • 消费失败后返回RECONSUME_LATER进行容错重试。

  • Producer刷盘后返回ACK时网络异常,此时producer会进行重发。

  • Consumer消费完返回ACK时网络异常,会触发一次重新消费。

  • Consumer消费完返回ACK时宕机,重启后再次消费。

解决方案: 

  • 消费端处理消息的业务逻辑保持幂等性

  • 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现

消息重试

生产者Producer端重试:默认最大重试次数为2,立刻重试,可能会导致消息重复;异步情况重试失效(有待验证)

RocketMQProperties配置文件中
/**
 * 在同步模式下声明发送失败之前在内部执行的最大重试次数.
 * 这可能会导致消息重复,这由应用程序开发人员来解决.
 */
private int retryTimesWhenSendFailed = 2;

/**
 * <p> 在异步模式下声明发送失败之前在内部执行的最大重试次数. </p>
 * 这可能会导致消息重复,这由应用程序开发人员来解决.
 */
private int retryTimesWhenSendAsyncFailed = 2;

消费者Consumer端重试

  • 顺序消息的重试:顺序消息消费失败后,Consumer会自动不断地进行消息重试(每次间隔时间为1秒),这时会出现消息消费被阻塞的情况。

  • 无序消息的重试:无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,可以通过设置返回状态达到消息重试的结果。无序消息的重试只对集群消费方式生效

重试次数:

第几次重试与上次重试的间隔时间第几次重试与上次重试的间隔时间
1 10秒 9 7分钟
2 30秒 10 8分钟
3 1分钟 11 9分钟
4 2分钟 12 10分钟
5 3分钟 13 20分钟
6 4分钟 14 30分钟
7 5分钟 15 1小时
8 6分钟 16 2小时

如果消息重试16次后仍然失败,消息将不再投递进入死信队列

负载均衡

生产者负载均衡

默认策略:从MessageQueue列表中随机选择一个,实现过程是通过自增随机数对列表大小取余获取位置信息,但获得的MessageQueue所在的集群不能是上次的失败集群。
超时容忍策略:先随机选择一个MessageQueue,如果因为超时等异常发送失败,会优先选择该broker集群下其他的messeagequeue进行发送;如果没有找到则从之前发送失败broker集群中选择一个MessageQueue进行发送;如果还没有找到则使用默认策略。

消费者负载均衡:

Consumer在拉取消息之前需要对TopicMessage进行负载操作,负载操作由一个定时器来完成单位,定时间隔默认20s。
AllocateMessageQueueAveragely算法(默认):平均负载策略

AllocateMessageQueueAveragelyByCircle算法:环形平均分配,每个消费者依次消费一个MessageQueue。

AllocateMachineRoomNearby算法:机房就近原则

AllocateMessageQueueByMachineRoom算法:机房负载策略,指定 Consumer只负载处在指定的机房内的MessageQueue

AllocateMessageQueueByConfig算法:用户自定义配置,自己指定需要监听的MessageQueue

AllocateMessageQueueConsistentHash算法:一致性哈希策略,距离MessageQueue顺时针方向最近的那个Consumer点,这个就是MessageQeueu最终归属的那个Consumer。

 容错机制

 

posted @ 2021-01-14 17:09  柒月丶  阅读(602)  评论(1编辑  收藏  举报