RocketMQ-基础介绍

为什么要用消息队列?

  1. 削峰填谷,像12306这样的网站,平时处于流量低谷,而在节假日的时候流量猛增,只有扩容服务器,来增加服务端的吞吐量,如果使用消息队列,系统就可以平稳处理订票。
  2. 程序间解耦
  3. 异步处理
  4. 数据的最终一致性

来一张RocketMQ整体的组件图
在这里插入图片描述

一、RocketMQ术语

1.1Producer

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

1.2Producer Group

生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者组。

1.3Consumer

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

1.4Consumer Group

消费者组,和生产者类似,消费同一类消息的多个 Consumer 实例组成一个消费者组。

1.5Topic

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

1.6Message

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

1.7Tag

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

1.8Broker

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

1.9Queue

Topic和Queue是1对多的关系,一个Topic下可以包含多个Queue,主要用于负载均衡。发送消息时,用户只指定Topic,Producer会根据Topic的路由信息选择具体发到哪个Queue上。Consumer订阅消息时,会根据负载均衡策略决定订阅哪些Queue的消息。

1.10Offset

RocketMQ在存储消息时会为每个Topic下的每个Queue生成一个消息的索引文件,每个Queue都对应一个Offset记录当前Queue中消息条数。

1.11NameServer

NameServer可以看作是RocketMQ的注册中心,它管理两部分数据:集群的Topic-Queue的路由配置;Broker的实时配置信息。其它模块通过Nameserv提供的接口获取最新的Topic配置和路由信息。
Producer/Consumer :通过查询接口获取Topic对应的Broker的地址信息
Broker : 注册配置信息到NameServer, 实时更新Topic信息到NameServer

二、topic存储

在这里插入图片描述
这个图很好理解,消息先发到Topic,然后消费者去Topic拿消息。只是Topic在这里只是个概念,那它到底是怎么存储消息数据的呢,这里就要引入Broker概念。
Topic是一个逻辑上的概念,实际上Message是在每个Broker上以Queue的形式记录。
在这里插入图片描述
从上面的图片可以总结下几条结论。

  1. 消费者发送的Message会在Broker中的Queue队列中记录。
  2. 一个Topic的数据可能会存在多个Broker中。
  3. 一个Broker存在多个Queue。
  4. 单个的Queue也可能存储多个Topic的消息。

也就是说每个Topic在Broker上会划分成几个逻辑队列,每个逻辑队列保存一部分消息数据,但是保存的消息数据实际上不是真正的消息数据,而是指向commit log的消息索引。Queue不是真正存储Message的地方,真正存储Message的地方是在CommitLog。
在这里插入图片描述
左边的是CommitLog。这个是真正存储消息的地方。RocketMQ所有生产者的消息都是往这一个地方存的。
右边是ConsumeQueue。这是一个逻辑队列。和上文中Topic下的Queue是一一对应的。消费者是直接和ConsumeQueue打交道。ConsumeQueue记录了消费位点,这个消费位点关联了commitlog的位置。所以即使ConsumeQueue出问题,只要commitlog还在,消息就没丢,可以恢复出来。还可以通过修改消费位点来重放或跳过一些消息。

三、部署模型

在部署RocketMQ时,会部署两种角色。NameServer和Broker。
在这里插入图片描述
针对这张图做个说明:

  1. Product和consumer集群部署,是你开发的项目进行集群部署。
  2. Broker 集群部署是为了高可用,因为Broker是真正存储Message的地方,集群部署是为了避免一台挂掉,导致整个项目KO.

那Name Server是做什么用呢,它和Product、Consumer、Broker之前存在怎样的关系呢?
先简单概括Name Server的特点:

  1. Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
  2. 每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。
  3. Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息。
  4. Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息。

这里面最核心的是每个Broker与Name Server集群中的所有节点建立长连接这样做好处多多。

  1. 这样可以使Name Server之间可以没有任何关联,因为它们绑定的Broker是一致的。
  2. 作为Producer或者Consumer可以绑定任何一个Name Server 因为它们都是一样的。

四、Broker介绍

4.1Broker与Name Server关系

1)连接 单个Broker和所有Name Server保持长连接。
2)心跳

  • 心跳间隔:每隔30秒向所有NameServer发送心跳,心跳包含了自身的Topic配置信息。
  • 心跳超时:NameServer每隔10秒,扫描所有还存活的Broker连接,若某个连接2分钟内没有发送心跳数据,则断开连接。

3)断开:当Broker挂掉;NameServer会根据心跳超时主动关闭连接,一旦连接断开,会更新Topic与队列的对应关系,但不会通知生产者和消费者。

