rockeMQ-producer源码

DefaultMQProducerImpl

启动

start方法

  1. producerGroup如果不是CLIENT_INNER_PRODUCER,并且intanceName=DEFAULT,则把instanceName改为PID
  2. mQClientFactory是一个MQClientInstance对象,由MQClientManager管理,这是一个单例,其中ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable 用来维护创建的MQClientInstance对象。
    1. 创建mQClientFactory时首先根据创建clientId=ip+instanceName+unitName,根据这个id在factoryTable中获取对象
    2. 如果为空,则创建一个新的对象,放入factoryTable中
  • 注意,如果一个进程中有两个不同nameserver的producer,需要配置不同的instanceName或者unitName,否则只会创建一个实例,只向一个broker集群发消息
  • MQClientInstance是rocketMQ的消息客户端实例,用于根broker和nameserver进行网络通信,还包含一些后台线程和定时任务
  1. 注册producer到mQClientFactory

    1. mQClientFactory中维护一个ConcurrentMap<String/* group */, MQProducerInner> producerTable对象缓存创建的producer
    2. 根据当前producer的producerGroup从map中获取,没有则直接将当前producer放入map中,如果producerGroup已存在则会提示producerGroup已存在
  2. DefaultMQProducerImpl 维护一个ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable,缓存topic和TopicPublishInfo,新建一个topicPublishInfoTable放入table中

  3. 调用mQClientFactory的start方法,启动流程包括:

    1. 获取nameServer地址,如果没有则用http请求从配置url中获取
    	this.mQClientAPIImpl.fetchNameServerAddr();
    
    1. 启动mqClientAPIImpl
    // Start request-response channel
    this.mQClientAPIImpl.start();
    
    1. 启动各种定时任务
    // Start various schedule tasks
    this.startScheduledTask();
    

    定时任务包括:

     * fetchNameServerAddr:每隔2分钟尝试获取一次NameServer地址
     * updateTopicRouteInfoFromNameServer:每隔30S尝试更新主题路由信息
     * cleanOfflineBroker&sendHeartbeatToAllBrokerWithLock:每隔30S 进行Broker心跳检测
     * persistAllConsumerOffset:默认每隔5秒持久化ConsumeOffset
     * adjustThreadPool:默认每隔1S检查线程池适配
    
    1. 启动pull service线程
    // Start pull service
    this.pullMessageService.start();
    
    1. 启动负载均衡线程
    // Start rebalance service
    this.rebalanceService.start();
    
    1. 启动push service,这里又是start方法,区别是startFactory
      参数为false
    // Start push service
    this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
    

start方法:

public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
        case *CREATE_JUST*:
            this.serviceState = ServiceState.*START_FAILED*;

            this.checkConfig();

            if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.*CLIENT_INNER_PRODUCER_GROUP*)) {
                this.defaultMQProducer.changeInstanceNameToPID();
            }

            this.mQClientFactory = MQClientManager.*getInstance*().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);

            boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
            if (!registerOK) {
                this.serviceState = ServiceState.*CREATE_JUST*;
                throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
                    + "] has been created before, specify another name please." + FAQUrl.*suggestTodo*(FAQUrl.*GROUP_NAME_DUPLICATE_URL*),
                    null);
            }

            this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

            if (startFactory) {
                mQClientFactory.start();
            }

            log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
                this.defaultMQProducer.isSendMessageWithVIPChannel());
            this.serviceState = ServiceState.*RUNNING*;
            break;
        case *RUNNING*:
        case *START_FAILED*:
        case *SHUTDOWN_ALREADY*:
            throw new MQClientException("The producer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.*suggestTodo*(FAQUrl.*CLIENT_SERVICE_NOT_OK*),
                null);
        default:
            break;
    }

    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}

发送消息

RockeMq支持三种消息发送方式

  • 同步,producer向mq发送消息时同步等待,直到服务器返回发送结果或超时
  • 异步,producer向mq发送消息时制定发送成功后的回调函数,消息发送成功或失败会在另外的线程执行回调函数
  • 单向,producer向mq发送消息时直接返回,不等待消息发送结果也不注册回调函数

消息发送的基本流程包括

  • 校验消息
  • 查找路由
  • 消息发送(含异常处理机制)

