RocketMQ面试题

本文详细介绍了RocketMQ在项目中的应用,对比了与其他MQ的优劣,着重阐述了性能、分布式架构、异步解耦、限流削峰和数据收集等特性,以及为何最终选择RocketMQ,涵盖了消息中间件的通用功能、专业术语和具体部署策略。
摘要由CSDN通过智能技术生成

RocketMQ在实习项目中的应用?

我们项目中用RocketMQ就是来做异步解耦的,将上游的流量匹配任务发送到MQ中,下游任务监控MQ是否有数据到达,如果有数据到达就去执行流量匹配程序。这样能够实现异步操作,增加上游系统的并发和降低系统的压力。
有一个回调任务去根据上游任务生成的任务号去查询任务的执行状态。

各种MQ的对比?项目是如何选型的?

ActiveMQ:ActiveMQ是使用Java语言开发一款MQ产品。早期很多公司与项目中都在使用。但现在的社区活跃度已经很低。现在的项目中已经很少使用了。RabbitMQ:RabbitMQ是使用ErLang语言开发的一款MQ产品。其吞吐量较Kafka与RocketMQ要低,且由于其不是Java语言开发,所以公司内部对其实现定制化开发难度较大。Kafka:Kafka是使用Scala/Java语言开发的一款MQ产品。其最大的特点就是高吞吐率,常用于大数据领域的实时计算、日志采集等场景。其没有遵循任何常见的MQ协议,而是使用自研协议。RocketMQ:RocketMQ是使用Java语言开发的一款MQ产品。经过数年阿里双11的考验,性能与稳定性非常高。其没有遵循任何常见的MQ协议,而是使用自研协议。对于Spring Cloud Alibaba,其支持RabbitMQ、Kafka,但提倡使用RocketMQ。

RabbitMQ比Kafka可靠,Kafka更适合IO高吞吐的处理,一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用,比如ELK日志收集。

| ​
| Kafka | RocketMQ | RabbitMQ | ActiveMQ | | --- | --- | --- | --- | --- | | 单机吞吐量 | 10万级 | 10万级 | 万级 | 万级 | | 开发语言 | Scala | Java | Erlang | Java | | 高可用 | 分布式架构 | 分布式架构 | 主从架构 | 主从架构 | | 性能 | ms级 | ms级 | us级 | ms级 | | Topic | 百级Topic会影响系统吞吐量 | 千级Topic会影响系统吞吐量 | - | - | | 社区活跃度 | 高 | 高 | 高 | 低 |

我们主要调研了几个主流的mq,kafka、rabbitmq、rocketmq、activemq,选型我们主要基于以下几个点去考虑:

  1. 由于我们系统的qps压力比较大,所以性能是首要考虑的要素。
  2. 对于高并发的业务场景是必须的,所以需要支持分布式架构的设计。
  3. 功能全面,由于不同的业务场景,可能会用到顺序消息、事务消息等。

基于以上几个考虑,我们最终选择了RocketMQ

MQ的三大特性?

1.异步解耦

上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发度,且系统耦合度太高。而异步调用则会解决这些问题。所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层间添加一个MQ层。image.png

2.限流削峰

MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统被压垮。image.pngimage.png

3.数据收集

分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术。通过MQ完成此类数据收集是最好的选择。

RocketMQ有什么功能?

先答出消息中间件的通用功能

  1. 业务解耦:这也是发布订阅的消息模型。生产者发送指令到MQ中,然后下游订阅这类指令的消费者会收到这个指令执行相应的逻辑,整个过程与具体业务无关,抽象成了一个发送指令,存储指令,消费指令的过程。
  2. 前端削峰:前端发起的请求在短时间内太多后端无法处理,可以堆积在MQ中,后端按照一定的顺序处理,秒杀系统就是这么实现的。
  3. 数据收集

再说说RocketMQ的特点

  1. 亿级消息的堆积能力,单个队列中的百万级消息的累积容量。
  2. 高可用性:Broker服务器支持多Master多Slave的同步双写以及Master多Slave的异步复制模式,其中同步双写可保证消息不丢失。
  3. 高可靠性:生产者将消息发送到Broker端有三种方式,同步、异步和单向,其中同步和异步都可以保证消息成功的成功发送。Broker在对于消息刷盘有两种策略:同步刷盘和异步刷盘,其中同步刷盘可以保证消息成功的存储到磁盘中。消费者的消费模式也有集群消费和广播消费两种,默认集群消费,如果集群模式中消费者挂了,一个组里的其他消费者会接替其消费。综上所述,是高可靠的。
  4. 支持分布式事务消息:这里是采用半消息确认和消息回查机制来保证分布式事务消息的,下面会详细描述。
  5. 支持消息过滤:建议采用消费者业务端的tag过滤
  6. 支持顺序消息:消息在Broker中是采用队列的FIFO模式存储的,也就是发送是顺序的,只要保证消费的顺序性即可。
  7. 支持定时消息和延迟消息:Broker中由定时消息的机制,消息发送到Broker中,不会立即被Consumer消费,会等到一定的时间才被消费。延迟消息也是一样,延迟一定时间之后才会被Consumer消费。

    RocketMQ消息模型的专业术语?

消息(Message)
消息是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。主题(Topic)
Topic表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。 topic:message 1:n message:topic 1:1;
一个生产者可以同时发送多种Topic的消息;而一个消费者只对某种特定的Topic感兴趣,即只可以订阅和消费一种Topic的消息。 producer:topic 1:n consumer:topic 1:1标签(Tag)
为消息设置的标签,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
Topic是消息的一级分类,Tag是消息的二级分类。
Topic:货物
tag=上海
tag=江苏
tag=浙江
------- 消费者 -----
topic=货物 tag = 上海
topic=货物 tag = 上海|浙江
topic=货物 tag = *队列(Queue)
存储消息的物理实体。一个Topic中可以包含多个Queue,每个Queue中存放的就是该Topic的消息。一个Topic的Queue也被称为一个Topic中消息的分区(Partition)。
一个Topic的Queue中的消息只能被一个消费者组中的一个消费者消费。一个Queue中的消息不允许同一个消费者组中的多个消费者同时消费。消息标识(Message/Key)
RocketMQ中每个消息拥有唯一的MessageId,且可以携带具有业务标识的Key,以方便对消息的查询。不过需要注意的是,MessageId有两个:在生产者send()消息时会自动生成一个MessageId(msgId),当消息到Broker后,Broker也会自动生成一个MessageId(offsetMsgId)。msgId、offsetMsgId与key都称为消息标识。

  • msgId:由producer端生成,其生成规则为:

producerIp + 进程pid + MessageClientIDSetter类的ClassLoader的hashCode +当前时间 + AutomicInteger自增计数器

  • offsetMsgId:由broker端生成,其生成规则为:brokerIp + 物理分区的offset(Queue中的偏移量)
  • key:由用户指定的业务相关的唯一标识

组(Group)
组,可分为ProducerGroup生产者组和ConsumerGroup消费者组,一个组可以订阅多个Topic。一般来说,某一类相同业务的生产者和消费者放在一个组里。偏移量(offset)
消息队列中的offset,可以认为就是下标,消息队列可看做数组。offset是java long型,64位,理论上100年不会溢出,所以可以认为消息队列是一个长度无限的数据结构。

RocketMQ的架构?


RocketMq一共有四个部分组成:NameServer,Broker,Producer生产者,Consumer消费者,每一部分都是集群部署的。

  1. Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳
  2. Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
  3. Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费

    Producer

消息生产者,负责生产消息。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
RocketMQ中的消息生产者都是以生产者组(Producer Group)的形式出现的。生产者组是同一类生产者的集合,这类Producer发送相同Topic类型的消息。一个生产者组可以同时发送多个主题的消息。

例如,业务系统产生的日志写入到MQ的过程,就是消息生产的过程 再如,电商平台中用户提交的秒杀请求写入到MQ的过程,就是消息生产的过程

Consumer

消息消费者,负责消费消息。一个消息消费者会从Broker服务器中获取到消息,并对消息进行相关业务处理。

例如,QoS系统从MQ中读取日志,并对日志进行解析处理的过程就是消息消费的过程。 再如,电商平台的业务系统从MQ中读取到秒杀请求,并对请求进行处理的过程就是消息消费的过程。

RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。
消费者组是同一类消费者的集合,这类Consumer消费的是同一个Topic类型的消息。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。负载均衡:将一个Topic中的不同的Queue平均分配给同一个Consumer Group的不同的Consumer,注意,并不是将消息负载均衡;容错:一个Consmer挂了,该Consumer Group中的其它Consumer可以接着消费原Consumer消费的Queue;image.png
消费者组中Consumer的数量应该小于等于订阅Topic的Queue数量。如果超出Queue数量,则多出的Consumer将不能消费消息。image.png
不过,一个Topic类型的消息可以被多个消费者组同时消费。

  1. 消费者组只能消费一个Topic的消息,不能同时消费多个Topic消息
  2. 一个消费者组中的消费者必须订阅完全相同的Topic

NameServer

NameServer是一个Broker与Topic路由的注册中心,支持Broker的动态注册与发现。

RocketMQ的思想来自于Kafka,而Kafka是依赖了Zookeeper的。所以,在RocketMQ的早期版本,即在MetaQ v1.0与v2.0版本中,也是依赖于Zookeeper的。从MetaQ v3.0,使用自己的NameServer。

主要包括两个功能:

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

    Broker

消息存储和中转角色,负责存储和转发消息。
Broker在RocketMQ系统中负责接收并存储从生产者发送来的消息,同时为消费者的拉取请求作准备。Broker同时也存储着消息相关的元数据,包括消费者组消费进度偏移offset、主题、队列等。image.png

  1. Remoting Module:整个Broker的实体,负责处理来自clients端的请求。而这个Broker实体则由以下模块构成。
  2. Client Manager:客户端管理器。负责接收、解析客户端(Producer/Consumer)请求,管理客户端。例如,维护Consumer的Topic订阅信息
  3. Store Service:存储服务。提供方便简单的API接口,处理消息存储到物理硬盘和消息查询功能。
  4. HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
  5. Index Service:索引服务。根据特定的Message key,对投递到Broker的消息进行索引服务,同时也提供根据Message Key对消息进行快速查询的功能。

    RocketMQ的工作流程?

  6. 启动NameServer,NameServer启动后开始监听端口,等待Broker、Producer、Consumer连接。

  7. 启动Broker时,Broker会与所有的NameServer建立并保持长连接,然后每30秒向NameServer定时发送心跳包。
  8. Producer发送消息前,可以先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,当然,在创建Topic时也会将Topic与Broker的关系写入到NameServer中。不过,这步是可选的,也可以在发送消息时自动创建Topic。
  9. Producer发送消息,启动时先和NameServer集群中的其中一台建立长连接,并从NameServer中获取路由信息(当前发送的Topic消息的Queue与Broker的地址(IP+Port)的映射关系)。然后根据算法策略选择一个Queue,与队列所在的Broker建立长连接从而向Broker发消息。当然,在获取到路由信息后,Producer会首先将路由信息缓存到本地,再每30秒从NameServer更新一次路由信息。
  10. Consumer跟Producer类似,启动时先和NameServer集群中的其中一台建立长连接,并从NameServer中获取路由信息(订阅Topic的路由信息)。然后根据算法策略从路由信息中获取到其所要消费的Queue,然后与队列所在的Broker建立长连接,开始消费其中的消息。Consumer在获取到路由信息后,同样也会每30秒从NameServer更新一次路由信息。不过不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存活状态。

    为什么RocketMQ不使用Zookeeper作为注册中心?

