RocketMQ Learning(一)

目录

一、RocketMQ

1、RocketMQ的产品发展

2、消息中间件

RocketMQ各部分角色介绍

使用RocketMQ的核心概念

3、RocketMQ安装

3.1、windows下的安装

3.2、Linux下的安装

3.3、源码的安装

3.4、控制台

4、消息发送方式

4.1、发送同步消息

4.2、发送异步消息

4.3、单向发送

5、消息消费方式

5.1、负载均衡模式(集群消费)

5.2、广播消费


一、RocketMQ

1、RocketMQ的产品发展

        MetaQ:2011年,阿里基于Kafka的设计使用Java完全重写并推出了MetaQ 1.0版本 。
        2012年,阿里对MetaQ的存储进行了改进,推出MetaQ 2.0,同年阿里把Meta2.0从阿里内部开源出来,取名RocketMQ,为了命名上的规范以及版本上的延续,对外称为RocketMQ3.0。
        2016年,阿里宣布将开源分布式消息中间件RocketMQ捐赠给Apache,同时RocketMQ3也升级为RocketMQ4,现在RocketMQ主要维护的是4.x的版本,也是大家使用得最多的版本。
        2021年,RocketMQ在github上发布5.0预览版。RocketMQ 5.0定义为云原生的消息、事件、流的超融合平台。

RocketMQ源码链接

RocketMQ官网下载地址

2、消息中间件

      消息中间件,英文Message Queue,简称MQ。它没有标准定义,一般认为:消息中间件属于分布式系统中一个子系统,关注于数据的发送和接收,利用高效可靠的异步消息传递机制对分布式系统中的其余各个子系统进行集成。
        高效:对于消息的处理处理速度快,RocketMQ可以达到单机10万+的并发
        可靠:一般消息中间件都会有消息持久化机制和其他的机制确保消息不丢失。
        异步:指发送完一个请求,不需要等待返回,随时可以再发送下一个请求,既不需要等待。
一句话总结:消息中间件不生产消息,只是消息的搬运工。

为什么要用消息中间件?

1.应用解耦
2.流量削峰
3.数据分发

RocketMQ各部分角色介绍

NameServer
        NameServer是整个RocketMQ的“大脑”,它是RocketMQ的服务注册中心,所以RocketMQ需要先启动NameServer再启动Rocket中的Broker。
        Broker在启动时向所有NameServer注册(主要是服务器地址等),生产者在发送消息之前先从NameServer获取Broker服务器地址列表(消费者一样),然后根据负载均衡算法从列表中选择一台服务器进行消息发送。
主机(Broker)
        RocketMQ的核心,用于暂存和传输消息。
生产者(Producer)
        生产者:也称为消息发布者,负责生产并发送消息至RocketMQ。
消费者(Consumer)
        消费者:也称为消息订阅者,负责从RocketMQ接收并消费消息。
消息(Message)
        消息:生产或消费的数据,对于RocketMQ来说,消息就是字节数组

使用RocketMQ的核心概念

主题(Topic)
        标识RocketMQ中一类消息的逻辑名字,消息的逻辑管理单位。无论消息生产还是消费,都需要指定Topic。主题主要用于区分消息的种类:一个生产者可以发送消息给一个或者多个Topic,消息的消费者也可以订阅一个或者多个Topic消息。
消息队列(Message Queue)
        简称Queue或Q。消息物理管理单位。一个Topic将有若干个Q。
        无论生产者还是消费者,实际的生产和消费都是针对Q级别。例如Producer发送消息的时候,会预先选择(默认轮询)好该Topic下面的某一条Q发送;Consumer消费的时候也会负载均衡地分配若干个Q,只拉取对应Q的消息。
        若一个Topic创建在不同的Broker,则不同的broker上都有若干Q,消息将物理地存储落在不同Broker结点上,具有水平扩展的能力。
分组(Group)
        生产者:标识发送同一类消息的Producer,通常发送逻辑一致。发送普通消息的时候,仅标识使用,并无特别用处。主要作用用于事务消息:
        消费者:标识一类Consumer的集合名称,这类Consumer通常消费一类消息(也称为Consumer Group),且消费逻辑一致。同一个Consumer Group下的各个实例将共同消费topic的消息,起到负载均衡的作用。
标签(Tag)
        RocketMQ支持给在发送的时候给消息打tag,同一个topic的消息虽然逻辑管理是一样的。但是消费同一个topic时,如果你消费订阅的时候指定的是tagA,那么tagB的消息将不会投递。
