RocketMq学习(一)初识RocketMq

572 阅读7分钟

在上一篇文章浅谈消息模型中我们初步了解了RocketMq的一些基本特性,从这篇开始我们将逐步的去学习RocketMq的每一个知识点。

基本概念

什么是MQ?

MQ通常被翻译为消息队列,这里我觉得比较合理的叫法应该是消息中间件。那么RocketMq可称作是一种发布-订阅模型的消息中间件。它常应用于以下场景:

  • 通信 例如跨语言间的通信,如果对于实时性要求不高,那么可以用MQ来实现通信。

  • 异步和解耦 用两幅图来说明:

  • 流量削峰 例如秒杀场景,除了基于令牌桶等算法等来做限流外,也可以利用MQ本身的高吞吐量、高性能、高可用特性,请求直接放进MQ,后续根据业务服务器处理能力慢慢处理秒杀请求,来达到流量削峰的目的。

架构

这里直接放上官网的架构图 可以看出,RocketMq主要由4部分组成:

名称服务器

名称服务器类似于kafka中的zookeeper,主要提供了两个功能:

  • 简单的路由注册和发现机制 broker集群启动后,会向nameserver集群中的每一台服务器发送集群的路由信息,这里需要明确两个点:一是nameserver集群中每一台服务器之间互相不通信;二是每一台服务器里都保存着一份完整的broker集群路由信息,这里路由信息说白了就是topic的路由信息,以及队列(分区)的信息

  • 心跳检测 首先心跳请求是由broker每隔30s发送给nameserver集群的,发送请求里包含什么呢?会包含该broker上所有的topic信息,nameserver接收topic信息进行更新(这里每次心跳请求会记录更新时间)。然后会每隔10s进行反向检查,如果检测距离上次更新时间超过了2分钟,即2分钟内都没有来自broker的心跳请求,则认为broker已经下线了,然后调整Topic跟Broker的对应关系

代理服务器

Broker主要负责消息的存储、投递、查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块。

  • Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
  • Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
  • Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
  • HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
  • Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。

生产者

生产者即消息的发送方,支持分布式集群部署。生产者在发送消息时,首先会从本地缓存的TopicPublishInfoTable中获取路由信息,如果没有则从nameserver拉取路由信息并更新本地路由缓存,然后每隔30s会向nameserver请求拉取一次。

消费者

  • 消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。
  • Consumer会每隔30秒发心跳信息到Broker。Broker端每10秒检查一次当前存活的Consumer,若发现某个Consumer2分钟内没有心跳, 就断开与该Consumer的连接,并且向该消费组的其他实例发送通知,触发该消费者集群的负载均衡(rebalance)。
  • 消息的消费支持集群消费和广播消费两种模式,他们的区别在于:

消息相关概念

消息

数据传输的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。消息存储的主体是commitlog,关于commitlog后续会专门去讲,这里不做过多阐述。

主题和队列

主题表示一类消息的集合。一个主题可以包含多个queue,这里queue跟kafka中的分区不是一回事,是逻辑分区的概念。二者的区别在于:

  • kafka中的分区实际存储消息,不同的分区可以位于不同的broker上,即可分布式存储,可扩展
  • rocketmq中的queue是逻辑分区的概念,不实际存储消息实体,而是存储队列消息在commitlog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值,实际消费时根据起始偏移量去commitlog中开始消费

二者的共同点在于:

  • 每个分区(queue)中的消息都是有序的,并且同一个消费组下分区的消息只能由一个消费者来消费,而一个消费者可以消费多个分区的消息

标签

为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。比如tag为order表示订单消息,goods为商品消息等等

顺序消息

顺序消息这里往往指消息消费的有序性,即发送方按照什么样的顺序发送消息,就会按照同样的顺序来消费。

而实际情况是一个topic可以设置多个queue,不同的消息可能发送到不同的queue,每个queue由不同的消费者消费时可能快慢不同,后发的消息可能先执行。拿订单模块举例: 如图,创建订单和支付订单的消息分别发送到了Queue1和Queue2,但是呢可能消费者1的消费比较慢,消费者2消费比较快,就可能导致支付消息先消费了,业务上也会因此出现问题。那怎么办呢?

前面介绍主题和队列时说了,每一个queue中收到的消息是有序的。那么我们可以在发消息时控制相同订单的消息都发往同一个队列不就可以了。具体实现时举个例子可以把订单ID进行hash计算,然后对队列数取模。这样做是不是就可以了?

答案是no。这里只是保证把相同的消息发送到了同一个队列,来避免不同队列消费快慢导致的顺序问题。但是我们知道一个消费者为了加快消费的进度,可能会开启多个work线程,多个work线程并发消费一个队列里的消息的话不就又可能会出现顺序问题。

怎么办呢?加锁啊亲们,实际上RocketMQ会为每个消息队列建一个对象锁来保证顺序消费,这样只要线程池中有该消息队列在处理,则需等待处理完才能进行下一次消费,保证在当前 Consumer 内,同一队列的消息进行串行消费。

幂等性

RocketMQ无法避免消息重复(Exactly-Once),所以如果业务对消费重复非常敏感,务必要在业务层面进行去重处理。可以借助关系数据库进行去重。首先需要确定消息的唯一键,可以是msgId,也可以是消息内容中的唯一标识字段,例如订单Id等。在消费之前判断唯一键是否在关系数据库中存在。如果不存在则插入,并消费,否则跳过。(实际过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳过)

msgId一定是全局唯一标识符,但是实际使用中,可能会存在相同的消息有两个不同msgId的情况(消费者主动重发、因客户端重投机制导致的重复等),这种情况就需要使业务字段进行重复消费。