早期的RocketMQ版本其实就是用Zookeeper作为注册中心的。
我认为有以下几个点是不使用zookeeper的原因:

  1. NameServer是自己写的,方便扩展,去中心化,只要有一个NameServer在,整个注册中心环境就可以用。
  2. Zookeeper是CP的,在进行选举的时候,整个选举的时间太长,期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的。
  3. 用Zookeeper架构更复杂,部署需要占用单独的服务器。

    NameServer的路由注册?

NameServer通常也是以集群的方式部署,不过,NameServer是无状态的,即NameServer集群中的各个节点间是无差异的,各节点间相互不进行信息通讯
那各节点中的数据是如何进行数据同步的呢?在Broker节点启动时,轮询NameServer列表,与每个NameServer节点建立长连接,发起注册请求。在NameServer内部维护着⼀个Broker列表,用来动态存储Broker的信息。

注意,这是与Zookeeper、Eureka、Nacos等注册中心不同的地方。 这种NameServer的无状态方式,有什么优缺点: 优点:NameServer集群搭建简单。 缺点:对于Broker,必须明确指出所有NameServer地址。否则未指出的将不会去注册。也正因为如此,NameServer并不能随便扩容。因为,若Broker不重新配置,新增的NameServer对于Broker来说是不可见的,其不会向这个NameServer进行注册。

Broker节点为了证明自己是活着的,为了维护与NameServer间的长连接,会将最新的信息以心跳包的方式上报给NameServer,每30秒发送一次心跳。心跳包中包含 BrokerId、Broker地址(IP+Port)、Broker名称、Broker所属集群名称等等。NameServer在接收到心跳包后,会更新心跳时间戳,记录这个Broker的最新存活时间。

NameServer的路由剔除?

由于Broker关机、宕机或网络抖动等原因,NameServer没有收到Broker的心跳,NameServer可能会将其从Broker列表中剔除。
NameServer中有⼀个定时任务,每隔10秒就会扫描⼀次Broker表,查看每一个Broker的最新心跳时间戳距离当前时间是否超过120秒,如果超过,则会判定Broker失效,然后将其从Broker列表中剔除。

NameServer的路由发现?

RocketMQ的路由发现采用的是Pull模型。当Topic路由信息出现变化时,NameServer不会主动推送给客户端,而是客户端定时拉取主题最新的路由。默认客户端每30秒会拉取一次最新的路由。

扩展: 1. Push模型:推送模型。其实时性较好,是一个“发布-订阅”模型,需要维护一个长连接。而长连接的维护是需要资源成本的。该模型适合于的场景:实时性要求较高;Client数量不多,Server数据变化较频繁。 2. Pull模型:拉取模型。存在的问题是,实时性较差。 2. Long Polling模型:长轮询模型。其是对Push与Pull模型的整合,充分利用了这两种模型的优势,屏蔽了它们的劣势。

客户端对NameServer的选择策略?

这里的客户端指的是Producer与Consumer

客户端在配置时必须要写上NameServer集群的地址,那么客户端到底连接的是哪个NameServer节点呢?
客户端首先会生产一个随机数,然后再与NameServer节点数量取模,此时得到的就是所要连接的节点索引,然后就会进行连接。如果连接失败,则会采用轮询策略,逐个尝试着去连接其它节点。总结:首先采用的是随机策略进行的选择,失败后采用的是轮询策略。

扩展:Zookeeper Client是如何选择Zookeeper Server的? 简单说就是:经过两次Shuffle,然后选择第一台Zookeeper Server。 详细说就是:将配置文件中的Zookeeper server地址进行第一次Shuffle,然后随机选择一个。这个选择出的一般都是一个hostname。然后获取到该hostname对应的所有ip,再对这些ip进行第二次Shuffle,从Shuffle过的结果中取第一个server地址进行连接。

RocketMQ的刷盘策略?

RocketMQ提供了两种刷盘策略:同步刷盘和异步刷盘

  1. 同步刷盘:在消息达到Broker的内存之后,必须刷到commitLog日志文件中才算成功,然后返回Producer数据已经发送成功。
  2. 异步刷盘:异步刷盘是指消息达到Broker内存(PageCache)后就返回Producer数据已经发送成功,会唤醒一个线程去将数据持久化到CommitLog日志文件中。

同步刷盘保证了消息不丢失,但是响应时间相对异步刷盘要多出10%左右,适用于对消息可靠性要求比较高的场景。
异步刷盘的吞吐量比较高,RT小,但是如果broker断电了内存中的部分数据会丢失,适用于对吞吐量要求比较高的场景。

RocketMQ的复制策略(Master和Slave之间)?

复制策略是Broker的Master与Slave间的数据同步方式。分为同步复制与异步复制:

  1. 同步复制:消息写入master后,master会等待slave同步数据成功后才向producer返回成功ACK;
  2. 异步复制:消息写入master后,master立即向producer返回成功ACK,无需等待slave同步数据成功;

    Broker集群是怎样部署的?

image.png
为了增强Broker性能与吞吐量,Broker一般都是以集群形式出现的。各集群节点中可能存放着相同Topic的不同Queue。不过,这里有个问题,如果某Broker节点宕机,如何保证数据不丢失呢?其解决方案是,将每个Broker集群节点进行横向扩展,即将Broker节点再建为一个HA集群,解决单点问题。
Broker节点集群是一个主备集群,即集群中具有Master与Slave两种角色。Master负责处理读写操作请求,Slave负责对Master中的数据进行备份。当Master挂掉了,Slave则会自动切换为Master去工作。Master和Slave是1:N的关系(N >= 1)
Master与Slave 的对应关系是通过指定相同的BrokerName、不同的BrokerId 来确定的。BrokerId为0表示Master,非0表示Slave。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。

Broker的四种集群方式?

  1. 单Master

只有一个broker(其本质上就不能称为集群)。这种方式也只能是在测试时使用,生产环境下不能使
用,因为存在单点问题。

  1. 多Master

broker集群仅由多个master构成,不存在Slave。同一Topic的各个Queue会平均分布在各个master节点
上。

  • 优点:配置简单
  • 缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅(不可消费),消息实时性会受到影响。
  1. 多Master和多Slave节点-异步复制