4.2负载均衡

一个Topic分布在多个Broker上,一个Broker可以配置多个Topic,它们是多对多的关系。
如果某个Topic消息量很大,应该给它多配置几个Queue,并且尽量多分布在不同Broker上,减轻某个Broker的压力。

4.3可用性

由于消息分布在各个Broker上,一旦某个Broker宕机,则该Broker上的消息读写都会受到影响。
所以RocketMQ提供了Master/Slave的结构,Salve定时从Master同步数据,如果Master宕机,则Slave提供消费服务,但是不能写入消息,此过程对应用透明,由RocketMQ内部解决。
有两个关键点:

  • 思考1一旦某个broker master宕机,生产者和消费者多久才能发现?
    受限于Rocketmq的网络连接机制,默认情况下最多需要30秒,因为消费者每隔30秒从nameserver获取所有topic的最新队列情况,这意味着某个broker如果宕机,客户端最多要30秒才能感知。
  • 思考2 master恢复恢复后,消息能否恢复。
    消费者得到Master宕机通知后,转向Slave消费,但是Slave不能保证Master的消息100%都同步过来了,因此会有少量的消息丢失。但是消息最终不会丢的,一旦Master恢复,未同步过去的消息会被消费掉。

4.4同步刷盘or异步刷盘

同步刷盘和异步刷盘指的是内存和磁盘的关系。RocketMQ的消息最终是是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。从客户端发送消息,一开始先写到内存,再写到磁盘上。两种策略:

  • 同步刷盘:当数据成功写到内存中之后立刻刷盘(同步),在保证消息写到磁盘也成功的前提下返回写成功状态。
  • 异步刷盘 :数据写入内存后,直接返回成功状态。异步将内存中的数据持久化到磁盘上。

同步刷盘和异步输盘的优劣:

  • 同步刷盘

  • 优点:保证了数据的可靠性,保证数据不会丢失。

  • 缺点:同步刷盘效率较低,因为需要内存将消息写入磁盘后才返回成功状态。

  • 异步刷盘

  • 优点:异步刷盘可以提高系统的吞吐量。因为它仅仅是写入内存成功后,就返回成功状态

  • 缺点:异步刷盘不能保证数据的可靠性。因为写入内存成功,但写入磁盘的时候因为某种原因写入失败,那就会丢失该条消息。

4.5同步复制or异步复制

同步复制和异步复制指的是Master节点和slave节点 的关系。如果一个Broker组有Master和Slave,消息需要从Master复制到Slave上,两种策略:

  • 同步复制: 当数据成功写到内存中Master节点之后立刻同步到Slave中,当Slave也成功的前提下返回写成功状态。
  • 异步复制: 当数据成功写到内存中Master节点之后,直接返回成功状态,异步将Master数据存入Slave节点。

同步复制和异步复制的优劣:

  • 同步复制 : 数据安全性高,性能低一点。
  • 异步复制 : 数据可能丢失,性能高一点。

建议线上采用 同步复制 + 异步刷盘;

4.6顺序消息

如果要保证顺序消费,那么他的核心点就是:生产者有序存储、消费者有序消费。
顺序消息分为全局顺序和局部顺序,如果一个topic只有一个queue,就是全局顺序,如果一个topic有多个queue,在各个queue内部保存顺序,就是局部顺序。

在实际开发有些场景中,少有场景需要消息完全按照先进先出,而是某些消息保证先进先出就可以了。就好比一个订单涉及 订单生成、订单支付、订单完成。我不用管其它的订单,只保证同样订单ID能保证这个顺序就可以了。
在这里插入图片描述

  1. 生产者有序存储
    同一订单按照先后顺序放到同一Queue,那么取消息的时候就可以保证先进先取出。生产者有序存储的例子:
producer.send(message, new MessageQueueSelector() {
    
    
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
    
    
        //3、arg的值其实就是下面传入 orderId
        String orderid = (String) arg;
        //4、因为订单是String类型,所以通过hashCode转成int类型
        int hashCode = orderid.hashCode();
        //5、因为hashCode可能为负数 所以取绝对值
        hashCode = Math.abs(hashCode);
        //6、保证同一个订单号 一定分配在同一个queue上
        long index = hashCode % mqs.size();
        return mqs.get((int) index);
    }
}, order.getOrderId(),50000);
  1. 消费者有序消费
    在一个消费者集群的情况下,消费者1先去Queue拿消息,它拿到了 订单生成,它拿完后,消费者2去queue拿到的是 订单支付。拿的顺序是没毛病了,但关键是先拿到不代表先消费完它。会存在虽然你消费者1先拿到订单生成,但由于网络等原因,消费者2比你真正的先消费消息。这是不是很尴尬了。订单付款还是可能会比订单生成更早消费的情况。那怎么办?Rocker采用的是分段锁,它不是锁整个Broker而是锁里面的单个Queue,因为只要锁单个Queue就可以保证局部顺序消费了。消费者1去Queue拿 订单生成,它就锁住了整个Queue,只有它消费完成并返回成功后,这个锁才会释放。然后下一个消费者去拿到 订单支付 同样锁住当前Queue,这样的一个过程来真正保证对同一个Queue能够真正意义上的顺序消费,而不仅仅是顺序取出。
public Consumer() throws MQClientException {
    
    
    consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
    consumer.setNamesrvAddr("IP:9876");
    //订阅主题和 标签( * 代表所有标签)下信息
    consumer.subscribe(JmsConfig.TOPIC, "*");
        //注册消费的监听 这里注意顺序消费为MessageListenerOrderly,内部实现了锁机制
    consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    
    
        //获取消息
        MessageExt msg = msgs.get(0);
        //消费者获取消息 这里只输出 不做后面逻辑处理
        log.info("Consumer-线程名称={},消息={}", Thread.currentThread().getName(), new String(msg.getBody()));
        return ConsumeOrderlyStatus.SUCCESS;
    });
    consumer.start();
}

消息类型对比:
在这里插入图片描述
发送方式对比:
在这里插入图片描述
顺序消息注意事项:
以上情况只是在正常情况下可以保证顺序消息,但发生故障后,就没办法保证消息的顺序了,当 Broker 宕机出现问题,此时生产端有可能会把顺序消息发送到不同的分区(还是发送失败?),这时会发生短暂消息顺序不一致的现象。想要做到严格的消息顺序,就要保证当集群出现故障后集群立马不可用,或者主题做成单分区,但这么做大大牺牲了集群的高可用,单分区也会让集群性能大大降低。

4.7分布式事务消息

4.7.1分布式事务场景

列子:假设 A 给 B 转 100块钱,同时它们不是同一个服务上。
目标:就是 A 减100块钱,B 加100块钱。
实际情况可能有四种:
1)就是A账户减100 (成功),B账户加100 (成功)
2)就是A账户减100(失败),B账户加100 (失败)
3)就是A账户减100(成功),B账户加100 (失败)
4)就是A账户减100 (失败),B账户加100 (成功)
这里 第1和第2 种情况是能够保证事务的一致性的,但是 第3和第4 是无法保证事务的一致性的。
那我们来看下RocketMQ是如何来保证事务的一致性的。
RocketMQ虽然之前也支持分布式事务,但并没有开源,等到RocketMQ 4.3才正式开源。

4.7.2基础概念

4.7.2.1最终一致性

RocketMQ是一种最终一致性的分布式事务,就是说它保证的是消息最终一致性,而不是像2PC、3PC、TCC那样强一致分布式事务,至于为什么说它是最终一致性事务下面会详细说明。

4.7.2.2Half Message(半消息)

是指暂不能被Consumer消费的消息。Producer 已经把消息成功发送到了 Broker 端,但此消息被标记为暂不能投递状态,处于该种状态下的消息称为半消息。需要 Producer
对消息的二次确认后,Consumer才能去消费它。

4.7.2.3消息回查

由于网络闪段,生产者应用重启等原因。导致 Producer 端一直没有对 Half Message(半消息) 进行 二次确认。这是Brock服务器会定时扫描长期处于半消息的消息,会主动询问 Producer端 该消息的最终状态(Commit或者Rollback),该消息即为 消息回查。

4.7.3交互流程

理解这张阿里官方的图,就能理解RocketMQ分布式事务的原理了。
在这里插入图片描述
我们来说明下上面这张图

1、A服务先发送个Half Message给Brock端,消息中携带 B服务 即将要+100元的信息。
2、当A服务知道Half Message发送成功后,那么开始第3步执行本地事务。
3、执行本地事务(会有三种情况1、执行成功。2、执行失败。3、网络等原因导致没有响应)
4.1)、如果本地事务成功,那么Product像Brock服务器发送Commit,这样B服务就可以消费该message。
4.2)、如果本地事务失败,那么Product像Brock服务器发送Rollback,那么就会直接删除上面这条半消息。
4.3)、如果因为网络等原因迟迟没有返回失败还是成功,那么会执行RocketMQ的回调接口,来进行事务的回查。

