【RocketMq 系列】springboot RocketMq 入门教程

3,453 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情

Apache RocketMQ 自诞生以来,因其架构简单、业务功能丰富、具备极强可扩展性等特点被众多企业开发者以及云厂商广泛采用。历经十余年的大规模场景打磨,RocketMQ 已经成为业内共识的金融级可靠业务消息首选方案,被广泛应用于互联网、大数据、移动互联网、物联网等领域的业务场景。

RocketMq 版本

MetaQ 1.x是RocketMQ前身的第一个版本,本质上把Kafka做了一次java版本的重(Kafka是sacla)。
MetaQ 2.x,主要是对存储部分进行了优化,因为kafka的数据存储,它的partition是一个全量的复制,在阿里、在淘宝的这种海量交易。Kafka这种机制的横向拓展是非常不好的。2012年阿里同时把MetaQ 2.0从阿里内部开源出来,取名RocketMQ,同时为了命名上的规范(版本上延续),所以这个就是RocketMQ3.0。 2017年RocketMQ从Apache顶级项目毕业。现在RocketMQ主要维护的是4.x的版本,也是大家使用得最多的版本,RocketMQ5的版本也已经出来了,主要的方向是Cloud Native(云原生)。

部署架构

Apache RocketMQ 部署架构上主要分为四部分:

  • 生产者 Producer 发布消息的角色。Producer通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败和重试。

  • 消费者 Consumer 消息消费的角色。

    • 支持以推(push),拉(pull)两种模式对消息进行消费。
    • 同时也支持集群方式和广播方式的消费
    • 提供实时消息订阅机制,可以满足大多数用户的需求。
  • 名字服务器 NameServer NameServer是 一个简单的 Topic 路由注册中心,支持 Topic、Broker 的动态注册与发现。 主要包括两个功能:

    1. Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;
    2. 路由信息管理,每个NameServer将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息。Producer和Consumer通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。

    NameServer通常会有多个实例部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,客户端仍然可以向其它NameServer获取路由信息。

  • 代理服务器 Broker Broker主要负责消息的存储、投递和查询以及服务高可用保证。

    NameServer几乎无状态节点,因此可集群部署,节点之间无任何信息同步。Broker部署相对复杂。

    在 Master-Slave 架构中,Broker 分为 Master 与 Slave。一个Master可以对应多个Slave,但是一个Slave只能对应一个Master。Master 与 Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。

运转流程

  1. 每个 Broker 与 NameServer 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 NameServer。NameServer 会定期30s向Broker发送心跳判断Broker 检测Broker 是否存活,如果超过120s未收到响应,则将Broker 从注册表移除

  2. Producer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取Topic路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master 发送心跳。Producer 完全无状态。

  3. Consumer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave发送心跳。Consumer 既可以从 Master 订阅消息,也可以从Slave订阅消息。

RocketMQ集群工作流程

1. 启动NameServer 启动NameServer。NameServer启动后监听端口,等待Broker、Producer、Consumer连接,相当于一个路由控制中心。

2. 启动 Broker 启动 Broker。与所有 NameServer 保持长连接,定时发送心跳包。心跳包中包含当前 Broker 信息以及存储所有 Topic 信息。注册成功后,NameServer 集群中就有 Topic跟Broker 的映射关系。

3. 创建 Topic 创建 Topic 时需要指定该 Topic 要存储在哪些 Broker 上,也可以在发送消息时自动创建Topic。

4. 生产者发送消息 生产者发送消息。启动时先跟 NameServer 集群中的其中一台建立长连接,并从 NameServer 中获取当前发送的 Topic存在于哪些 Broker 上,轮询从队列列表中选择一个队列,然后与队列所在的 Broker建立长连接从而向 Broker发消息。

5. 消费者接受消息 消费者接受消息。跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,然后开始消费消息。

RocketMQ的概念模型

