04 Producer详解


1 环境准备

  1. 新建一个moudel进行演示: 03-rocket-mq-producer
  2. 导入依赖
 <dependencies>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <!-- logback-classic包含logback-core依赖 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>
    </dependencies>

2 顺序消息

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序

顺序消费的原理解析,
在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

2.1 API介绍

  1. 发送顺序消息的接口
/****
   *  第一个参数:message 就是要发送的消息
   *  第二个参数MessageQueueSelector: 定义如何选择消息队列
   *  第三个参数:Object类型,可以理解就是业务参数,会传给MessageQueueSelector接口中select方法的第三个参数
   *  用于队列的选择
   */
  SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg)
  1. MessageQueueSelector 接口
    从名字就能看出该接口的作用消息队列选择器
public interface MessageQueueSelector {
	/***
       * List<MessageQueue> mqs: 就是消息队列的list集合,也就是topic下的消息队列的集合,默认大小是4
       * Message message: 就是要发送的消息
       * Object o: 见上面的解释
       * 返回值:就是选择的队列
       */
    MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}
  1. 消息消费的时候需要使用MessageListenerOrderly这个监听器

2.2 演示

下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。

  1. 消息生产者
package study.wyy.mq.rocket.producer;

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.slf4j.Logger;
import study.wyy.mq.rocket.model.OrderStep;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

/**
 * @author: wyaoyao
 * @date: 2020-12-25 14:14
 * @description: 顺序消息发送
 */
@Slf4j
public class OrderedMessageProducer {

