RocketMQ的生产者原理和最佳实践-学习笔记

对于消息队列,生产者通常是入门第一个接触的对象,用于生产

消息给消费者消费。本章通过介绍生产者实现类的属性、方法,引出Ω生产者的启动过程、高可靠的实现方式等,主要讲解内容如下:
● RocketMQ支持3种消息:普通消息(并发消息)、顺序消息、 事务消息。
● RocketMQ支持3种发送方式:同步发送、异步发送、单向发送。
● RocketMQ生产者最佳实践和总结。
 
2.1 生产者原理

通过第1章的讲解,相信读者对RocketMQ有了一个基本的认识,本 节将对RocketMQ中的生产者做基本介绍。
2.1.1 生产者概述
发送消息的一方被称为生产者,它在整个RocketMQ的生产和消费体 系中扮演的角色如图2-1所示。

生产者组:一个逻辑概念,在使用生产者实例的时候需要指定一 个组名。一个生产者组可以生产多个Topic的消息。

生产者实例: 一个生产者组部署了多个进程,每个进程都可以称 为一个生产者实例。
Topic: 主题名字,一个Topic由若干Queue组成。 RocketMQ 客 户 端 中 的 生 产 者 有 两 个 独 立
实 现 类 : org.apache.rocketmq.client.producer.DefaultMQProducer 和
org.apache.rocketmq.client.producer.TransactionMQProducer 。 前 者用于生产普通消息、顺序消息、单向消息、批量消息、延迟消息,后者主要用于生产事务消息。
2.1.2 消息结构和消息类型
消息类的核心字段定义如下:

Topic: 主题名字,可以通过RocketMQ Console创建。
Flag: 目前没用。
Properties :消息扩展信息,Tag、keys、延迟级别都保存在这 里。
Body :消息体,字节数组。需要注意生产者使用什么编码,消费者 也必须使用相同编码解码,否则会产生乱码。setKeys ( ) : 设 置 消 息 的 key , 多 个 key 可 以 用MessageConst.KEY_SEPARATOR(空格)分隔或者直接用另一个重载方 法。如果 Broker 中 messageIndexEnable=true 则会根据 key创建消 息的Hash索引,帮助用户进行快速查询。
setTags(): 消息过滤的标记,用户可以订阅某个Topic的某些 Tag,这样Broker只会把订阅了topic-tag的消息发送给消费者。
setDelayTimeLevel() :设置延迟级别,延迟多久消费者可以消 费。
putUserProperty(): 如果还有其他扩展信息,可以存放在这 里。内部是一个Map,重复调用会覆盖旧值。
RocketMQ支持普通消息、分区有序消息、全局有序消息、延迟消息 和事务消息。 普通消息:普通消息也称为并发消息,和传统的队列相比,并发 消息没有顺序,但是生产消费都是并行进行的,单机性能可达十万级别 的TPS。
分区有序消息 :与Kafka中的分区类似,把一个Topic消息分为多 个分区“保存”和消费,在一个分区内的消息就是传统的队列,遵循 FIFO(先进先出)原则。 全局有序消息:如果把一个 Topic 的分区数设置为 1,那么该 Topic 中的消息就是单分区,所有消息都遵循FIFO(先进先出)的原 则。延迟消息: 消息发送后,消费者要在一定时间后,或者指定某个 时间点才可以消费。在没有延迟消息时,基本的做法是基于定时计划任 务调度,定时发送消息。在 RocketMQ中只需要在发送消息时设置延迟 级别即可实现。
事务消息: 主要涉及分布式事务,即需要保证在多个操作同时成 功或者同时失败时,消费者才能消费消息。RocketMQ通过发送Half消息、处理本地事务、提交(Commit)消息或者回滚(Rollback)消息优 雅地实现分布式事务。
 