分组(Group)

  • 生产者组:同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。 发送普通消息的时候,仅标识使用,并无特别用处。主要作用用于事务消息:(事务消息中如果某条发送某条消息的producer-A宕机,使得事务消息一直处于PREPARED状态并超时,则broker会回查同一个group的其它producer,确认这条消息应该commit还是rollback)

  • 消费者组:同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
    消费进度以Consumer Group为粒度管理,不同Consumer Group之间消费进度彼此不受影响,即消息A被Consumer Group1消费过,也会再给Consumer Group2消费。

主题(Topic)

表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。无论消息生产还是消费,都需要指定Topic。 一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息

标签(Tag)

Topic 与 Tag 都是业务上用来归类的标识,区别在于 Topic 是一级分类,而 Tag 可以理解为是二级分类。使用 Tag 可以实现对 Topic 中的消息进行过滤。
RocketMQ支持给在发送的时候给topic打tag,同一个topic的消息虽然逻辑管理是一样的。但是消费topic1的时候,如果你消费订阅的时候指定的是tagA,那么tagB的消息将不会投递。

Topic和Tag区别:

  • Topic:消息主题,通过 Topic 对不同的业务消息进行分类。
  • Tag:消息标签,用来进一步区分某个 Topic 下的消息分类,消息从生产者发出即带上的属性。

Topic和Tag关系图: Topic和Tag关系图

什么时候该用 Topic,什么时候该用 Tag?

可以从以下几个方面进行判断:

  • 消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型使用不同的 Topic,无法通过 Tag 进行区分。

  • 业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的 Topic 进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用 Tag 进行区分。

  • 消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市 24 小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的 Topic 进行区分。

  • 消息量级是否相当:有些业务消息虽然量小但是实时性要求高,如果跟某些万亿量级的消息使用同一个 Topic,则有可能会因为过长的等待时间而“饿死”,此时需要将不同量级的消息进行拆分,使用不同的 Topic。

总的来说,针对消息分类,您可以选择创建多个 Topic,或者在同一个 Topic 下创建多个 Tag。但通常情况下,不同的 Topic 之间的消息没有必然的联系,而 Tag 则用来区分同一个 Topic 下相互关联的消息,例如全集和子集的关系、流程先后的关系。

Keys

Apache RocketMQ 每个消息可以在业务层面的设置唯一标识码 keys 字段,方便将来定位消息丢失问题。 Broker 端会为每个消息创建索引(哈希索引),应用可以通过 topic、key 来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证 key 尽可能唯一,这样可以避免潜在的哈希冲突。

RocketMQ系统保留的属性Key集合有如下,需要在使用过程中避免:

TRACE_ON、MSG_REGION、KEYS、TAGS、DELAY、RETRY_TOPIC、REAL_TOPIC、REAL_QID、TRAN_MSG、PGROUP、MIN_OFFSET、MAX_OFFSET、BUYER_ID、ORIGIN_MESSAGE_ID、TRANSFER_FLAG、CORRECTION_FLAG、MQ2_FLAG、RECONSUME_TIME、UNIQ_KEY、MAX_RECONSUME_TIMES、CONSUME_START_TIME、POP_CK、POP_CK_OFFSET、1ST_POP_TIME、TRAN_PREPARED_QUEUE_OFFSET、DUP_INFO、EXTEND_UNIQ_INFO、INSTANCE_ID、CORRELATION_ID、REPLY_TO_CLIENT、TTL、ARRIVE_TIME、PUSH_REPLY_TIME、CLUSTER、MSG_TYPE、INNER_MULTI_QUEUE_OFFSET、_BORNHOST

偏移量(Offset)

RocketMQ中,有很多offset的概念。一般我们只关心暴露到客户端的offset。不指定的话,就是指Message Queue下面的offset。 Message queue是无限长的数组。一条消息进来下标就会加1,而这个数组的下标就是offset,Message queue中的max offset表示消息的最大offset。

Consumer offset可以理解为标记Consumer Group在一条逻辑Message Queue上,消息消费到哪里即消费进度。但从源码上看,这个数值是消费过的最新消费的消息offset+1,即实际上表示的是下次拉取的offset位置。