broker集群由多个master构成,每个master又配置了多个slave。master与slave的关系是主备关系,即master负责处理消息的读写请求,而slave仅负责消息的备份与master宕机后的角色切换。
异步复制即前面所讲的复制策略中的异步复制策略,即消息写入master成功后,master立即向producer返回成功ACK,无需等待slave同步数据成功。
该模式的最大特点之一是,当master宕机后slave能够自动切换为master。不过由于slave从master的同步具有短暂的延迟(毫秒级),所以当master宕机后,这种异步复制方式可能会存在少量消息的丢失问题

  1. 多Master和多Slave节点-同步复制

所谓同步双写,指的是消息写入master成功后,master会等待slave同步数据成功后才向producer返回成功ACK,即master与slave都要写入成功后才会返回成功ACK,也即双写。
该模式与异步复制模式相比,优点是消息的安全性更高,不存在消息丢失的情况。但单个消息的RT略高,从而导致性能要略低(大约低10%)。
该模式存在一个大的问题:对于目前的版本,Master宕机后,Slave不会自动切换到Master。

RocketMQ消息的生产?

消息的生产过程?

Producer可以将消息写入到某Broker中的某Queue中,其经历了如下过程:

  1. Producer发送消息之前,会先向NameServer发出获取消息Topic的路由信息的请求
  2. NameServer返回该Topic的路由表及Broker列表
  3. Producer根据代码中指定的Queue选择策略,从Queue列表中选出一个队列,用于后续存储消息
  4. Produer对消息做一些特殊处理,例如,消息本身超过4M,则会对其进行压缩
  5. Producer向选择出的Queue所在的Broker发出RPC请求,将消息发送到选择出的Queue > 路由表的key为Topic名称,value则为所有涉及该Topic的BrokerName列表。 > Broker列表其实际也是一个Map。key为BrokerName,value为BrokerData; > 一套BrokerName名称相同的Master-Slave小集群对应一个BrokerData。BrokerData中包含brokerName及一个map。该map的key为brokerId,value为该broker对应的地址。brokerId为0表示该broker为Master,非0表示Slave。

Producer消息的发送方式?

Producer发送消息有三种方式:同步、异步和单向

  1. 同步:同步发送是指发送方发出数据后等待接收方发回响应后在发送下一个数据包。一般用于重要的消息通知,如重要的通知邮件或者营销短信等。
  2. 异步:异步发送是指发送方发出数据后不等接收方发回响应就发出下一个数据包。一般用于可能链路耗时较长而对响应时间比较敏感的场景。如视频上传后通知启动转码服务。
  3. 单向:单向发送是指只负责发送消息而不等待接收方发送响应且没有回调函数,适合那些耗时比较短且对可靠性要求不高的场景,例如日志收集。

    RocketMQ消息的存储?

RocketMQ中的消息存储在本地文件系统中,这些相关文件默认在当前用户主目录下的store目录中。image.png

  • abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失。若在没有启动Broker的情况下,发现这个文件是存在的,则说明之前Broker的关闭是非正常关闭。
  • checkpoint:其中存储着commitlog、consumequeue、index文件的最后刷盘时间戳
  • commitlog:其中存放着commitlog文件,而消息是写在commitlog文件中的
  • config:存放着Broker运行期间的一些配置数据
  • consumequeue:其中存放着consumequeue文件,队列就存放在这个目录中
  • index:其中存放着消息索引文件indexFile
  • lock:运行期间使用到的全局资源锁

    commitlog文件

commitlog目录中存放着很多的mappedFile文件,当前Broker中的所有消息都是落盘到这些mappedFile文件中的。mappedFile文件大小为1G(小于等于1G),写满后自动生成一个新的文件。文件名由20位十进制数构成,表示当前文件的第一条消息的起始位移偏移量。

  1. 一个Broker中仅包含一个commitlog目录,所有的mappedFile文件都是存放在该目录中的。即无论当前Broker中存放着多少Topic的消息,这些消息都是被顺序写入到了mappedFile文件中的。
  2. 也就是说,这些消息在Broker中存放时并没有被按照Topic进行分类存放。

mappedFile文件内容由一个个的消息单元构成。每个消息单元中包含消息总长度MsgLen、消息的物理位置physicalOffset、消息体内容Body、消息体长度BodyLength、消息主题Topic、Topic长度、TopicLength、消息生产者BornHost、消息发送时间戳BornTimestamp、消息所在的队列QueueId、消息在Queue中存储的偏移量QueueOffset等近20余项消息相关属性。 image.png

consumequeue文件

image.png
由于同一个topic的消息并不是连续的存储在commitlog中,消费者如果直接从commitlog获取消息效率非常低,所以通过consumequeue保存commitlog中消息的偏移量的物理地址,这样消费者在消费的时候先从consumequeue中根据偏移量定位到具体的commitlog物理文件,然后根据一定的规则(offset和文件大小取模)在commitlog中快速定位。
为了提高效率,会为每个Topic在/store/consumequeue中创建一个目录,目录名为Topic名称。在该Topic目录下,会再为每个该Topic的Queue建立一个目录,目录名为queueId。每个目录中存放着若干consumequeue文件,consumequeue文件是commitlog的索引文件,可以根据consumequeue定位到具体的消息。
consumequeue文件名也由20位数字构成,表示当前文件的第一个索引条目的起始位移偏移量。和mappedFile文件名不同的是,其后续文件名是固定的。因为consumequeue文件大小是固定不变的。consumequeue文件中存储着一个个的索引条目,如下图所示。image.png

消息的写入过程

image.png

  1. Broker根据queueId,获取到该消息对应索引条目要在consumequeue目录中的写入偏移量,即QueueOffset
  2. 将queueId、queueOffset等数据,与消息一起封装为消息单元
  3. 将消息单元写入到commitlog
  4. 同时,形成消息索引条目
  5. 将消息索引条目分发到相应的consumequeue

    RocketMQ消息的消费?