验证消息

校验消息的主题,消息体不能为空,长度大于0且小于4M(默认)

查找路由信息

  1. 在本地topicPublishInfoTable缓存中根据topic获取

  2. 如果没有或者状态不ok,则updateTopicRouteInfoFromNameServer,从nameServer根据topic获取并放入缓存,拿到TopicPublishInfo对象,转为topicRouteData对象

  3. 路由信息在TopicPublishInfo对象中

private boolean orderTopic = false; //是否是顺序消息
private boolean haveTopicRouterInfo = false; 
private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>(); //该主题的消息队列
private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex(); //每选择一次消息队列,会+1,用于次选择消息队列
private TopicRouteData topicRouteData;

topicRouteData对象

private String orderTopicConf;
private List<QueueData> queueDatas; //topic队列元数据
private List<BrokerData> brokerDatas; //topic分布的broker元数据
private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable; //broker上过滤服务器地址列表

选择消息队列

获取到路由信息之后,发送消息之前和每次重试之前都会选择一个合适的消息队列。

消息队列的选择分为两种情况:
* 不启用故障延迟:sendLatencyFaultEnable=false
* 启用故障延迟:sendLatencyFaultEnable=true

不启用故障延迟

如果上一次选择的broker为空,表明是第一次,直接选择,如果上一次选择的broker不为空则sendWhichQUeue+1之后跟消息队列的个数取模,从消息队列中取出第sendWhichQueue个并且brokerName和上一次不一样的消息队列。

由于nameServer感知broker是否故障不是实时的,检测broker心跳是每10s一次,并且producer每隔30s才更新路由信息,所以producer不可能实时感知消息队列是否故障。所以可以引入一种机制在broker宕机期间,如果一次消息发送失败则将这个broker暂时排除在外

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
    if (lastBrokerName == null) {
        return selectOneMessageQueue();
    } else {
        int index = this.sendWhichQueue.getAndIncrement();
        for (int i = 0; i < this.messageQueueList.size(); i++) {
            int pos = Math./abs/(index++) % this.messageQueueList.size();
            if (pos < 0)
                pos = 0;
            MessageQueue mq = this.messageQueueList.get(pos);
            if (!mq.getBrokerName().equals(lastBrokerName)) {
                return mq;
            }
        }
        return selectOneMessageQueue();
    }
}

public MessageQueue selectOneMessageQueue() {
    int index = this.sendWhichQueue.getAndIncrement();
    int pos = Math./abs/(index) % this.messageQueueList.size();
    if (pos < 0)
        pos = 0;
    return this.messageQueueList.get(pos);
}

启用故障延迟机制

启用故障延迟机制的消息队列选择是在MQFaultStrategy对象的selectOneMessageQueue方法。

sendLatencyFaultEnable=true的情况

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
    if (this.sendLatencyFaultEnable) {
        try {
/**首先选择一个isAvailable并且brokerName= lastBrokerName的mq*/
            int index = tpInfo.getSendWhichQueue().getAndIncrement();
            for (int I = 0; I < tpInfo.getMessageQueueList().size(); I++) {
                int pos = Math.*abs*(index++) % tpInfo.getMessageQueueList().size();
                if (pos < 0)
                    pos = 0;
                MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                    if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                        return mq;
                }
            }
/**其次在latencyFaultTolerance选择一个稍微好一点的mq*/
            final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
            int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
            if (writeQueueNums > 0) {
                final MessageQueue mq = tpInfo.selectOneMessageQueue();
                if (notBestBroker != null) {
                    mq.setBrokerName(notBestBroker);
                    mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                }
                return mq;
            } else {
                latencyFaultTolerance.remove(notBestBroker);
            }
        } catch (Exception e) {
            *log*.error(“Error occurred when selecting message queue”, e);
        }
/**随机选择一个*/
        return tpInfo.selectOneMessageQueue();
    }
/**根据上一次brokerName随机选择一个*/
    return tpInfo.selectOneMessageQueue(lastBrokerName);
}
RocketMq故障延迟机制核心类
MQFaultStrategy
private final LatencyFaultTolerance<String> latencyFaultTolerance = new LatencyFaultToleranceImpl();