偏移量(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位置

3、RocketMQ安装

3.1、windows下的安装

1.确保安装好了JDK1.8&64位系统

2.解压运行版本(Binary)

3.配置环境变量

变量名:ROCKETMQ_HOME
变量值:MQ解压路径\MQ文件夹名

4.启动

在RocketMQ的架构中,都是需要先启动NameServer再启动Broker的。所以先启动NameServer。
启动NameServer
使用cmd命令框执行进入至‘MQ文件夹\bin’下,然后执行‘start mqnamesrv.cmd’,启动NameServer。成功后会弹出提示框,此框勿关闭。

启动Broker
使用cmd命令框执行进入至‘MQ文件夹\bin’下,然后执行‘start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true’,启动Broker。成功后会弹出提示框,此框勿关闭。

注意事项

弹出提示框‘错误: 找不到或无法加载主类 Files\Java\jdk1.8.0_202\lib\tools.jar;C:\Program’的处理
打开‘MQ文件夹\bin’下的runbroker.cmd,然后将‘%CLASSPATH%’加上英文双引号。保存并重新执行start语句。

 再次启动

3.2、Linux下的安装

...

3.3、源码的安装

1.解压源码版本(Source)

2.导入idea中,maven编译通过

3.创建目录D盘创建文件夹RocketMQ

        1.把源码下的distribution下的conf文件夹拷贝到RocketMQ下面

        2.再创建logs和store文件夹

 

4、启动RocketMQ源码

4.1、启动NameServer

namesrv工程下NamesrvStartup启动类,启动前需要配置环境变量

ROCKETMQ_HOME=D:\RocketMQ

4.2、启动Broker
在broker模块找到broker模块,同时找到启动类BrokerStartup.java

需要修改配置文件broker.conf

#配置如下:
#nameServer
namesrvAddr=127.0.0.1:9876
autoCreateTopicEnable = true
storePathRootDir = D:\\RocketMQ\\store
#commitLog存储路径
storePathCommitLog = D:\\RocketMQ\\store\\commitlog
#消费队列存储路径
storePathConsumeQueue =D:\\RocketMQ\\store\\consumequeue
#消息索引存储路径
storePathindex = D:\\RocketMQ\\store\\index
#checkpoint文件存储路径
storeCheckpoint = D:\\RocketMQ\\store\\checkpoint
#abort文件存储路径
abortFile = D:\\RocketMQ\\store\\abort

配置环境变量

ROCKETMQ_HOME=D:\RocketMQ

配置参数

-c D:\RocketMQ\conf\broker.conf

 

 启动成功,检查下数据文件

3.4、控制台

Rocketmq老版本下载

Rocketmq新版本下载

这里下载的新版本rocketmq-dashboard启动项目下面如下

浏览器中输入‘http://localhost:8080’,成功后即可进行管理端查看。

运维页面
你可以修改这个服务使用的namesrv的地址
你可以修改这个服务是否使用VIPChannel(如果你的mq server版本小于3.5.8,请设置不使用)

驾驶舱
查看broker的消息量(总量/5分钟图)
查看单一主题的消息量(总量/趋势图)

集群
查看集群的分布情况:cluster与broker关系、broker
查看broker具体信息/运行信息
查看broker配置信息

 

主题页面
展示所有的主题,可以通过搜索框进行过滤
筛选 普通/重试/死信 主题
添加/更新主题
        clusterName 创建在哪几个cluster上
        brokerName 创建在哪几个broker上
        topicName 主题名
        writeQueueNums 写队列数量
        readQueueNums 读队列数量
        perm //2是写 4是读 6是读写
状态 查询消息投递状态(投递到哪些broker/哪些queue/多少量等)
路由 查看消息的路由(现在你发这个主题的消息会发往哪些broker,对应broker的queue信息)
CONSUMER管理(这个topic都被哪些group消费了,消费情况何如)
topic配置(查看变更当前的配置)
发送消息(向这个主题发送一个测试消息)
重置消费位点(分为在线和不在线两种情况,不过都需要检查重置是否成功)
删除主题 (会删除掉所有broker以及namesrv上的主题配置和路由信息)

消费者页面
展示所有的消费组,可以通过搜索框进行过滤
刷新页面/每隔五秒定时刷新页面
按照订阅组/数量/TPS/延迟 进行排序
添加/更新消费组
        clusterName 创建在哪几个集群上
        brokerName 创建在哪几个broker上
        groupName 消费组名字
        consumeEnable //是否可以消费 FALSE的话将无法进行消费
        consumeBroadcastEnable //是否可以广播消费
        retryQueueNums //重试队列的大小
        brokerId //正常情况从哪消费
        whichBrokerWhenConsumeSlowly//出问题了从哪消费
终端 在线的消费客户端查看,包括版本订阅信息和消费模式
消费详情 对应消费组的消费明细查看,这个消费组订阅的所有Topic的消费情况,每个queue对应的消费client查看(包括Retry消息)
配置 查看变更消费组的配置
删除 在指定的broker上删除消费组

生产者页面
通过Topic和Group查询在线的消息生产者客户端
信息包含客户端主机 版本

消息查询页面
根据Topic和时间区间查询由于数据量大 最多只会展示2000条,多的会被忽略
根据Topic和Key进行查询
最多只会展示64条
根据消息主题和消息Id进行消息的查询
消息详情可以展示这条消息的详细信息,查看消息对应到具体消费组的消费情况(如果异常,可以查看具体的异常信息)。可以向指定的消费组重发消息

4、消息发送方式

4.1、发送同步消息

同步发送是指消息发送方发出数据后,同步等待,直到收到接收方发回响应之后才发下一个请求。这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。

RocketMQ源码中的example模块的org.apache.rocketmq.example.quickstart.Producer

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;

public class Producer {
    public static final int MESSAGE_COUNT = 1000;
    public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
    public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
    public static final String TOPIC = "TopicTest";
    public static final String TAG = "TagA";

    public static void main(String[] args) throws MQClientException, InterruptedException {

        DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
        producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
        producer.start();

        for (int i = 0; i < MESSAGE_COUNT; i++) {
            try {
                Message msg = new Message(TOPIC /* Topic */,
                    TAG /* Tag */,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
                );
                SendResult sendResult = producer.send(msg);
               
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.sleep(1000);
            }
        }
        producer.shutdown();
    }
}

发送结果分析

SendResult [sendStatus=SEND_OK, msgId=C0A800AB34BC18B4AAC228E6CFB90000, offsetMsgId=C0A8380100002A9F0000000000046F14, messageQueue=MessageQueue [topic=TopicTest, brokerName=MS-TGOOFNKABBOB, queueId=3], queueOffset=275]
SendResult [sendStatus=SEND_OK, msgId=C0A800AB34BC18B4AAC228E6CFCD0001, offsetMsgId=C0A8380100002A9F0000000000047003, messageQueue=MessageQueue [topic=TopicTest, brokerName=MS-TGOOFNKABBOB, queueId=0], queueOffset=275]
SendResult [sendStatus=SEND_OK, msgId=C0A800AB34BC18B4AAC228E6CFD10002, offsetMsgId=C0A8380100002A9F00000000000470F2, messageQueue=MessageQueue [topic=TopicTest, brokerName=MS-TGOOFNKABBOB, queueId=1], queueOffset=275]
SendResult [sendStatus=SEND_OK, msgId=C0A800AB34BC18B4AAC228E6CFD30003, offsetMsgId=C0A8380100002A9F00000000000471E1, messageQueue=MessageQueue [topic=TopicTest, brokerName=MS-TGOOFNKABBOB, queueId=2], queueOffset=275]
SendResult [sendStatus=SEND_OK, msgId=C0A800AB34BC18B4AAC228E6CFD50004, offsetMsgId=C0A8380100002A9F00000000000472D0, messageQueue=MessageQueue [topic=TopicTest, brokerName=MS-TGOOFNKABBOB, queueId=3], queueOffset=276]
SendResult [sendStatus=SEND_OK, msgId=C0A800AB34BC18B4AAC228E6CFD60005, offsetMsgId=C0A8380100002A9F00000000000473BF, messageQueue=MessageQueue [topic=TopicTest, brokerName=MS-TGOOFNKABBOB, queueId=0], queueOffset=276]
SendResult [sendStatus=SEND_OK, msgId=C0A800AB34BC18B4AAC228E6CFD90006, offsetMsgId=C0A8380100002A9F00000000000474AE, messageQueue=MessageQueue [topic=TopicTest, brokerName=MS-TGOOFNKABBOB, queueId=1], queueOffset=276
....

msgId
消息的全局唯一标识(RocketMQ的ID生成是使用机器IP和消息偏移量的组成),由消息队列 MQ 系统自动生成,唯一标识某条消息。
sendStatus
发送的标识:成功,失败等
queueId
queueId是Topic的分区;Producer发送具体一条消息的时,对应选择的该Topic下的某一个Queue的标识ID。
queueOffset
Message queue是无限长的数组。一条消息进来下标就会涨1,而这个数组的下标就是queueOffset,queueOffset是从0开始递增。

4.2、发送异步消息

        异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。消息发送方在发送了一条消息后,不等接收方发回响应,接着进行第二条消息发送。发送方通过回调接口的方式接收服务器响应,并对响应结果进行处理。

RocketMQ源码中的example模块的org.apache.rocketmq.example.quickstart.Producer

package org.apache.rocketmq.example.simple;

import java.io.UnsupportedEncodingException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;

public class AsyncProducer {
    public static void main(
        String[] args) throws MQClientException, InterruptedException, UnsupportedEncodingException {

        DefaultMQProducer producer = new DefaultMQProducer("Jodie_Daily_test");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
        // suggest to on enableBackpressureForAsyncMode in heavy traffic, default is false
        producer.setEnableBackpressureForAsyncMode(true);
        producer.setRetryTimesWhenSendAsyncFailed(0);

        int messageCount = 100;
        final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
        for (int i = 0; i < messageCount; i++) {
            try {
                final int index = i;
                Message msg = new Message("Jodie_topic_1023",
                    "TagA",
                    "OrderID188",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        countDownLatch.countDown();
                        System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
                    }

                    @Override
                    public void onException(Throwable e) {
                        countDownLatch.countDown();
                        System.out.printf("%-10d Exception %s %n", index, e);
                        e.printStackTrace();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.shutdown();
    }
}

4.3、单向发送

        这种方式主要用在不特别关心发送结果的场景,例如日志发送。单向(Oneway)发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。

package org.apache.rocketmq.example.simple;

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;

import java.nio.charset.StandardCharsets;

public class OnewayProducer {
    public static void main(String[] args) throws Exception {
        //Instantiate with a producer group name.
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // Specify name server addresses.
        producer.setNamesrvAddr("localhost:9876");
        //Launch the instance.
        producer.start();
        for (int i = 0; i < 100; i++) {
            //Create a message instance, specifying topic, tag and message body.
            Message msg = new Message("TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " +
                            i).getBytes(StandardCharsets.UTF_8) /* Message body */
            );
            //Call send message to deliver message to one of brokers.
            producer.sendOneway(msg);
        }
        //Wait for sending to complete
        Thread.sleep(5000);
        producer.shutdown();
    }
}

5、消息消费方式

5.1、负载均衡模式(集群消费)

        消费者采用负载均衡方式消费消息,一个分组(Group)下的多个消费者共同消费队列消息,每个消费者处理的消息不同。一个Consumer Group中的各个Consumer实例分摊去消费消息,即一条消息只会投递到一个Consumer Group下面的一个实例。例如某个Topic有3个队列,其中一个Consumer Group 有 3 个实例,那么每个实例只消费其中的1个队列。集群消费模式是消费者默认的消费方式。

package org.apache.rocketmq.example.quickstart;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;

public class Consumer {

    public static final String CONSUMER_GROUP = "please_rename_unique_group_name_4";
    public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
    public static final String TOPIC = "TopicTest";

    public static void main(String[] args) throws MQClientException {
        //实例化消息生产者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        //指定Namesrv地址信息
        consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //订阅Topic
        consumer.subscribe(TOPIC, "*");
        //负载均衡模式消费(可以不设置,默认就是负载均衡模式)
        consumer.setMessageModel(MessageModel.CLUSTERING);
        //注册回调函数,处理消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
            System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();//启动消费者

        System.out.printf("Consumer Started.%n");
    }
}

5.2、广播消费

        广播消费模式中消息将对一个Consumer Group下的各个Consumer实例都投递一遍。即使这些 Consumer属于同一个Consumer Group,消息也会被Consumer Group 中的每个Consumer都消费一次。实际上,是一个消费组下的每个消费者实例都获取到了topic下面的每个Message Queue去拉取消费。所以消息会投递到每个消费者实例。

package org.apache.rocketmq.example.quickstart;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel;

public class Consumer {

    public static final String CONSUMER_GROUP = "please_rename_unique_group_name_4";
    public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
    public static final String TOPIC = "TopicTest";

    public static void main(String[] args) throws MQClientException {
        //实例化消息生产者,指定组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        //指定Namesrv地址信息
        consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //订阅Topic
        consumer.subscribe(TOPIC, "*");
        //广播消费模式
        consumer.setMessageModel(MessageModel.BROADCASTING);
        //注册回调函数,处理消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
            System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();//启动消费者

        System.out.printf("Consumer Started.%n");
    }
}

消息消费时的权衡
负载均衡模式:适用场景&注意事项 
        消费端集群化部署,每条消息只需要被处理一次。
        由于消费进度在服务端维护,可靠性更高。
        集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
        集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。
广播模式:适用场景&注意事项 
        每条消息都需要被相同逻辑的多台机器处理。
        消费进度在客户端维护,出现重复的概率稍大于集群模式。
        广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。
        广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。
        广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
目前仅 Java 客户端支持广播模式。
        广播消费模式下不支持顺序消息。
        广播消费模式下不支持重置消费位点。
        广播模式下服务端不维护消费进度,所以消息队列 RocketMQ 控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。

RocketMQ Learning(二)

不是你觉的悟到的东西给了你,你也接不住!

干我们这行,啥时候懈怠,就意味着长进的停止,长进的停止就意味着被淘汰,只能往前冲,直到凤凰涅槃的一天!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

杀神lwz

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

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

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

打赏作者

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

抵扣说明:

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

余额充值