Consumer获取消息的方式?

拉取式消费
Consumer主动从Broker中拉取消息,主动权由Consumer控制。一旦获取了批量消息,就会启动消费过程。不过,该方式的实时性较弱,即Broker中有了新的消息时消费者并不能及时发现并消费。

由于拉取时间间隔是由用户指定的,所以在设置该间隔时需要注意平稳:间隔太短,空请求比例会增加;间隔太长,消息的实时性太差

推送式消费(项目就采用这种模式)
该模式下Broker收到数据后会主动推送给Consumer。该获取方式一般实时性较高。
该获取方式是典型的发布-订阅模式,即Consumer向其关联的Queue注册了监听器,一旦发现有新的消息到来就会触发回调的执行,回调方法是Consumer去Queue中拉取消息。而这些都是基于Consumer与Broker间的长连接的。长连接的维护是需要消耗系统资源的。

pull:需要应用去实现对关联Queue的遍历,实时性差;但便于应用控制消息的拉取 push:封装了对关联Queue的遍历,实时性强,但会占用较多的系统资源

Consumer消费消息的模式?

消费模型由Consumer决定,消费维度为Topic广播消费image.png

  1. 广播消费模式下,相同Consumer Group的每个Consumer实例都接收同一个Topic的全量消息。即每条消息都会被发送到Consumer Group中的每个Consumer。
  2. 消费进度保存在consumer端。因为广播模式下consumer group中每个consumer都会消费所有消息,但它们的消费进度是不同。所以consumer各自保存各自的消费进度。

集群消费image.png

  1. 集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊同一个Topic的消息。即每条消息只会被发送到Consumer Group中的某个Consumer。
  2. 消费进度保存在broker中。consumer group中的所有consumer共同消费同一个Topic中的消息,同一条消息只会被消费一次。消费进度会参与到了消费的负载均衡中,故消费进度是需要共享的。

    RocketMQ的Rebalance机制?

什么是Rebalance机制?

将⼀个Topic下的多个Queue在同⼀个Consumer Group中的多个Consumer间进行重新分配的过程。image.png
Rebalance机制的本意是为了提升消息的并行消费能力。例如,⼀个Topic下5个队列,在只有1个消费者的情况下,这个消费者将负责消费这5个队列的消息。如果此时我们增加⼀个消费者,那么就可以给其中⼀个消费者分配2个队列,给另⼀个分配3个队列,从而提升消息的并行消费能力。

Rebalance的限制?

由于⼀个队列最多分配给⼀个消费者,因此当某个消费者组下的消费者实例数量大于队列的数量时,多余的消费者实例将分配不到任何队列

Rebalance的危害?

  1. 消费暂停:在只有一个Consumer时,其负责消费所有队列;在新增了一个Consumer后会触发Rebalance的发生。此时原Consumer就需要暂停部分队列的消费,等到这些队列分配给新的Consumer后,这些暂停消费的队列才能继续被消费。
  2. 消费重复:Consumer 在消费新分配给自己的队列时,必须接着之前Consumer 提交的消费进度的offset继续消费。然而默认情况下,offset是异步提交的,这个异步性导致提交到Broker的offset与Consumer实际消费的消息并不一致。这个不一致的差值就是可能会重复消费的消息。
  3. 消费突刺:由于Rebalance可能导致重复消费,如果需要重复消费的消息过多,或者因为Rebalance暂停时间过长从而导致积压了部分消息。那么有可能会导致在Rebalance结束之后瞬间需要消费很多消息。

    Rebalance产生的原因?

  4. 消费者所订阅Topic的Queue数量发生变化

  5. 消费者组中消费者的数量发生变化

    RocketMQ的负载均衡是如何实现的?

RocketMQ是分布式消息服务,负载均衡是在生产者和消费者的客户端完成的。

消费者的负载均衡:

  1. 平均分配策略

image.png
该算法是要根据avg = QueueCount / ConsumerCount的计算结果进行分配的。如果能够整除,则按顺序将avg个Queue逐个分配Consumer;如果不能整除,则将多余出的Queue按照Consumer顺序逐个分配。

  1. 环形平均策略

image.png
根据消费者的顺序,依次在由queue队列组成的环形图中逐个分配。

  1. 一致性哈希

image.png
该算法会将consumer的hash值作为Node节点存放到hash环上,然后将queue的hash值也放到hash环上,通过顺时针方向,距离queue最近的那个consumer就是该queue要分配的consumer。

缺点:分配效率低,容易出现不平衡现象; 优点:可以有效减少由于消费者组扩容或缩容所带来的大量的Rebalance;

  1. 同机房策略

image.png
该算法会根据queue的部署机房位置和consumer的位置,过滤出当前consumer相同机房的queue。然后按照平均分配策略或环形平均策略对同机房queue进行分配。如果没有同机房queue,则按照平均分配策略或环形平均策略对所有queue进行分配。

生产者的负载均衡:

对于无序消息,其Queue选择算法,也称为消息投递算法,常见的有两种:

  1. 轮询算法

默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息。
该算法存在一个问题:由于某些原因,在某些Broker上的Queue可能投递延迟较严重。从而导致Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。

  1. 最小投递延迟算法

该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。
如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。
该算法也存在一个问题:消息在Queue上的分配不均匀。投递延迟小的Queue其可能会存在大量的消息。而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆积。

什么是消费的至少一次原则?

RocketMQ有一个原则:每条消息必须要被成功消费一次。
那么什么是成功消费呢?Consumer在消费完消息后会向其消费进度记录器提交其消费消息的offset,
offset被成功记录到记录器中,那么这条消费就被成功消费了。

什么是消费进度记录器? 对于广播消费模式来说:Consumer本身就是消费进度记录器。 对于集群消费模式来说:Broker是消费进度记录器。

RocketMQ的offset的管理?

offset指的是Consumer的消费进度offset

offset本地管理模式