private boolean sendLatencyFaultEnable = false;

private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};

LatencyFaultToleranceImpl
private final ConcurrentHashMap<String, FaultItem> faultItemTable = new ConcurrentHashMap<String, FaultItem>(16);

private final ThreadLocalIndex whichItemWorst = new ThreadLocalIndex();

FaultItem失败条目
private final String name; //条目唯一键,这里指brokerName
private volatile long currentLatency; //本次消息发送延迟
private volatile long startTimestamp;//故障规避开始时间

在defaultMQProducerImpl中,发送消息catch异常之后会把异常broker添加到latencyFaultTolerance中。

参数:broker名称,本次消息发送延迟时间,是否需要隔离,true表示使用默认时长30s老极端故障规避时长,false表示使用本次消息发送延迟来计算(在notAvailableDuration找到大于等于本次延迟的时间)

endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
exception = e;
continue;

发送结束也会默认添加broker

this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);

总结:

  1. 所有的broker延迟信息都会被记录
  2. 发送消息时会选择延迟最低的broker来发送,提高效率
  3. broker延迟过高会自动减少它的消息分配,充分发挥所有服务器的能力

消息发送

消息发送在DefaultMQProducerImpl的sendKernelImpl方法

发送流程为:

  1. 根据brokerName在本地缓存获取broker地址,如果获取不到则去name server拉取
  2. 设置消息全局唯一id,如果消息超过4K,则压缩,如果是事务消息,标记为/TRANSACTION_PREPARED_TYPE
  3. 如果注册了消息发送狗子函数则执行狗子函数的方法增强消息发送逻辑
  4. 构建消息请求体
  5. 根据消息发送同步,异步单向进行网络传输