    public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
        // 构造订单数据
        List<OrderStep> orderSteps = buildData();
        // 1 构建生产者
        DefaultMQProducer producer = new DefaultMQProducer("test_producer_group");
        // 2 设置name server
        producer.setNamesrvAddr("localhost:9876");
        // 3 启动
        producer.start();
        // 遍历数据发送
        for (OrderStep orderStep:orderSteps) {
            Message message = new Message("myTopic", "myTags", orderStep.toString().getBytes(Charset.defaultCharset()));
            /****
             *  第一个参数:message 就是要发送的消息
             *  第二个参数MessageQueueSelector: 定义如何选择消息队列
             *  第三个参数:Object类型,可以理解就是业务参数,会传给MessageQueueSelector接口中select方法的第三个参数
             *  用于队列的选择,比如这里就把orderId塞进去
             */
            SendResult send = producer.send(message, new MessageQueueSelector() {
                /***
                 * List<MessageQueue> mqs: 就是消息队列的list集合,也就是topic下的消息队列的集合,默认大小是4
                 * Message message: 就是要发送的消息
                 * Object o: 见上面的解释
                 * 返回值:就是选择的队列
                 */
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message message, Object o) {
                    // 强转
                    Long orderId = (Long) o;
                    // 对orderId进行取余数,余数相同的放到一个队列中
                    long index = orderId % mqs.size();
                    MessageQueue select = mqs.get((int) index);
                    log.info("队列的大小: {}; orderID: {}; select: {}",mqs.size(), orderId,select.getQueueId());
                    return select;
                }
            }, orderStep.getOrderId());
        }
        producer.shutdown();

    }

    private static List<OrderStep> buildData(){
        List<OrderStep> orderList = new ArrayList<OrderStep>();

        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(1L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);


        orderDemo = new OrderStep();
        orderDemo.setOrderId(2L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(1L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(3L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(2L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(3L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(2L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(1L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(3L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(1L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        return orderList;
    }
}

package study.wyy.mq.rocket.model;

import lombok.Data;

/**
 * @author: wyaoyao
 * @date: 2020-12-25 14:01
 * @description: 订单的流程
 */
@Data
public class OrderStep {
    private long orderId;
    private String desc;

    @Override
    public String toString() {
        return this.getOrderId() + "-->" + this.getDesc();
    }
}

  1. 消息消费
package study.wyy.mq.rocket.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * @author: wyaoyao
 * @date: 2020-12-25 15:11
 * @description:
 */
public class OrderedMessageConsumer {

    public static void main(String[] args) throws MQClientException {
        // 1 构建消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_consumer_group");
       // 2 设置 name server地址
        consumer.setNamesrvAddr("localhost:9876");
        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
         * 如果非第一次启动,那么按照上次消费的位置继续消费
         *
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        // 3 设置订阅topic
        consumer.subscribe("myTopic", "*");
        // 4 注册回调函数,注意这里注册的是顺序监听器
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);

                for (MessageExt msg : msgs) {
                    // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
                    System.out.println("consumeThread=" + Thread.currentThread().getName() + ", queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
    }
}
  1. 测试结果: 每个订单都是按照订单流程进行的
consumeThread=ConsumeMessageThread_1, queueId=1, content:1-->创建
consumeThread=ConsumeMessageThread_3, queueId=3, content:3-->创建
consumeThread=ConsumeMessageThread_2, queueId=2, content:2-->创建
consumeThread=ConsumeMessageThread_2, queueId=2, content:2-->付款
consumeThread=ConsumeMessageThread_3, queueId=3, content:3-->付款
consumeThread=ConsumeMessageThread_1, queueId=1, content:1-->付款
consumeThread=ConsumeMessageThread_3, queueId=3, content:3-->完成
consumeThread=ConsumeMessageThread_2, queueId=2, content:2-->完成
consumeThread=ConsumeMessageThread_1, queueId=1, content:1-->推送
consumeThread=ConsumeMessageThread_1, queueId=1, content:1-->完成

3 延时消息

比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。

3.1 API介绍

  1. 在构建消息的时候可以设置一个延时属性
 // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
message.setDelayTimeLevel(3);

现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级1到18

// org/apache/rocketmq/store/config/MessageStoreConfig.java
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

3.2 演示

  1. 消息生产者
package study.wyy.mq.rocket.producer;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.Charset;

/**
 * @author: wyaoyao
 * @date: 2020-12-25 15:53
 * @description: 延时消息发送
 *
 */
public class ScheduledMessageProducer {

    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // 1 创建生产者
        DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");
        // 2 设置 name server地址
        producer.setNamesrvAddr("localhost:9876");
        // 3 启动
        producer.start();
        // 4 构建消息
        Message message = new Message();
        message.setTopic("myTopic");
        message.setTags("myTags");
        // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
        message.setDelayTimeLevel(3);
        message.setBody("我的第一个延时消息".getBytes(Charset.defaultCharset()));
        producer.send(message);
        producer.shutdown();
    }
}

  1. 消费者
package study.wyy.mq.rocket.consumer;

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
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.message.MessageExt;

import java.nio.charset.Charset;
import java.util.List;

/**
 * @author: wyaoyao
 * @date: 2020-12-25 16:06
 * @description: 延时消息消费者
 */
@Slf4j
public class ScheduledMessageConsumer {

    public static void main(String[] args) throws MQClientException {
        // 1 定义消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer-group");
        // 2 设置 name server地址
        consumer.setNamesrvAddr("localhost:9876");
        // 3 设置订阅的主题
        consumer.subscribe("myTopic","*");
        // 4 设置回调函数
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.println(" ======遍历消息======= ");
                if (msgs != null && msgs.size() > 0) {
                    for (MessageExt msg : msgs) {
                        System.out.println("消息id: " + msg.getMsgId());
                        System.out.println("topic: " + msg.getTopic());
                        System.out.println("tag: " + msg.getTags());
                        System.out.println("消息体:"+ new String(msg.getBody(), Charset.defaultCharset()));
                        System.out.println("=======end===========");
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 5 启动
        consumer.start();
    }
}
  1. 测试
    • 先启动消费者等待消息
    • 启动生产者发送消息
    • 十秒后消费者收到消息

4 批量消息

批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic相同的waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB

package study.wyy.mq.rocket.producer;

import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

/**
 * @author: wyaoyao
 * @date: 2020-12-25 16:24
 * @description:
 */
public class BatchMessageProducer {
    public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
        // 1 创建生产者
        DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");
        // 2 设置 name server地址
        producer.setNamesrvAddr("localhost:9876");
        // 3 启动
        producer.start();
        // 4 构建消息
        List<Message> messages = new ArrayList<>();
        Message message = new Message();
        message.setTopic("myTopic");
        message.setTags("myTags");
        message.setBody("批量消息1".getBytes(Charset.defaultCharset()));
        messages.add(message);
        message = new Message();
        message.setTopic("myTopic");
        message.setTags("myTags");
        message.setBody("批量消息2".getBytes(Charset.defaultCharset()));
        messages.add(message);
        producer.send(messages);
        producer.shutdown();
    }
}

5 事务消息

5.1 事务消息的状态

事务消息共有三种状态,提交状态、回滚状态、中间状态:

  • TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。
  • TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
  • TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。

5.2 事务消息的流程

在这里插入图片描述
上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。

  • HalfI(prepare)Message
    指的是暂不能投递的消息发送方已经将消息成功发送到MQ服务器(消息中心),但是服务器未收到对生产者对该消息的二次确认,此时消息被标记为”暂不能投递“状态,处于状态的消息及半消息
  • Message Status Check
    由于网络闪断,生产者应用重启等原因,导致某条消息的二次确认丢失,MQ服务端会扫描发现某条消息长期处于”半消息状态“,需要主动向消息生产者询问该消息的最终状态(commit或者rollback),该过程称之消息回查

执行大致流程:

  • 发送方向MQ服务端发送消息
  • MQ Server将消息持久化成功之后,向发送方ACK确认消息已经发送成功,此时消息为半消息
  • 发送方开始执行本地事务逻辑
  • 发送方根据本地事务执行的结果,向MQ Server提交二次确认(commit或者rollback),MQ Server收到Commit状态则将半消息标记为可投递,订阅方(消费方)最终将收到该消息,Mq Server收到Rollback状态,则删除半消息,订阅方不会收到该消息
  • 在断网或者应用重启的特殊情况下,上诉步骤4的二次确认会最终未到达MQ server,经过固定的时间后,Mq server将对该消息发起消息回查
  • 发送方收到消息回查之后,需要检查对相应消息的的本地事务执行的最终结果
  • 发送方根据检查到的结果,再次提交二次确认呢,MQ Server扔按照步骤4进行半消息操作

5.3 API介绍

当发送半消息成功时,我们使用 executeLocalTransaction 方法来执行本地事务。它返回前一节中提到的三个事务状态之一。checkLocalTransaction 方法用于检查本地事务状态,并回应消息队列的检查请求。它也是返回前一节中提到的三个事务状态之一。

  1. 事务的监听接口

package org.apache.rocketmq.client.producer;

import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

public interface TransactionListener {
  
    LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);

    LocalTransactionState checkLocalTransaction(final MessageExt msg);
}
  1. 生产者要使用TransactionMQProducer ,具体使用看下面的案例

5.4 演示

  1. 事务消息的生产者
package study.wyy.mq.rocket.producer;


import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import study.wyy.mq.rocket.spi.TransactionListenerImpl;

import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

/**
 * @author: wyaoyao
 * @date: 2020-12-25 17:06
 * @description: 事务消息
 */
public class TransactionMessageProducer {
    // 记录事务的状态,key为事务的id,value为事务状态
    private static Map<String,LocalTransactionState> STATE_MAP = new HashMap<String, LocalTransactionState>();

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

        // 1 创建消息发送者,注意这里是事务消息生产者
        TransactionMQProducer producer = new TransactionMQProducer();
        // 2 设置 生产者组
        producer.setProducerGroup("test-producer-group");
        // 3 设置name sever
        producer.setNamesrvAddr("localhost:9876");
        // 4 设置事务监听器
        producer.setTransactionListener(new TransactionListener(){
            // 记录事务的状态,key为事务的id,value为事务状态
            /*****
             * 当发送半消息成功时,我们使用 executeLocalTransaction 方法来执行本地事务。
             * 也就是执行具体的业务逻辑
             * 返回三个事务状态之一。
             * - TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。
             * - TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
             * - TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。
             */
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                LocalTransactionState localTransactionState= null;
                try{
                    // 执行本地的事务逻辑
                    System.out.println("执行本地的事务逻辑");
                    System.out.println("arg: " + arg);
                    String s = new String(msg.getBody(), Charset.defaultCharset());
                    System.out.println("message: " + s);
                    // 模拟出现异常
                     int a = 1/0;
                    // 没有异常就返回可以提交
                    localTransactionState = LocalTransactionState.COMMIT_MESSAGE;
                }catch (Exception e){
                    e.printStackTrace();
                    // 出现异常就回滚
                     localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
                }finally {
                    // 记录一下
                    STATE_MAP.put(msg.getTransactionId(),localTransactionState);
                    // 没有异常就返回可以提交
                    return localTransactionState;
                }

            }
            /***
             * 用于检查本地事务状态,并回应消息队列的检查请求。它也是返回三个事务状态之一。
             */
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                return STATE_MAP.get(msg.getTransactionId());
            }
        });

        // 5 启动
        producer.start();
        // 6 发送消息,注意发送消息的方法为sendMessageInTransaction
        Message message = new Message();
        message.setTopic("myTopic");
        message.setBody("一条事务消息".getBytes(Charset.defaultCharset()));
        // 第二个参数其他参数,会传到executeLocalTransaction的第二个参数
        producer.sendMessageInTransaction(message,"事务消息测试");
        // 还存在会查生产者的逻辑,生产者就不关闭了
        // producer.shutdown();

    }
}

  1. 消费者
package study.wyy.mq.rocket.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
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.message.MessageExt;

import java.nio.charset.Charset;
import java.util.List;

/**
 * @author 20116651
 * @description
 * @date 2020/12/28 16:10
 */
public class TransactionMessageConsumer {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer-group");
        consumer.setNamesrvAddr("localhost:9876");
        //订阅消息,接收的是所有消息
        consumer.subscribe("myTopic","*");
        // 注册消息监听
        consumer.registerMessageListener(new MessageListenerConcurrently(){
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.println(" ======遍历消息======= ");
                if (msgs != null && msgs.size() > 0) {
                    for (MessageExt msg : msgs) {
                        System.out.println("消息id: " + msg.getMsgId());
                        System.out.println("topic: " + msg.getTopic());
                        System.out.println("tag: " + msg.getTags());
                        System.out.println("消息体:"+ new String(msg.getBody(), Charset.defaultCharset()));
                        System.out.println("=======end===========");
                    }
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();

    }
}

  1. 测试
  • 当出现一场的时候消费者是不会拿到消息的
  • 当没有异常的时候,消费者会成功打印出消息内容
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值