当消费模式为广播消费时,offset使用本地模式存储。因为每条消息会被所有的消费者消费,每个消费者管理自己的消费进度,各个消费者之间不存在消费进度的交集。
Consumer在广播消费模式下offset相关数据以json的形式持久化到Consumer本地磁盘文件中,默认文件路径为当前用户主目录下的.rocketmq_offsets/${clientId}/${group}/Offsets.json。其中${clientId}为当前消费者id,默认为ip@DEFAULT;${group}为消费者组名称。

offset远程管理模式

当消费模式为集群消费时,offset使用远程模式管理。因为所有Cosnumer实例对消息采用的是均衡消费,所有Consumer共享Queue的消费进度。
Consumer在集群消费模式下offset相关数据以json的形式持久化到Broker磁盘文件中,文件路径为当前用户主目录下的store/config/consumerOffset.json
Broker启动时会加载这个文件,并写入到一个双层Map(ConsumerOffsetManager)。外层map的key为topic@group,value为内层map。内层map的key为queueId,value为offset。当发生Rebalance时,新的Consumer会从该Map中获取到相应的数据来继续消费。image.png

集群模式下offset采用远程管理模式,主要是为了保证Rebalance机制。

offset用途

消费者是如何从最开始持续消费消息的?消费者要消费的第一条消息的起始位置是用户自己通过consumer.setConsumeFromWhere()方法指定的。
在Consumer启动后,其要消费的第一条消息的起始位置常用的有三种,这三种位置可以通过枚举类型常量设置。这个枚举类型为ConsumeFromWhere。

CONSUMEFROMLASTOFFSET:从queue的当前最后一条消息开始消费; CONSUMEFROMFIRSTOFFSET:从queue的第一条消息开始消费; CONSUMEFROMTIMESTAMP:从指定的具体时间戳位置的消息开始消费。这个具体时间戳是通过另外一个语句指定的;

当消费完一批消息后,Consumer会提交其消费进度offset给Broker,Broker在收到消费进度后会将其更新到那个双层Map(ConsumerOffsetManager)及consumerOffset.json文件中,然后向该Consumer进行ACK,而ACK内容中包含三项数据:

  • 当前消费队列的最小offset(minOffset)
  • 最大offset(maxOffset)
  • 下次消费的起始offset(nextBeginOffset)

    重试队列

image.png
当RocketMQ对消息的消费出现异常时,会将发生异常的消息的offset提交到Broker中的重试队列。系统在发生消息消费异常时会为当前的topic@group创建一个重试队列,该队列以%RETRY%开头,到达重试时间后进行消费重试。

"%RETRY%persons-topic@persons-group" : {0:6, 1:9}

offset的同步提交与异步提交

集群消费模式下,Consumer消费完消息后会向Broker提交消费进度offset,其提交方式分为两种:

  1. 同步提交:消费者在消费完一批消息后会向broker提交这些消息的offset,然后等待broker的成功响应。若在等待超时之前收到了成功响应,则继续读取下一批消息进行消费(从ACK中获取nextBeginOffset)。若没有收到响应,则会重新提交,直到获取到响应。而在这个等待过程中,消费者是阻塞的。其严重影响了消费者的吞吐量。
  2. 异步提交:消费者在消费完一批消息后向broker提交offset,但无需等待Broker的成功响应,可以继续读取并消费下一批消息。这种方式增加了消费者的吞吐量。但需要注意,broker在收到提交的offset后,还是会向消费者进行响应的。可能还没有收到ACK,此时Consumer会从Broker中直接获取、nextBeginOffset。

    RocketMQ中的消息被消费后会被清理掉吗?

消息是被顺序存储在commitlog文件的,且消息大小不定长,所以消息的清理是不可能以消息为单位进行清理的,而是以commitlog文件为单位进行清理的。否则会急剧下降清理效率,并实现逻辑复杂。
commitlog文件存在一个过期时间,默认为72小时,即三天。除了用户手动清理外,在以下情况下也会被自动清理,无论文件中的消息是否被消费过:

  • 文件过期,且到达清理时间点(默认为凌晨4点)后,自动清理过期文件
  • 文件过期,且磁盘空间占用率已达过期清理警戒线(默认75%)后,无论是否达到清理时间点,都会自动清理过期文件
  • 磁盘占用率达到清理警戒线(默认85%)后,开始按照设定好的规则清理文件,无论是否过期。默认会从最老的文件开始清理
  • 磁盘占用率达到系统危险警戒线(默认90%)后,Broker将拒绝消息写入

    RocketMQ为什么这么快?

是因为使用了顺序存储、Page Cache、异步刷盘、mmap零拷贝技术。

  1. 我们在写入commitlog的时候是顺序写入的,这样比随机写入的性能就会提高很多;
  2. 写入commitlog的时候并不是直接写入磁盘,而是先写入操作系统的PageCache;
    1. 写操作:OS会先将数据写入到Page Cache中,随后会以异步方式由pdflush内核线程将Cache中的数据刷盘到物理磁盘;
    2. 读操作:若用户要读取数据,其首先会从Page Cache中读取,若没有命中,则OS在从物理磁盘上加载该数据到PageCache的同时,也会顺序对其相邻数据块中的数据进行预读取;
  3. RocketMQ对文件的读写操作是通过mmap零拷贝进行的,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率。

    RocketMQ如何保证消息不被重复消费(幂等性)?

image.png

消费重复的场景分析?

  1. 发送时消息重复

当一条消息已被成功发送到Broker并完成持久化,此时出现了网络闪断,从而导致Broker对Producer应答失败。 如果此时Producer意识到消息发送失败并尝试再次发送消息,此时Broker中就可能会出现两条内容相同并且Message ID也相同的消息,那么后续Consumer就一定会消费两次该消息。

  1. 消费时消息重复