private SendResult sendKernelImpl(final Message msg,
                                  final MessageQueue mq,
                                  final CommunicationMode communicationMode,
                                  final SendCallback sendCallback,
                                  final TopicPublishInfo topicPublishInfo,
                                  final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    long beginStartTime = System./currentTimeMillis/();
/**1. 根据brokerName在本地缓存获取broker地址,如果获取不到则去name server拉取*/
    String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
    if (null == brokerAddr) {
        tryToFindTopicPublishInfo(mq.getTopic());
        brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
    }

    SendMessageContext context = null;
    if (brokerAddr != null) {
        brokerAddr = MixAll./brokerVIPChannel/(this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr);

        byte[] prevBody = msg.getBody();
        try {
            //for MessageBatch,ID has been set in the generating process
/**2. 设置消息全局唯一id,如果消息超过4K,则压缩,如果是事务消息,标记为/TRANSACTION_PREPARED_TYPE*/
            if (!(msg instanceof MessageBatch)) {
                MessageClientIDSetter./setUniqID/(msg);
            }

            int sysFlag = 0;
            boolean msgBodyCompressed = false;
            if (this.tryToCompressMessage(msg)) {
                sysFlag |= MessageSysFlag./COMPRESSED_FLAG/;
                msgBodyCompressed = true;
            }

            final String tranMsg = msg.getProperty(MessageConst./PROPERTY_TRANSACTION_PREPARED/);
            if (tranMsg != null && Boolean./parseBoolean/(tranMsg)) {
                sysFlag |= MessageSysFlag./TRANSACTION_PREPARED_TYPE/;
            }
/**3. 如果注册了消息发送狗子函数则执行狗子函数的方法增强消息发送逻辑*/
            if (hasCheckForbiddenHook()) {
                CheckForbiddenContext checkForbiddenContext = new CheckForbiddenContext();
                checkForbiddenContext.setNameSrvAddr(this.defaultMQProducer.getNamesrvAddr());
                checkForbiddenContext.setGroup(this.defaultMQProducer.getProducerGroup());
                checkForbiddenContext.setCommunicationMode(communicationMode);
                checkForbiddenContext.setBrokerAddr(brokerAddr);
                checkForbiddenContext.setMessage(msg);
                checkForbiddenContext.setMq(mq);
                checkForbiddenContext.setUnitMode(this.isUnitMode());
                this.executeCheckForbiddenHook(checkForbiddenContext);
            }

            if (this.hasSendMessageHook()) {
                context = new SendMessageContext();
                context.setProducer(this);
                context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
                context.setCommunicationMode(communicationMode);
                context.setBornHost(this.defaultMQProducer.getClientIP());
                context.setBrokerAddr(brokerAddr);
                context.setMessage(msg);
                context.setMq(mq);
                String isTrans = msg.getProperty(MessageConst./PROPERTY_TRANSACTION_PREPARED/);
                if (isTrans != null && isTrans.equals("true")) {
                    context.setMsgType(MessageType./Trans_Msg_Half/);
                }

                if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst./PROPERTY_DELAY_TIME_LEVEL/) != null) {
                    context.setMsgType(MessageType./Delay_Msg/);
                }
                this.executeSendMessageHookBefore(context);
            }
/**4. 构建消息请求体*/
            SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
            requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
            requestHeader.setTopic(msg.getTopic());
            requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
            requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
            requestHeader.setQueueId(mq.getQueueId());
            requestHeader.setSysFlag(sysFlag);
            requestHeader.setBornTimestamp(System./currentTimeMillis/());
            requestHeader.setFlag(msg.getFlag());
            requestHeader.setProperties(MessageDecoder./messageProperties2String/(msg.getProperties()));
            requestHeader.setReconsumeTimes(0);
            requestHeader.setUnitMode(this.isUnitMode());
            requestHeader.setBatch(msg instanceof MessageBatch);
            if (requestHeader.getTopic().startsWith(MixAll./RETRY_GROUP_TOPIC_PREFIX/)) {
                String reconsumeTimes = MessageAccessor./getReconsumeTime/(msg);
                if (reconsumeTimes != null) {
                    requestHeader.setReconsumeTimes(Integer./valueOf/(reconsumeTimes));
                    MessageAccessor./clearProperty/(msg, MessageConst./PROPERTY_RECONSUME_TIME/);
                }

                String maxReconsumeTimes = MessageAccessor./getMaxReconsumeTimes/(msg);
                if (maxReconsumeTimes != null) {
                    requestHeader.setMaxReconsumeTimes(Integer./valueOf/(maxReconsumeTimes));
                    MessageAccessor./clearProperty/(msg, MessageConst./PROPERTY_MAX_RECONSUME_TIMES/);
                }
            }
/**5. 根据消息发送同步,异步单向进行网络传输*/
            SendResult sendResult = null;
            switch (communicationMode) {
                case /ASYNC/:
                    Message tmpMessage = msg;
                    if (msgBodyCompressed) {
                        //If msg body was compressed, msgbody should be reset using prevBody.
                        //Clone new message using compressed message body and recover origin massage.
                        //Fix bug:https://github.com/apache/rocketmq-externals/issues/66
                        tmpMessage = MessageAccessor./cloneMessage/(msg);
                        msg.setBody(prevBody);
                    }
                    long costTimeAsync = System./currentTimeMillis/() - beginStartTime;
                    if (timeout < costTimeAsync) {
                        throw new RemotingTooMuchRequestException(“sendKernelImpl call timeout”);
                    }
                    sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                        brokerAddr,
                        mq.getBrokerName(),
                        tmpMessage,
                        requestHeader,
                        timeout - costTimeAsync,
                        communicationMode,
                        sendCallback,
                        topicPublishInfo,
                        this.mQClientFactory,
                        this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),
                        context,
                        this);
                    break;
                case /ONEWAY/:
                case /SYNC/:
                    long costTimeSync = System./currentTimeMillis/() - beginStartTime;
                    if (timeout < costTimeSync) {
                        throw new RemotingTooMuchRequestException(“sendKernelImpl call timeout”);
                    }
                    sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
                        brokerAddr,
                        mq.getBrokerName(),
                        msg,
                        requestHeader,
                        timeout - costTimeSync,
                        communicationMode,
                        context,
                        this);
                    break;
                default:
                    assert false;
                    break;
            }

            if (this.hasSendMessageHook()) {
                context.setSendResult(sendResult);
                this.executeSendMessageHookAfter(context);
            }

            return sendResult;
        } catch (RemotingException e) {
            if (this.hasSendMessageHook()) {
                context.setException(e);
                this.executeSendMessageHookAfter(context);
            }
            throw e;
        } catch (MQBrokerException e) {
            if (this.hasSendMessageHook()) {
                context.setException(e);
                this.executeSendMessageHookAfter(context);
            }
            throw e;
        } catch (InterruptedException e) {
            if (this.hasSendMessageHook()) {
                context.setException(e);
                this.executeSendMessageHookAfter(context);
            }
            throw e;
        } finally {
            msg.setBody(prevBody);
        }
    }

    throw new MQClientException(“The broker[+ mq.getBrokerName() +] not exist”, null);
}