从上面流程可以得知 只有A服务本地事务执行成功 ,B服务才能消费该message。然后我们再来思考几个问题?
1、为什么要先发送Half Message(半消息)

1)可以先确认 Brock服务器是否正常 ,如果半消息都发送失败了 那说明Brock挂了。
2)可以通过半消息来回查事务,如果半消息发送成功后一直没有被二次确认,那么就会回查事务状态。

2、什么情况会回查

1)执行本地事务的时候,由于突然网络等原因一直没有返回执行事务的结果(commit或者rollback)导致最终返回UNKNOW,那么就会回查。
2) 本地事务执行成功后,返回Commit进行消息二次确认的时候的服务挂了,在重启服务那么这个时候在brock端,它还是个Half Message(半消息),这也会回查。

3、为什么说MQ是最终一致性事务
通过上面这幅图,我们可以看出,在上面举例事务不一致的两种情况中,永远不会发生

A账户减100 (失败),B账户加100 (成功)

因为:如果A服务本地事务都失败了,那B服务永远不会执行任何操作,因为消息压根就不会传到B服务。
那么 A账户减100 (成功),B账户加100 (失败) 会不会可能存在的。答案是会的,因为A服务只负责当我消息执行成功了,保证消息能够送达到B,至于B服务接到消息后最终执行结果A并不管。那B服务失败怎么办?

如果B最终执行失败,几乎可以断定就是代码有问题所以才引起的异常,因为消费端RocketMQ有重试机制,如果不是代码问题一般重试几次就能成功。

如果是代码的原因引起多次重试失败后,也没有关系,将该异常记录下来,由人工处理,人工兜底处理后,就可以让事务达到最终的一致性。

4.8消息类型对比

消息类型 优点 缺点 备注
普通消息(并发消息) 性能最好,单机TPS的级别为10万 消息生产和消费都是无序 大部分场景适用
分区有序消息 单分区中消息有序,单机发送TPS万级别 单点问题,如果Broker宕机,则会导致发送失败 大部分有序消息场景适用
全局有序消息 类似传统的Queue,全部消息有序,单机发送TPS千级别 单点问题,如果Broker宕机,则会导致发送失败 极少场景使用
延迟消息 不能根据任意时间延迟 非精确、延迟级别不多的场景
事务消息

五、Consumer消费者

5.1Consumer与Name Server关系

  1. 连接 :单个Consumer和一台NameServer保持长连接,如果该NameServer挂掉,消费者会自动连接下一个NameServer,直到有可用连接为止,并能自动重连。
  2. 心跳: 与NameServer没有心跳
  3. 轮询时间 : 默认情况下,消费者每隔30秒从NameServer获取所有Topic的最新队列情况,这意味着某个Broker如果宕机,客户端最多要30秒才能感知。

5.2 Consumer与Broker关系

连接 :单个消费者和该消费者关联的所有broker保持长连接。

5.3负载均衡

集群消费模式下,一个消费者集群多台机器共同消费一个Topic的多个队列,一个队列只会被一个消费者消费。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。

5.3重试

消费者和生产者的重试还是有区别的,主要有两点

  1. 默认重试次数:Product默认是2次,而Consumer默认是16次。
  2. 重试时间间隔:Product是立刻重试,而Consumer是有一定时间间隔的。它照1S,5S,10S,30S,1M,2M····2H进行重试。
  3. Product在异步情况不会进行重试,同步情况下在超时间之前,进行重试,而对于Consumer在广播情况下重试失效。

六、Producer生产者

6.1Producer与Name Server关系

  1. 连接单个Producer和一台NameServer保持长连接,如果该NameServer挂掉,生产者会自动连接下一个NameServer,直到有可用连接为止,并能自动重连。
  2. 轮询时间 默认情况下,生产者每隔30秒从NameServer获取所有Topic的最新队列情况,这意味着某个Broker如果宕机,生产者最多要30秒才能感知,在此期间,发往该broker的消息发送失败。
  3. 心跳 与nameserver没有心跳

6.2与broker关系

连接 单个生产者和该生产者关联的所有broker保持长连接。

6.3发送方式

RocketMQ 发送普通消息有三种实现方式:可靠同步发送、可靠异步发送、单向(Oneway)发送,主要区别:

发送方式 发送 TPS 发送结果反馈 可靠性
同步发送 不丢失
异步发送 不丢失
单向发送 最快 可能丢失

猜你喜欢

转载自blog.csdn.net/lihuayong/article/details/108547816