消息已投递到Consumer并完成业务处理,当Consumer给Broker反馈应答时网络闪断,Broker没有接收到消费成功响应。为了保证消息至少被消费一次的原则,Broker将在网络恢复后再次尝试投递之前已被处理过的消息。此时消费者就会收到与之前处理过的内容相同、Message ID也相同的消息。

  1. Rebalance时消息重复

当Consumer Group中的Consumer数量发生变化时,或其订阅的Topic的Queue数量发生变化时,会触发Rebalance,此时Consumer可能会收到曾经被消费过的消息。

什么是消费幂等?

当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响,那么这个消费过程就是消费幂等的。

幂等:若某操作执行多次与执行一次对系统产生的影响是相同的,则称该操作是幂等的。

在互联网应用中,尤其在网络不稳定的情况下,消息很有可能会出现重复发送或重复消费。如果重复的消息可能会影响业务处理,那么就应该对消息做幂等处理。

怎样解决重复消息问题?

因为消息队列的服务质量是At least once ,因此消息队列无法保证消息不重复,因此消费重复的问题得由Consumer端来解决。
RocketMQ能保证消息不丢失,但是
实现幂等的最好方式:从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。

  1. 数据库唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条该唯一主键的记录。

  1. 使用Redis缓存+数据库实现

以支付场景为例:

  1. 当支付请求到达后,首先在Redis缓存中却获取key为支付流水号的缓存value。若value不空,则说明本次支付是重复操作,业务系统直接返回调用侧重复支付标识;若value为空,则进入下一步操作
  2. 到DBMS中根据支付流水号查询是否存在相应实例。若存在,则说明本次支付是重复操作,业务系统直接返回调用侧重复支付标识;若不存在,则说明本次操作是首次操作,进入下一步完成唯一性处理
  3. 在分布式事务中完成三项操作:
    1. 完成支付任务
    2. 将当前支付流水号作为key,任意字符串作为value,通过set(key, value, expireTime)将数据写入到Redis缓存
    3. 将当前支付流水号作为主键,与其它相关数据共同写入到DBMS

      RocketMQ如何处理消息大量堆积?

消息堆积的原因?

消息堆积的主要瓶颈在于客户端的消费能力,而消费能力由消费耗时消费并发度决定。注意,消费耗时的优先级要高于消费并发度。即在保证了消费耗时的合理性前提下,再考虑消费并发度问题。

  1. 消费耗时

影响消息处理时长的主要因素是代码逻辑。而代码逻辑中可能会影响处理时长代码主要有两种类型:CPU内部计算型代码和外部I/O操作型代码
内部计算耗时相对外部I/O操作来说几乎可以忽略,所以外部IO型代码是影响消息处理时长的主要症结所在,通常消息堆积是由于下游系统出现了服务异常或达到了DBMS容量限制,导致消费耗时增加。外部IO操作型代码举例

  • 读写外部数据库,例如对远程MySQL的访问
  • 读写外部缓存系统,例如对远程Redis的访问
  • 下游系统调用,例如Dubbo的RPC远程调用,Spring Cloud的对下游系统的Http接口调用
    1. 消费并发度

消费者端的消费并发度由单节点线程数和节点数量共同决定,其值为单节点线程数*节点数量。不过,通常需要优先调整单节点的线程数,若单机硬件资源达到了上限,则需要通过横向扩展来提高消费并发度。单节点线程数,即单个Consumer所包含的线程数量节点数量,即Consumer Group所包含的Consumer数量

  1. 单机线程数如何设置?

对于一台主机中线程池中线程数的设置需要谨慎,不能盲目直接调大线程数,设置过大的线程数反而会带来大量的线程切换的开销。理想环境下单节点的最优线程数计算模型为:C *(T1 + T2)/ T1。
C:CPU内核数 T1:CPU内部逻辑计算耗时 T2:外部IO操作耗时

如何避免消息堆积?

为了避免在业务使用时出现非预期的消息堆积和消费延迟问题,需要在前期设计阶段对整个业务逻辑进行完善的排查和梳理。其中最重要的就是梳理消息的消费耗时和设置消息消费的并发度。

  1. 梳理消息的消费耗时

通过压测获取消息的消费耗时,并对耗时较高的操作的代码逻辑进行分析。梳理消息的消费耗时需要关注以下信息:

  • 消息消费逻辑的计算复杂度是否过高,代码是否存在无限循环和递归等缺陷。
  • 消息消费逻辑中的I/O操作是否是必须的,能否用本地缓存等方案规避。
  • 消费逻辑中的复杂耗时的操作是否可以做异步化处理。如果可以,是否会造成逻辑错乱。
  1. 设置消费并发度

对于消息消费并发度的计算,可以通过以下两步实施:

  • 逐步调大单个Consumer节点的线程数,并观测节点的系统指标,得到单个节点最优的消费线程数和消息吞吐量。
  • 根据上下游链路的流量峰值计算出需要设置的节点数 > 节点数 = 流量峰值 / 单个节点消息吞吐量

RocketMQ怎么保证消息不丢失(可靠性)?

image.png
这里我们将消息的整体处理阶段分为3个阶段进行分析:
Producer发送消息阶段。
Broker处理消息阶段。
Consumer消费消息阶段。

Producer发送消息阶段

发送消息阶段涉及到Producer到broker的网络通信,因此丢失消息的几率一定会有,那RocketMQ在此阶段用了哪些手段保证消息不丢失了(或者说降低丢失的可能性)。手段一:提供SYNC的发送消息方式,等待broker处理结果。
RocketMQ提供了3种发送消息方式,分别是:
同步发送:Producer 向 broker 发送消息,阻塞当前线程等待 broker 响应 发送结果。
异步发送:Producer 首先构建一个向 broker 发送消息的任务,把该任务提交给线程池,等执行完该任务时,回调用户自定义的回调函数,执行处理结果。
Oneway发送:Oneway 方式只负责发送请求,不等待应答,Producer只负责把请求发出去,而不处理响应结果。
我们在调用producer.send方法时,不指定回调方法,则默认采用同步发送消息的方式,这也是丢失几率最小的一种发送方式。手段二:发送消息如果失败或者超时,则重新发送。
发送重试源码如下,本质其实就是一个for循环,当发送消息发生异常的时候重新循环发送。默认重试3次,重试次数可以通过producer指定。手段三:broker提供多master模式,即使某台broker宕机了,保证消息可以投递到另外一台正常的broker上。
如果broker只有一个节点,则broker宕机了,即使producer有重试机制,也没用,因此利用多主模式,当某台broker宕机了,换一台broker进行投递。总结:producer消息发送方式虽然有3种,但为了减小丢失消息的可能性尽量采用同步的发送方式,同步等待发送结果,利用同步发送+重试机制+多个master节点,尽可能减小消息丢失的可能性。