消息队列(Message Queue)

为了支持高并发和水平扩展,需要对 Topic 进行分区,在 RocketMQ 中这被称为队列,一个 Topic 可能有多个队列,并且可能分布在不同的 Broker 上。
Topic和队列关系

消息队列是消息的物理管理单位。一个Topic将有若干个队列。若一个Topic创建在不同的Broker,则不同的broker上都有若干队列,消息将物理地存储落在不同Broker结点上,具有水平扩展的能力。

无论生产者还是消费者,实际的生产和消费都是针对队列级别。例如Producer发送消息的时候,会预先选择(默认轮询)好该Topic下面的某一条队列发送;Consumer消费的时候也会负载均衡地分配若干个队列,只拉取对应队列的消息。 每一条message queue均对应一个文件,这个文件存储了实际消息的索引信息。并且即使文件被删除,也能通过实际纯粹的消息文件(commit log)恢复回来。

一般来说一条消息,如果没有重复发送(比如因为服务端没有响应而进行重试),则只会存在在 Topic 的其中一个队列中,消息在队列中按照先进先出的原则存储,每条消息会有自己的位点,每个队列会统计当前消息的总条数,这个称为最大位点 MaxOffset;队列的起始位置对应的位置叫做起始位点 MinOffset。队列可以提升消息发送和消费的并发度。

实践

  1. 新建一个springboot项目,导入rocketmq starter
<dependency>
     <groupId>org.apache.rocketmq</groupId>
     <artifactId>rocketmq-spring-boot-starter</artifactId>
     <version>2.1.1</version>
 </dependency>

rocketmq starter 2.1.1版本对应的rocketmq 版本是 4.7.1。 目前最新版本的rocketmq starter版本是2.2.2,但是不建议使用,有时候消息会消费失败,rocketmq starter 2.2.2对应的版本是rocketmq 4.9.3。 这里使用的是rocketmq 4.9.3版本。

  1. 添加配置
# 名字服务器地址
rocketmq.name-server=localhost:9876
# 生产者组
rocketmq.producer.group=producer_group

rocketmq.consumer.topic=consumer_topic

rocketmq.consumer.group=consumer_group
  1. 新建消费者、生产者

消费者:

@RocketMQMessageListener(topic = "${rocketmq.consumer.topic}",consumerGroup ="${rocketmq.consumer.group}")
@Component
public class ConsumerTagADemo implements RocketMQListener<String> {
    
    @Override
    public void onMessage(String o) {
        System.out.println("ConsumerTagADemo onMessage="+o);
    }
}

创建消费者主要有两点要注意:

  1. 继承RocketMQListener接口,RocketMQListener接口泛型表示要接收的消息类型
  2. 在实现类上加RocketMQMessageListener注解

生产者:

@Component
public class ProducerDemo {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    // 发送消息的实例
    public void sendMessage(String topic, String msg) {
        rocketMQTemplate.convertAndSend(topic, msg);
    }
}

测试类:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RocketmqApplication.class)
class RocketmqApplicationTests {

    @Value("${rocketmq.consumer.topic}")
    private String topic;

    @Autowired
    private ProducerDemo producerDemo;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Test
    public void testProducer(){
    	String msg =  UUID.randomUUID().toString();
        rocketMQTemplate.convertAndSend(topic , msg);
    }
}
  1. 启动RocketMq并测试 这里是在windows系统进行单机测试。

分别启动mqnamesrv和mqbroker

start mqnamesrv.cmd

start mqbroker.cmd -n localhost:9876 autoCreateTopic=true

启动测试类和启动类进行测试,可以在控制台看到如下消费信息

ConsumerTagADemo onMessage=0b030ae2-f180-4d12-a21a-a57c35a853df

这里只是演示了RocketMq 发送普通消息。RocketMq 可以发送普通消息、顺序消息、延迟消息、批量消息、事务消息等,有兴趣的可以再去学习一下。

参考资料:

rocketmq.apache.org/docs/