2.1.3 生产者高可用
通常,我们希望不管Broker、Namesrv出现什么情况,发送消息都 不要出现未知状态或者消息丢失。在消息发送的过程中,客户端、 Broker、Namesrv 都有可能发生服务器损坏、掉电等各种故障。当这些 故障发生时,RocketMQ是怎么处理的呢?
1.客户端保证
第一种保证机制 :重试机制。RocketMQ 支持同步、异步发送,不 管哪种方式都可以在配置失败后重试,如果单个 Broker 发生故障,重 试会选择其他 Broker 保证消息正常发送。 配置项 retryTimesWhenSendFailed表示同步重试次数,默认为 2 次,加上正常发送 1次,总共3次机会。
同 步 发 送 的 重 试 代 码 可 以 参 考 org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(),每次发送失败后,除非发送被打断否则都会执 行重试代码。同步发送重试代码如下:

异 步 发 送 重 试 代 码 可 以 参 考
org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessageAsync(),具体代码如下:

重试是在通信层异步发送完成的,当operationComplete()方法 返回的response值为null时,会重新执行重试代码。返回值 response 为 null 通常是因为客户端收到 TCP请求解包失败,或者没有找到匹配 的request。 生产者配置项 retryTimesWhenSendAsyncFailed 表示异步重试的 次数,默认为 2 次,加上正常发送的1次,总共有3次发送机会。
第二种保证机制 :客户端容错。RocketMQ Client会维护一个 “Broker-发送延迟”关系,根据这个关系选择一个发送延迟级别较低 的 Broker 来发送消息,这样能最大限度地利用 Broker 的能力,剔除 已经宕机、不可用或者发送延迟级别较高的 Broker,尽量保证消息的 正常发送。 这种机制主要体现在发送消息时如何选择 Queue,源代码在 sendDefaultImpl()方法调用的selectOneMessageQueue()方法中, 我们分两段来讲。
第一段代码如下:

sendLatencyFaultEnable:发送延迟容错开关,默认为关闭,如 果开关打开了,会触发发送延迟容错机制来选择发送Queue。

发送Queue时如何选择呢?
第一步:获取一个在延迟上可以接受,并且和上次发送相同的 Broker。首先获取一个自增序号 index,通过取模获取Queue的位置下标 Pos。如果 Pos对应的 Broker的延迟时间是可以接受的,并且是第 一次发送,或者和上次发送的Broker相同,则将Queue返回。
第二步:如果第一步没有选中一个Broker,则选择一个延迟较低的 Broker。
第三步:如果第一、二步都没有选中一个Broker,则随机选择一个 Broker。
第 二 段 代 码 主 要 包 括 一 个 随 机 选 择 方 法 tpInfo.selectOneMessageQueue (lastBrokerName),该方法的功能 就是随机选择一个Broker,具体实现代码如下:

上面这段代码标注了三个步骤,分别解释如下:
第一步:如果没有上次使用的Broker作为参考,那么随机选择一个 Broker。第二步:如果存在上次使用的Broker,就选择非上次使用的 Broker,目的是均匀地分散Broker的压力。
第三步:如果第一、二步都没有选中一个Broker,则采用兜底方案 ——随机选择一个Broker。
在执行如上两段代码时,需要 Broker 和发送延迟的数据作为判断 的依据,这些数据是怎么来的呢? 客户端在发送消息后,会调用 updateFaultItem()方法来更新当 前 接 收 消 息 的 Broker 的 延 迟 情 况 , 这 些 主 要 逻 辑 都 在 MQFaultStrategy 类 中 实 现 , 延 迟 策 略 有 一 个 标 准 接 口 LatencyFaultTolerance,如果读者想要自己实现一种延迟策略,可以 通过这个接口来实现。
2.Broker端保证
数据同步方式保证:在后面 Broker章节中会讲到 Broker主从复制 分为两种:同步复制和异步复制。同步复制是指消息发送到Master Broker后,同步到Slave Broker才算发送成功;异步复制是指消息发送 到Master Broker,即为发送成功。在生产环境中,建议至少部署2个
Master和2个Slave,下面分为几种情况详细描述。
(1)1个Slave掉电。Broker同步复制时,生产第一次发送失败, 重试到另一组Broker后成功;Broker异步复制时,生产正常不受影响。
(2)2个 Slave掉电。Broker同步复制时,生产失败;Broker异步 复制时,生产正常不受影响。(3)1 个 Master 掉电。Broker 同步复制时,生产第一次失败, 重试到另一组 Broker后成功;Broker异步复制时的做法与同步复制相 同。
(4)2个Master掉电。全部生产失败。
(5)同一组Master和Slave掉电。Broker同步复制时,生产第一次 发送失败,重试到另一组Broker后成功;Broker异步复制时,生产正常 不受影响。
(6)2组机器都掉电:全部生产失败。
综上所述,想要做到绝对的高可靠,将 Broker 配置的主从同步进 行复制即可,只要生产者收到消息保存成功的反馈,消息就肯定不会丢 失。一般适用于金融领域的特殊场景。绝大部分场景都可以配置Broker 主从异步复制,这样效率极高。

