RocketMQ简介
RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。
Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。
RocketMQ架构图
核心概念
NameServer主要包括两个功能:
-
Broker管理:NameServer接收Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;
-
路由信息管理:每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。
NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。
当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer和Consumer仍然可以动态感知Broker的路由的信息。
消费模式
消费模式由Consumer决定,消费维度为Topic;主要包含集群消费和广播消费。
发送方式
RocketMQ消息发送有三种方式:同步、异步、单项。
死信队列
死信队列用于处理无法被正常消费的消息。
当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
RocketMQ将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。
在RocketMQ中,可以通过使用console控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。
消息顺序
顺序消息分为全局顺序消息与分区顺序消息。
全局顺序是指某个Topic下的所有消息都要保证顺序;分区顺序消息只要保证每一组消息被顺序消费即可。
-
Sharding Key:顺序消息中用来区分Topic中不同分区的关键字段,和普通消息的Key是完全不同的概念。消息队列RocketMQ版会将设置了相同Sharding Key的消息路由到同一个分区下,同一个分区内的消息将按照消息发布顺序进行消费。
-
分区:即Topic Partition,每个Topic包含一个或多个分区,Topic中的消息会分布在这些不同的分区中。
-
物理分区:区别于逻辑分区,消息实际存储的单元,每个物理分区都会分配到某一台机器指定节点上。
核心知识考点
MQ功能
MQ选型
负载均衡
-
生产者
-
无序消息(普通消息、事务消息、定时和延时消息):Producer将消息以轮询的方式发送至Queue
-
顺序消息:相同Sharding Key的消息被发送至同一个Queue
-
消费者
-
无序消息:消息队列RocketMQ版Broker按照消息维度将Topic中的所有消息平均分配给消费者集群中的多个消费者处理。
-
顺序消息:消息消费根据Sharding Key负载均衡,不同Sharding Key的消息可被均衡的负载到多个Queue中消费,且为保证顺序消息的语义,相同Sharding Key的消息会被同一个消费者消费。
解决重复消费
通过消息消费记录表,检验消息是否已经处理过。
-
唯一索引:唯一索引或者唯一组合索引用来防止新增数据存在脏数据。
-
悲观锁。获取数据的时候加锁获取,select * from table_xxx where id='xxx' for update;id字段是主键或者唯一索引,不然是锁表。悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选择。
-
乐观锁。乐观锁只是在更新数据的那一刻锁表,其他时间不锁表,相对于悲观锁,效率更高。
以上基于业务表本身做去重,增加了业务开发复杂度。可以抽象出一个工具类设计通用的消息幂等处理方法,用来适用各个业务场景👇
基于消息幂等表的非事务方案:
这个方案实际上没有事务的,只需要一个存储的中心媒介,可以选择更灵活的存储媒介,例如Redis。
使用Redis有两个好处:
-
性能上损耗更低
-
超时时间可以直接利用Redis本身的ttl实现
当然Redis存储的数据可靠性、一致性等方面是不如MySQL的,需要自己取舍。
顺序消费原理
在消息队列RocketMQ版中,消息的顺序需要由以下三个阶段保证:
-
消息发送
上图A1、B1、A2、A3、B2、B3是订单A和订单B的消息产生的顺序,业务上要求同一订单的消息保持顺序,例如订单A的消息发送和消费都按照A1、A2、A3的顺序。
如果是普通消息,订单A的消息可能会被轮询发送到不同的队列中,不同队列的消息将无法保持顺序,而顺序消息发送时消息队列支持将Sharding Key相同(例如同一订单号)的消息序路由到一个分区中。
-
消息存储
Topic中的每个逻辑分区可以对应多个物理分区,当消息按照顺序发送到Topic中的逻辑分区时,每个分区的消息将按照负载均匀的存储到对应的多个物理分区中。
在物理分区中消息的存储可以不用保持顺序,但消息队列RocketMQ版中会记录消息在逻辑分区和物理分区中的映射关系及存储位置。
-
消息消费
即使同一逻辑分区的消息被存储在不同的物理分区中且没有保持消息的顺序,但是基于顺序消息2.0的保序协议,消息队列RocketMQ版服务端在投递消息时,最终还是会按照消息在逻辑队列中存储的顺序投递给Consumer。
Consumer消费消息时,同一Sharding Key的消息使用单线程消费,保证消息消费顺序和存储顺序一致,最终实现消费顺序和发布顺序的一致。
全局顺序消息实际上是一种特殊的分区顺序消息,即Topic中只有一个分区,因此全局顺序和分区顺序的实现原理相同。因为分区顺序消息有多个分区,所以分区顺序消息比全局顺序消息的并发度和性能更高。
防止消息丢失
丢失原因👇:
-
生产阶段:Producer通过网络将消息发送给Broker,若网络延迟不可达,可能发生丢失
-
存储阶段:Broker先将消息放到内存,然后根据刷盘策略持久化到硬盘。如果在持久化之前服务器异常宕机,则消息丢失
-
消费阶段:消息消费失败
解决方案👇:
-
Producer端:
-
采用send() 同步发送消息,发送结果是同步感知的;
-
发送失败后可以重试,设置重试次数,默认3次;
-
通过集群部署,如果因为Broker宕机了发送失败,重试的时候会发送到其他Broker上;
-
Broker端
-
修改刷盘策略为同步刷盘,默认异步;
-
集群部署,主从模式;
-
Consumer端
-
完全消费正常后再进行手动ack确认;
解决消息堆积
-
准备一个临时topic;
-
queue的数量是堆积的几倍,分布到多Broker中;
-
上线一台Consumer做消息搬运工,把原来topic消息挪到新的topic里(不做业务处理)
-
上线N台Consumer同时消费临时topic中数据;
-
改bug定位原因,恢复原来Consumer,继续消费之前topic;
事务原理
消息队列RocketMQ版分布式事务消息不仅可以实现应用之间的解耦,又能保证数据的最终一致性。
同时,传统的大事务可以被拆分为小事务,不仅能提升效率,还不会因为某一个关联应用的不可用导致整体回滚,从而最大限度保证核心系统的可用性。
在极端情况下,如果关联的某一个应用始终无法处理成功,也只需对当前应用进行补偿或数据订正处理,而无需对整体业务进行回滚。
RocketMQ事务分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
-
事务消息发送及提交:
-
发送消息(half消息)
-
服务端响应消息写入结果
-
根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
-
根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
2.补偿流程:
-
对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
-
Producer收到回查消息,检查回查消息对应的本地事务的状态
-
根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。