Broker处理消息阶段

手段四:提供同步刷盘的策略
public enum FlushDiskType { SYNCFLUSH, //同步刷盘 ASYNCFLUSH//异步刷盘(默认) }
我们知道,当消息投递到broker之后,会先存到page cache,然后根据broker设置的刷盘策略是否立即刷盘,也就是如果刷盘策略为异步,broker并不会等待消息落盘就会返回producer成功,也就是说当broker所在的服务器突然宕机,则会丢失部分页的消息。手段五:提供主从模式,同时主从支持同步双写
即使broker设置了同步刷盘,如果主broker磁盘损坏,也是会导致消息丢失。 因此可以给broker指定slave,同时设置master为SYNC_MASTER,然后将slave设置为同步刷盘策略。
此模式下,producer每发送一条消息,都会等消息投递到master和slave都落盘成功了,broker才会当作消息投递成功,保证休息不丢失。总结:在broker端,消息丢失的可能性主要在于刷盘策略和同步机制。
RocketMQ默认broker的刷盘策略为异步刷盘,如果有主从,同步策略也默认的是异步同步,这样子可以提高broker处理消息的效率,但是会有丢失的可能性。因此可以通过同步刷盘策略+同步slave策略+主从的方式解决丢失消息的可能。

Consumer消费消息阶段

手段六:consumer默认提供的是At least Once机制
从producer投递消息到broker,即使前面这些过程保证了消息正常持久化,但如果consumer消费消息没有消费到也不能理解为消息绝对的可靠。因此RockerMQ默认提供了At least Once机制保证消息可靠消费。何为At least Once?
Consumer先pull 消息到本地,消费完成后,才向服务器返回ack。
通常消费消息的ack机制一般分为两种思路:
1、先提交后消费;
2、先消费,消费成功后再提交;
思路一可以解决重复消费的问题但是会丢失消息,因此Rocketmq默认实现的是思路二,由各自consumer业务方保证幂等来解决重复消费问题。手段七:消费消息重试机制
当消费消息失败了,如果不提供重试消息的能力,则也不能算完全的可靠消费,因此RocketMQ本身提供了重新消费消息的能力。总结:consumer端要保证消费消息的可靠性,主要通过At least Once+消费重试机制保证。

RocketMQ如何实现消息过滤?

  1. 一种是在broker端按照Consumer的去重逻辑进行过滤,这样做的好处是避免了无用的消息传输到Consumer端,缺点是加重了Broker的负担,实现起来相对复杂。
  2. 另一种是在Consumer端过滤,比如按照消息设置的tag去重,这样的好处是实现起来简单,缺点是有大量无用的消息到达了Consumer端只能丢弃不处理。

    RocketMQ如何保证消息的最终一致性?

https://juejin.cn/post/6936441094880264229

RocketMQ如何保证消息的顺序性?

顺序由producer发送到broker的消息队列是满足FIFO的,所以发送是顺序的,单个queue里的消息是顺序的。多个Queue同时消费是无法绝对保证消息的有序性的。
所以,同一个topic,同一个queue,发消息的时候一个线程发送消息,消费的时候一个线程去消费一个queue里的消息。

怎么保证消息发到同一个queue里?

RocketMQ给我们提供了MessageQueueSelector接口,可以重写里面的接口,实现自己的算法,比如判断i%2==0,那就发送消息到queue1否则发送到queue2。 java for (int i = 0; i < 5; i++) { Message message = new Message("orderTopic", ("hello!" + i).getBytes()); producer.send( // 要发的那条消息 message, // queue 选择器 ,向 topic中的哪个queue去写消息 new MessageQueueSelector() { // 手动 选择一个queue @Override public MessageQueue select( // 当前topic 里面包含的所有queue List mqs, // 具体要发的那条消息 Message msg, // 对应到 send() 里的 args,也就是2000前面的那个0 Object arg) { // 向固定的一个queue里写消息,比如这里就是向第一个queue里写消息 if (Integer.parseInt(arg.toString()) % 2 == 0) { return mqs.get(0); } else { return mqs.get(1); } } }, // 自定义参数:0 // 2000代表2000毫秒超时时间 i, 2000); }

你知道半消息吗?RocketMQ是怎么实现分布式事务消息的?

半消息:是指暂时还不能被Consumer消费的消息,Producer成功发送到broker端的消息,但是此消息被标记为“暂不可投递”状态,只有等Producer端执行完本地事务后经过二次确认了之后,Consumer才能消费此条消息。
上图就是分布式事务消息的实现过程,依赖半消息,二次确认以及消息回查机制。

  1. Producer向broker发送半消息
  2. Producer端收到响应,消息发送成功,此时消息是半消息,标记为“不可投递”状态,Consumer消费不了。
  3. Producer端执行本地事务。
  4. 正常情况本地事务执行完成,Producer向Broker发送Commit/Rollback,如果是Commit,Broker端将半消息标记为正常消息,Consumer可以消费,如果是Rollback,Broker丢弃此消息。
  5. 异常情况,Broker端迟迟等不到二次确认。在一定时间后,会查询所有的半消息,然后到Producer端查询半消息的执行情况。
  6. Producer端查询本地事务的状态
  7. 根据事务的状态提交commit/rollback到broker端。(5,6,7是消息回查)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Luke@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值