2.2 生产者启动流程
DefaultMQProducer 是 RocketMQ 中 默 认 的 生 产 者 实 现 , DefaultMQProducer的类之间的继承关系如图2-2所示,可以看到这个生 产者在实现时包含生产者的操作和配置属性,这是典型的类对象设计。
下面我们将介绍类对象的一些核心属性和方法。 以下是一些核心属性:
namesrvAddr: 继承自 ClientConfig,表示 RocketMQ 集群的 Namesrv 地址,如果是多个则用分号分开。比如:127.0.0.1:9876; 127.0.0.2:9876。 clientIP:使用的客户端程序所在机器的 IP地址。支持 IPv4和 Pv6,IPv4 排除了本地的环回地址(127.0.xxx.xxx)和私有内网地址
(192.168.xxx.xxx)。这里需要注意的是,如果 Client 运行在 Docker 容器中,获取的 IP 地址是容器所在的 IP 地址,而非宿主机 的IP地址。

我们会在同一个机器上部署多个程序进程,如果名字有重复就会导致
启动失败。
vipChannelEnabled: 这是一个 boolean 值,表示是否开启 VIP 通道。VIP 通道和非VIP通道的区别是:在通信过程中使用的端口号不 同。
clientCallbackExecutorThreads: 客户端回调线程数。该参数 表 示 Netty 通 信 层 回 调 线 程 的 个 数 , 默 认 值 Runtime.getRuntime).availableProcessors()表示当前CPU的有 效个数。
pollNameServerInterval:获取 Topic 路由信息的间隔时长,单 位为 ms,默认为30 000ms。
heartbeatBrokerInterval:与Broker心跳间隔的时长,单位为 ms,默认为30 000ms。
defaultMQProducerImpl: 默认生产者的实现类,其中封装了
Broker的各种API(启动及关闭生产者的接口)。如果你想自己实现一 个生产者,可以添加一个新的实现,保持DefaultMQProducer对外接口 不变,用户完全没有感知。
producerGroup: 生 产 者 组 名 , 这 是 一 个 必 须 传 递 的 参 数 。
RocketMQ-way表示同一个生产者组中的生产者实例行为需要一致。
sendMsgTimeout: 发送超时时间,单位为ms。
compressMsgBodyOverHowmuch: 消息体的容量上限,超过该上限 时消息体会通过ZIP进行压缩,该值默认为4MB。该参数在Client中是如 何生效的呢?具体实现代码如下:

retryTimesWhenSendFailed: 同步发送失败后重试的次数。默认 为2次,也就是说,一共有3次发送机会。
retryTimesWhenSendAsyncFailed: 异步发送失败后重试的次 数。默认为 2次。异步重试是有条件的重试,并不是每次发送失败后都 重 试 。 源 代 码 可 以 查 看
org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessageAsy nc ( ) 方 法 。 每 次 发 送 失 败 抛 出 异 常 后 , 通 过 执 行
onExceptionImpl()方法来决定什么场景进行重试。 以下是一些核心方法:
start(): 这是启动整个生产者实例的入口,主要负责校验生产 者的配置参数是否正确,并启动通信通道、各种定时计划任务、Pull服 务、Rebalance服务、注册生产者到Broker等操作。
shutdown(): 关闭本地已注册的生产者,关闭已注册到Broker 的客户端。fetchPublishMessageQueues(Topic): 获取一个Topic有哪些
Queue。在发送消息、Pull消息时都需要调用。
send(Message msg): 同步发送普通消息。
send(Message msg,long timeout): 同步发送普通消息(超时 设置)。
send(Message msg,SendCallback sendCallback): 异步发送 普通消息。
send ( Message msg , SendCallback sendCallback , long
timeout): 异步发送普通消息(超时设置)。
sendOneway(Message msg): 发送单向消息。只负责发送消息, 不管发送结果。
send(Message msg,MessageQueue mq): 同步向指定队列发送 消息。
send(Message msg,MessageQueue mq,long timeout): 同步 向指定队列发送消息(超时设置)。 同步向指定队列发送消息时,如果只有一个发送线程,在发送到某
个指定队列中时,这个指定队列中的消息是有顺序的,那么就按照发送 时间排序;如果某个Topic的队列都是这种情况,那么我们称该Topic的 全部消息是分区有序的。send ( Message msg , MessageQueue mq , SendCallback sendCallback):异步发送消息到指定队列。
send ( Message msg , MessageQueue mq , SendCallback
sendCallback,long timeout): 异步发送消息到指定队列(超时设 置)。
send(Message msg,MessageQueueSelector selector,Object
arg,SendCallback sendCallback): 自定义消息发送到指定队列。
通过实现MessageQueueSelector接口来选择将消息发送到哪个队列。
send(Collection<Message>msgs): 批量发送消息。
下面介绍两个核心管理接口:
createTopic ( String key , String newTopic , int queueNum):创建Topic。
viewMessage(String offsetMsgId): 根据消息id查询消息内 容。
生产者启动的流程比消费者启动的流程更加简单,一般用户使用 DefaultMQProducer的构造函数构造一个生产者实例,并设置各种参 数。比如Namesrv地址、生产者组名等,调用start()方法启动生产者 实例,start()方法调用了生产者默认实现类的start()方法启动, 这里我们主要讲实现类的start()方法内部是怎么实现的,其流程如图2-3所示。

MQClientInstance 实例的功能是管理本实例中全部生产者与消费 者 的 生 产 和 消 费 行 为 。 下 面 我 们 来 看 一 下 org.apache.rocketmq.client.impl.factory.MQClientInstance 类 的 核心属性,具体代码(篇幅原因,删去了初始化代码)如下:

下面给大家解读这段代码:
producerTable: 当前client实例的全部生产者的内部实例。
consumerTable :当前client实例的全部消费者的内部实例。
adminExtTable: 当前client实例的全部管理实例。
mQClientAPIImpl: 其实每个client也是一个Netty Server,也会 支持Broker访问,这里实现了全部client支持的接口。
mQAdminImpl: 管理接口的本地实现类。
topicRouteTable: 当前生产者、消费者中全部Topic的本地缓存 路由信息。
scheduledExecutorService: 本地定时任务,比如定期获取当前 Namesrv 地址、定期同步Namesrv信息、定期更新Topic路由信息、定期 发送心跳信息给Broker、定期清理已下线Broker、定期持久化消费位点、定期调整消费线程数(这部分源代码被官方删除了)。
clientRemotingProcessor : 请 求 的 处 理 器 , 从 处 理 方 法
processRequest()中我们可以知道目前支持哪些功能接口。
pullMessageService: Pull服务。 这里为什么会启动用于消费的Pull服务呢?这是一个兼容写法。通 过查看源代码运行过程,读者就会发现Pull服务是由一个状态变量方法 this.isStopped()控制的,这个stopped状态变量默认是False,而pullRequestQueue 也 是 空 的 , 所 以 这 里 只 是 启 动 了 pullMessageService,并没有真正地执行Pull操作,相关代码如下:

rebalanceService :重新平衡服务。定期执行重新平衡方法 this.mqClientFactory.doRebalance()。这里的 mqClientFactory 就是 MQClientInstance 实例,通过依次调用MQClientInstance中保存 的消费者实例的doRebalance()方法,来感知订阅关系的变化、集群 变化等,以达到重新平衡。 consumerStatsManager : 消 费 监 控 。 比 如 拉 取 RT ( Response Time,响应时间)、拉取TPS(Transactions Per Second,每秒处理消 息数)、消费RT等都可以统计。
MQClientInstance中还有一些核心方法如下:

 下面对这些方法逐一进行讲解:

updateTopicRouteInfoFromNameServer: 从多个Namesrv中获取 最新Topic路由信息,更新本地缓存。
cleanOfflineBroker: 清理已经下线的Broker。
checkClientInBroker: 检查Client是否在Broker中有效。 sendHeartbeatToAllBrokerWithLock: 发送客户端的心跳信息给 所有的Broker。
registerConsumer: 在本地注册一个消费者。
unregisterConsumer: 取消本地注册的消费者。
registerProducer: 在本地注册一个生产者。
unregisterProducer: 取消本地注册的生产者。
registerAdminExt: 注册一个管理实例。
rebalanceImmediately :立即执行一次 Rebalance。该操作是通 过 RocketMQ 的一个CountDownLatch2锁来实现的。
doRebalance : 对 于 所 有 已 经 注 册 的 消 费 者 实 例 , 执 行 一 次 Rebalance。
findBrokerAddressInAdmin :在本地缓存中查找Master或者Slave Broker信息。
findBrokerAddressInSubscribe : 在 本 地 缓 存 中 查 找 Slave Broker信息。
findBrokerAddressInPublish :在本地缓存中查找Master Broker 地址。
findConsumerIdList: 查找消费者id列表。 findBrokerAddrByTopic :通过Topic名字查找Broker址。
resetOffset: 重置消费位点。
getConsumerStatus: 获取一个订阅关系中每个队列的消费进度。
getTopicRouteTable: 获取本地缓存Topic路由。
consumeMessageDirectly: 直接将消息发送给指定的消费者消费,和正常投递不同的是,指定了已经订阅的消费者组中的一个,而不 是全部已经订阅的消费者。一般适用于在消费消息后,某个消费者组 想再消费一次的场景。
consumerRunningInfo: 获取消费者的消费统计信息。包含消费 RT、消费TPS等。
 
2.3 消息发送流程
RocketMQ客户端的消息发送通常分为以下3层:
业务层 :通常指直接调用RocketMQ Client发送API的业务代码。 消息处理层:指RocketMQ Client获取业务发送的消息对象后,一 系列的参数检查、消息发送准备、参数包装等操作。
通信层 :指RocketMQ基于Netty封装的一个RPC通信服务,RocketMQ 的各个组件之间的通信全部使用该通信层。
总体上讲,消息发送流程首先是 RocketMQ 客户端接收业务层消 息,然后通DefaultMQProducerImpl发送一个RPC请求给Broker,再由 Broker 处 理 请 求 并 保 存 消 息 。 下 面 以 DefaultMQProducer.send(Message msg)接口为例讲解发送流程,如
图2-4所示。
消息发送流程具体分为3步:
第一步:调用defaultMQProducerImpl.send()方法发送消息。
第 二 步 : 通 过 设 置 的 发 送 超 时 时 间 , 调 用 defaultMQProducerImpl.send()方法发送消息。设置的超时时间可以 通过sendMsgTimeout进行变更,其默认值为3s。
第 三 步 : 执 行 defaultMQProducerImpl.sendDefaultImpl ( ) 方 法。这是一个公共发送方法,我们先看看入参:

communicationMode:通信模式,同步、异步还是单向。
sendCallback:对于异步模式,需要设置发送完成后的回调。
该方法是发送消息的核心方法,执行过程分为5步: 第一步,两个检查:生产者状态、消息及消息内容。没有运行的生 产者不能发送消息。消息检查主要检查消息是否为空,消息的Topic的
名字是否为空或者是否符合规范;消息体大小是否符合要求,最大值为 4MB,可以通过maxMessageSize进行设置。
第二步,执行tryToFindTopicPublishInfo()方法:获取Topic路 由信息,如果不存在则发出异常提醒用户。如果本地缓存没有路由信 息,就通过Namesrv获取路由信息,更新到本地,再返回。体实现代 码如下:
第三步,计算消息发送的重试次数,同步重试和异步重试的执行方
式是不同的。
第四步,执行队列选择方法selectOneMessageQueue()。根据队 列对象中保存的上次发送消息的Broker的名字和Topic路由,选择(轮 询 ) 一 个 Queue 将 消 息 发 送 到 Broker 。 我 们 可 以 通 过
sendLatencyFaultEnable 来设置是否总是发送到延迟级别较低的
Broker,默认值为False。 第五步,执行sendKernelImpl()方法。该方法是发送消息的核心方法,主要用于准备通信层的入参(比如Broker地址、请求体等),将请求传递给通信层,内部实现是基于Netty的,在封装为通信层request对象RemotingCommand前,会设置RequestCode表示当前请求是发送单个消息还是批量消息。具体实现代码如下:

Netty 本身是一个异步的网络通信框架,怎么实现同步的调用呢? 我 们 可 以 通 过
org.apache.rocketmq.remoting.netty.NettyRemotingAbstract.invokeSyncImpl()方法来实现同步的调用,具体实现代码如下:

在每次发送同步请求后,程序会执行 waitResponse()方法,直 到 Netty接收 Broker的返回结果,相关代码如下:

然后,通过putResponse()方法释放锁,让请求线程同步返回。 异步发送时有很多request,每个response返回后怎么与request进 行对应呢?这里面有一个关键参数——opaque,RocketMQ每次发送同步 请求前都会为一个request分配一个opaque,这是一个原子自增的id, 一个response会以opaque作为key保存在responseTable中,这样用 opaque就将request和response连接起来了。 无论请求发送成功与否,都执行 updateFaultItem()方法,这就 是在第三步中讲的总是发送到延迟级别较低的Broker的逻辑。

2.4.2 发送顺序消息
同步发送消息时,根据HashKey将消息发送到指定的分区中,每个 分区中的消息都是按照发送顺序保存的,即分区有序。如果 Topic 的 分区被设置为 1,这个 Topic 的消息就是全局有序的。注意,顺序消 息的发送必须是单线程,多线程将不再有序。顺序消息的消费和普通消 息的消费方式不同,后面会详细讲解。
下面来看一下发送顺序消息的实现代码:

2.4.3 发送延迟消息
生产者发送消息后,消费者在指定时间才能消费消息,这类消息被 称为延迟消息或定时消息。生产者发送延迟消息前需要设置延迟级别, 目前开源版本支持18个延迟级别:Broker在接收用户发送的消息后,首 先将消息保存到名为SCHEDULE_TOPIC_XXXX的Topic中。此时,消费者无
法消费该延迟消息。然后,由Broker端的定时投递任务定时投递给消费 者。
保 存 延 迟 消 息 的 实 现 逻 辑 见 org.apache.rocketmq.store.schedule.ScheduleMessageService 类。
按照配置的延迟级别初始化多个任务,每秒执行一次。如果消息投递满 足时间条件,那么将消息递到原始的Topic中。消费者此时可以消费 该延迟消息。 生产者代码中怎么设置延迟级别呢?相关代码如下:

 

2.4.4 发送事务消息
事务消息的发送、消费流程和延迟消息类似,都是先发送到对消费 者不可见的 Topic中。当事务消息被生产者提交后,会被二次投递到原 始Topic中,此时消费者正常消费。事务消息的发送具体分为以下两个
步骤。
第 一 步 : 用 户 发 送 一 个 Half 消 息 到 Broker , Broker 设 置 queueOffset=0,即对消费者不可见。
第二步:用户本地事务处理成功,发送一个 Commit 消息到 Broker,Broker 修改queueOffset为正常值,达到重新投递的目的,此 时消费者可以正常消费;如果本地事务处理失败,那么将发送一个 Rollback消息给Broker,Broker将删除Half消息,如图2-5所示。 有读者可能会有疑问:如果生产者忘记了提交或回滚,那么 Broker怎么处理 Half消息呢? Broker会定期回查生产者,确认生产者本地事务的执行状态,再决 定是提交、回滚还是删除Half消息。

 

2.4.5 发送单向消息
单向消息的生产者只管发送过程,不管发送结果。单项消息主要用
于日志传输等消息允许丢失的场景,常用的发送代码如下:

2.4.6 批量消息发送
批量消息发送能提高发送效率,提升系统吞吐量。批量消息发送有
以下3点注意事项:
(1)消息最好小于1MB。
(2)同一批批量消息的Topic、waitStoreMsgOK属性必须一致。
(3)批量消息不支持延迟消息。
批量发送实现代码如下:

2.5 生产者最佳实践总结
相对消费者而言,生产者的使用更加简单,一般读者主要关注消息 类型、消息发送方法和发送参数,即可正常使用RocketMQ发送消息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值