发送消息的入口是MQClientAPIImpl的sendMessage方法。requestCode 是/SEND_MESSAGE/

最终处理这个命令的的地方是在broker里,SendMessageProcessor的processRequest方法(在channelRead0()里根据request类型选择相应的processor进行处理)。

  1. 检查该broker是否有写权限
  2. 检查topic是否可以进行消息发送,主要针对默认主题,默认主题不能发送消息只能供路由查找
  3. 检查队列是否合法
  4. 重试次数是否超过限制,如果超过则放在延迟队列,主题为%DLQ%+消费组名
  5. 调用defaultMessageStore的putMessage进行消息存储
private RemotingCommand sendMessage(final ChannelHandlerContext ctx,
                                    final RemotingCommand request,
                                    final SendMessageContext sendMessageContext,
                                    final SendMessageRequestHeader requestHeader) throws RemotingCommandException {

    final RemotingCommand response = RemotingCommand./createResponseCommand/(SendMessageResponseHeader.class);
    final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader)response.readCustomHeader();

    response.setOpaque(request.getOpaque());

    response.addExtField(MessageConst./PROPERTY_MSG_REGION/, this.brokerController.getBrokerConfig().getRegionId());
    response.addExtField(MessageConst./PROPERTY_TRACE_SWITCH/, String./valueOf/(this.brokerController.getBrokerConfig().isTraceOn()));

    /log/.debug("receive SendMessage request command, {}", request);

    final long startTimstamp = this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp();
    if (this.brokerController.getMessageStore().now() < startTimstamp) {
        response.setCode(ResponseCode./SYSTEM_ERROR/);
        response.setRemark(String./format/("broker unable to service, until %s", UtilAll./timeMillisToHumanString2/(startTimstamp)));
        return response;
    }

    response.setCode(-1);
    super.msgCheck(ctx, requestHeader, response);
    if (response.getCode() != -1) {
        return response;
    }

    final byte[] body = request.getBody();

    int queueIdInt = requestHeader.getQueueId();
    TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());

    if (queueIdInt < 0) {
        queueIdInt = Math./abs/(this.random.nextInt() % 99999999) % topicConfig.getWriteQueueNums();
    }

    MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
    msgInner.setTopic(requestHeader.getTopic());
    msgInner.setQueueId(queueIdInt);

    if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig)) {
        return response;
    }

    msgInner.setBody(body);
    msgInner.setFlag(requestHeader.getFlag());
    MessageAccessor./setProperties/(msgInner, MessageDecoder./string2messageProperties/(requestHeader.getProperties()));
    msgInner.setPropertiesString(requestHeader.getProperties());
    msgInner.setBornTimestamp(requestHeader.getBornTimestamp());
    msgInner.setBornHost(ctx.channel().remoteAddress());
    msgInner.setStoreHost(this.getStoreHost());
    msgInner.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes());
    PutMessageResult putMessageResult = null;
    Map<String, String> oriProps = MessageDecoder./string2messageProperties/(requestHeader.getProperties());
    String traFlag = oriProps.get(MessageConst./PROPERTY_TRANSACTION_PREPARED/);
    if (traFlag != null && Boolean./parseBoolean/(traFlag)) {
        if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
            response.setCode(ResponseCode./NO_PERMISSION/);
            response.setRemark(
                "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
                    + "] sending transaction message is forbidden");
            return response;
        }
        putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner);
    } else {
        putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
    }

    return handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt);

}

批量消息发送

将同意主题的多条消息一起打包发送到消息服务器,减少网络调用次数,提高效率。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值