rocketmq源码解析请求处理重置消费者client的offset

说在前面

请求处理 重置消费者client的offset

 

源码解析

进入这个方法,org.apache.rocketmq.client.impl.ClientRemotingProcessor#resetOffset

public RemotingCommand resetOffset(ChannelHandlerContext ctx,        RemotingCommand request) throws RemotingCommandException {        final ResetOffsetRequestHeader requestHeader =            (ResetOffsetRequestHeader) request.decodeCommandCustomHeader(ResetOffsetRequestHeader.class);        log.info("invoke reset offset operation from broker. brokerAddr={}, topic={}, group={}, timestamp={}",            RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.getTopic(), requestHeader.getGroup(),            requestHeader.getTimestamp());        Map<MessageQueue, Long> offsetTable = new HashMap<MessageQueue, Long>();        if (request.getBody() != null) {            ResetOffsetBody body = ResetOffsetBody.decode(request.getBody(), ResetOffsetBody.class);            offsetTable = body.getOffsetTable();        }//        重置offset=》        this.mqClientFactory.resetOffset(requestHeader.getTopic(), requestHeader.getGroup(), offsetTable);        return null;    }

进入这个方法,重置offset,org.apache.rocketmq.client.impl.factory.MQClientInstance#resetOffset

public void resetOffset(String topic, String group, Map<MessageQueue, Long> offsetTable) {        DefaultMQPushConsumerImpl consumer = null;        try {//            获取消费组的消费者            MQConsumerInner impl = this.consumerTable.get(group);            if (impl != null && impl instanceof DefaultMQPushConsumerImpl) {                consumer = (DefaultMQPushConsumerImpl) impl;            } else {                log.info("[reset-offset] consumer dose not exist. group={}", group);                return;            }//            消费者暂停            consumer.suspend();
            ConcurrentMap<MessageQueue, ProcessQueue> processQueueTable = consumer.getRebalanceImpl().getProcessQueueTable();            for (Map.Entry<MessageQueue, ProcessQueue> entry : processQueueTable.entrySet()) {                MessageQueue mq = entry.getKey();                if (topic.equals(mq.getTopic()) && offsetTable.containsKey(mq)) {                    ProcessQueue pq = entry.getValue();//                  处理队列删除                    pq.setDropped(true);//                    清空处理队列=》                    pq.clear();                }            }
            try {                TimeUnit.SECONDS.sleep(10);            } catch (InterruptedException e) {            }
            Iterator<MessageQueue> iterator = processQueueTable.keySet().iterator();            while (iterator.hasNext()) {                MessageQueue mq = iterator.next();//                获取处理队列中的消息队列的offset                Long offset = offsetTable.get(mq);                if (topic.equals(mq.getTopic()) && offset != null) {                    try {//                        更新消费的offset=》                        consumer.updateConsumeOffset(mq, offset);//                        删除不需要的消息队列=》                        consumer.getRebalanceImpl().removeUnnecessaryMessageQueue(mq, processQueueTable.get(mq));                        iterator.remove();                    } catch (Exception e) {                        log.warn("reset offset failed. group={}, {}", group, mq, e);                    }                }            }        } finally {            if (consumer != null) {//                消费者暂停取消=》                consumer.resume();            }        }    }

进入这个方法,清空处理队列,org.apache.rocketmq.client.impl.consumer.ProcessQueue#clear

public void clear() {        try {            this.lockTreeMap.writeLock().lockInterruptibly();            try {                this.msgTreeMap.clear();//                消息排序                this.consumingMsgOrderlyTreeMap.clear();                this.msgCount.set(0);                this.msgSize.set(0);                this.queueOffsetMax = 0L;            } finally {                this.lockTreeMap.writeLock().unlock();            }        } catch (InterruptedException e) {            log.error("rollback exception", e);        }    }

进入这个方法,更新消费的offset,org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#updateOffset

@Override    public void updateOffset(MessageQueue mq, long offset, boolean increaseOnly) {        if (mq != null) {            AtomicLong offsetOld = this.offsetTable.get(mq);            if (null == offsetOld) {                offsetOld = this.offsetTable.putIfAbsent(mq, new AtomicLong(offset));            }
            if (null != offsetOld) {                if (increaseOnly) {                    MixAll.compareAndIncreaseOnly(offsetOld, offset);                } else {                    offsetOld.set(offset);                }            }        }    }

进入这个方法,删除不需要的消息队列,org.apache.rocketmq.client.impl.consumer.RebalancePullImpl#removeUnnecessaryMessageQueue,defaultMQPullConsumerImpl

@Override    public boolean removeUnnecessaryMessageQueue(MessageQueue mq, ProcessQueue pq) {//        消息队列持久化=》        this.defaultMQPullConsumerImpl.getOffsetStore().persist(mq);//        删除消息队列=》        this.defaultMQPullConsumerImpl.getOffsetStore().removeOffset(mq);        return true;    }

进入这个方法,消息队列持久化,org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#persist

@Override    public void persist(MessageQueue mq) {        AtomicLong offset = this.offsetTable.get(mq);        if (offset != null) {            try {//                更新broker消费的offset=》                this.updateConsumeOffsetToBroker(mq, offset.get());                log.info("[persist] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}",                    this.groupName,                    this.mQClientFactory.getClientId(),                    mq,                    offset.get());            } catch (Exception e) {                log.error("updateConsumeOffsetToBroker exception, " + mq.toString(), e);            }        }    }

进入这个方法,更新broker消费的offset,org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#updateConsumeOffsetToBroker(org.apache.rocketmq.common.message.MessageQueue, long, boolean)

 @Override    public void updateConsumeOffsetToBroker(MessageQueue mq, long offset, boolean isOneway) throws RemotingException,        MQBrokerException, InterruptedException, MQClientException {//        查询broker=》        FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName());        if (null == findBrokerResult) {
//            从namesrv更新topic路由信息=》            this.mQClientFactory.updateTopicRouteInfoFromNameServer(mq.getTopic());//            查找broker的地址 =》            findBrokerResult = this.mQClientFactory.findBrokerAddressInAdmin(mq.getBrokerName());        }
        if (findBrokerResult != null) {            UpdateConsumerOffsetRequestHeader requestHeader = new UpdateConsumerOffsetRequestHeader();            requestHeader.setTopic(mq.getTopic());            requestHeader.setConsumerGroup(this.groupName);            requestHeader.setQueueId(mq.getQueueId());            requestHeader.setCommitOffset(offset);
            if (isOneway) {//                单途更新消费者的offset=》                this.mQClientFactory.getMQClientAPIImpl().updateConsumerOffsetOneway(                    findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5);            } else {//                更新消费者的offset=》                this.mQClientFactory.getMQClientAPIImpl().updateConsumerOffset(                    findBrokerResult.getBrokerAddr(), requestHeader, 1000 * 5);            }        } else {            throw new MQClientException("The broker[" + mq.getBrokerName() + "] not exist", null);        }    }

进入这个方法,查询broker,org.apache.rocketmq.client.impl.factory.MQClientInstance#findBrokerAddressInAdmin

public FindBrokerResult findBrokerAddressInAdmin(final String brokerName) {        String brokerAddr = null;        boolean slave = false;        boolean found = false;
//        查询broker的地址列表        HashMap<Long/* brokerId */, String/* address */> map = this.brokerAddrTable.get(brokerName);        if (map != null && !map.isEmpty()) {            for (Map.Entry<Long, String> entry : map.entrySet()) {                Long id = entry.getKey();                brokerAddr = entry.getValue();                if (brokerAddr != null) {                    found = true;                    if (MixAll.MASTER_ID == id) {                        slave = false;                    } else {                        slave = true;                    }                    break;
                }            } // end of for        }
        if (found) {//            =》            return new FindBrokerResult(brokerAddr, slave, findBrokerVersion(brokerName, brokerAddr));        }
        return null;    }

进入这个方法, 从namesrv更新topic路由信息,org.apache.rocketmq.client.impl.factory.MQClientInstance#updateTopicRouteInfoFromNameServer(java.lang.String, boolean, org.apache.rocketmq.client.producer.DefaultMQProducer)

public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,        DefaultMQProducer defaultMQProducer) {        try {            if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {                try {                    TopicRouteData topicRouteData;                    if (isDefault && defaultMQProducer != null) {//                        获取默认的topic路由信息 =》                        topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),                            1000 * 3);                        if (topicRouteData != null) {//                            获取队列信息                            for (QueueData data : topicRouteData.getQueueDatas()) {//                                读写队列最大数量4                                int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());                                data.setReadQueueNums(queueNums);                                data.setWriteQueueNums(queueNums);                            }                        }                    } else {//                        获取topic路由信息=》                        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);                    }                    if (topicRouteData != null) {                        TopicRouteData old = this.topicRouteTable.get(topic);//                        判断topic路由是否改变=》                        boolean changed = topicRouteDataIsChange(old, topicRouteData);                        if (!changed) {//                            需要更新路由信息                            changed = this.isNeedUpdateTopicRouteInfo(topic);                        } else {                            log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);                        }
                        if (changed) {                            TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
                            for (BrokerData bd : topicRouteData.getBrokerDatas()) {//                                更新broker的地址列表                                this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());                            }
                            // Update Pub info                            {//                                topic路由信息转换成topic发布信息=》                                TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);                                publishInfo.setHaveTopicRouterInfo(true);//                                遍历生产者信息                                Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();                                while (it.hasNext()) {                                    Entry<String, MQProducerInner> entry = it.next();                                    MQProducerInner impl = entry.getValue();                                    if (impl != null) {//                                        更新topic发布信息=》                                        impl.updateTopicPublishInfo(topic, publishInfo);                                    }                                }                            }
                            // Update sub info                            {//                                获取消息队列订阅信息                                Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);//                                遍历消费者                                Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();                                while (it.hasNext()) {                                    Entry<String, MQConsumerInner> entry = it.next();                                    MQConsumerInner impl = entry.getValue();                                    if (impl != null) {//                                        更新topic的订阅信息=》                                        impl.updateTopicSubscribeInfo(topic, subscribeInfo);                                    }                                }                            }                            log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData);                            this.topicRouteTable.put(topic, cloneTopicRouteData);                            return true;                        }                    } else {                        log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}", topic);                    }                } catch (Exception e) {                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && !topic.equals(MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC)) {                        log.warn("updateTopicRouteInfoFromNameServer Exception", e);                    }                } finally {                    this.lockNamesrv.unlock();                }            } else {                log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms", LOCK_TIMEOUT_MILLIS);            }        } catch (InterruptedException e) {            log.warn("updateTopicRouteInfoFromNameServer Exception", e);        }
        return false;    }

进入这个方法,获取默认的topic路由信息,org.apache.rocketmq.client.impl.MQClientAPIImpl#getTopicRouteInfoFromNameServer(java.lang.String, long, boolean)

public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis,        boolean allowTopicNotExist) throws MQClientException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException {        GetRouteInfoRequestHeader requestHeader = new GetRouteInfoRequestHeader();        requestHeader.setTopic(topic);
        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINTO_BY_TOPIC, requestHeader);
//        同步获取topic的路由信息=》        RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis);        assert response != null;        switch (response.getCode()) {            case ResponseCode.TOPIC_NOT_EXIST: {                if (allowTopicNotExist && !topic.equals(MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC)) {                    log.warn("get Topic [{}] RouteInfoFromNameServer is not exist value", topic);                }
                break;            }            case ResponseCode.SUCCESS: {                byte[] body = response.getBody();                if (body != null) {                    return TopicRouteData.decode(body, TopicRouteData.class);                }            }            default:                break;        }
        throw new MQClientException(response.getCode(), response.getRemark());    }

进入这个方法,同步获取topic的路由信息,org.apache.rocketmq.remoting.netty.NettyRemotingClient#invokeSync 前面介绍过了。

 

往上返回到这个方法,获取topic路由信息,org.apache.rocketmq.client.impl.MQClientAPIImpl#getTopicRouteInfoFromNameServer(java.lang.String, long) 上面介绍过了。

 

往上返回到这个方法,判断topic路由是否改变,org.apache.rocketmq.client.impl.factory.MQClientInstance#topicRouteDataIsChange

private boolean topicRouteDataIsChange(TopicRouteData olddata, TopicRouteData nowdata) {        if (olddata == null || nowdata == null)            return true;        TopicRouteData old = olddata.cloneTopicRouteData();        TopicRouteData now = nowdata.cloneTopicRouteData();        Collections.sort(old.getQueueDatas());        Collections.sort(old.getBrokerDatas());        Collections.sort(now.getQueueDatas());        Collections.sort(now.getBrokerDatas());        return !old.equals(now);
    }

往上返回到这个方法,单途更新消费者的offset,org.apache.rocketmq.client.impl.MQClientAPIImpl#updateConsumerOffsetOneway

public void updateConsumerOffsetOneway(        final String addr,        final UpdateConsumerOffsetRequestHeader requestHeader,        final long timeoutMillis    ) throws RemotingConnectException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException,        InterruptedException {        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, requestHeader);
//        =》        this.remotingClient.invokeOneway(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis);    }

进入这个方法,org.apache.rocketmq.remoting.netty.NettyRemotingClient#invokeOneway前面介绍过了。

 

往上返回都这个方法,更新消费者的offset,org.apache.rocketmq.client.impl.MQClientAPIImpl#updateConsumerOffset

public void updateConsumerOffset(        final String addr,        final UpdateConsumerOffsetRequestHeader requestHeader,        final long timeoutMillis    ) throws RemotingException, MQBrokerException, InterruptedException {        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_CONSUMER_OFFSET, requestHeader);
//        =》        RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),            request, timeoutMillis);        assert response != null;        switch (response.getCode()) {            case ResponseCode.SUCCESS: {                return;            }            default:                break;        }
        throw new MQBrokerException(response.getCode(), response.getRemark());    }

进入到这个方法,org.apache.rocketmq.remoting.netty.NettyRemotingClient#invokeSync前面介绍过了。

 

往上返回到这个方法,删除消息队列,org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#removeOffset

public void removeOffset(MessageQueue mq) {        if (mq != null) {            this.offsetTable.remove(mq);            log.info("remove unnecessary messageQueue offset. group={}, mq={}, offsetTableSize={}", this.groupName, mq,                offsetTable.size());        }    }

往上返回到这个方法,org.apache.rocketmq.client.impl.consumer.RebalancePushImpl#removeUnnecessaryMessageQueue,defaultMQPushConsumerImpl

@Override    public boolean removeUnnecessaryMessageQueue(MessageQueue mq, ProcessQueue pq) {//        持久化消息队列        this.defaultMQPushConsumerImpl.getOffsetStore().persist(mq);//        删除消息队列的offset=》        this.defaultMQPushConsumerImpl.getOffsetStore().removeOffset(mq);//        如果是有序消费,消息类型是集群消费        if (this.defaultMQPushConsumerImpl.isConsumeOrderly()            && MessageModel.CLUSTERING.equals(this.defaultMQPushConsumerImpl.messageModel())) {            try {                if (pq.getLockConsume().tryLock(1000, TimeUnit.MILLISECONDS)) {                    try {//                        解锁延迟=》                        return this.unlockDelay(mq, pq);                    } finally {                        pq.getLockConsume().unlock();                    }                } else {                    log.warn("[WRONG]mq is consuming, so can not unlock it, {}. maybe hanged for a while, {}",                        mq,                        pq.getTryUnlockTimes());
                    pq.incTryUnlockTimes();                }            } catch (Exception e) {                log.error("removeUnnecessaryMessageQueue Exception", e);            }
            return false;        }        return true;    }

进入这个方法,持久化消息队列,org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#persist 前面介绍过了。

 

往上返回到这个方法, 删除消息队列的offset,org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#removeOffset 前面介绍过了。

 

进入这个方法,解锁延迟,org.apache.rocketmq.client.impl.consumer.RebalancePushImpl#unlockDelay

private boolean unlockDelay(final MessageQueue mq, final ProcessQueue pq) {
//        处理队列中有临时消息=》        if (pq.hasTempMessage()) {            log.info("[{}]unlockDelay, begin {} ", mq.hashCode(), mq);            this.defaultMQPushConsumerImpl.getmQClientFactory().getScheduledExecutorService().schedule(new Runnable() {                @Override                public void run() {                    log.info("[{}]unlockDelay, execute at once {}", mq.hashCode(), mq);//                    解锁消息队列=》                    RebalancePushImpl.this.unlock(mq, true);                }            }, UNLOCK_DELAY_TIME_MILLS, TimeUnit.MILLISECONDS);        } else {            this.unlock(mq, true);        }        return true;    }

进入这个方法,处理队列中有临时消息,org.apache.rocketmq.client.impl.consumer.ProcessQueue#hasTempMessage

public boolean hasTempMessage() {        try {            this.lockTreeMap.readLock().lockInterruptibly();            try {                return !this.msgTreeMap.isEmpty();            } finally {                this.lockTreeMap.readLock().unlock();            }        } catch (InterruptedException e) {        }
        return true;    }

进入这个方法,解锁消息队列,org.apache.rocketmq.client.impl.consumer.RebalanceImpl#unlock

public void unlock(final MessageQueue mq, final boolean oneway) {//        按brokerName查询broker地址在订阅信息中=》        FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true);        if (findBrokerResult != null) {            UnlockBatchRequestBody requestBody = new UnlockBatchRequestBody();            requestBody.setConsumerGroup(this.consumerGroup);            requestBody.setClientId(this.mQClientFactory.getClientId());            requestBody.getMqSet().add(mq);
            try {//                解锁批量消息队列,1s超时                this.mQClientFactory.getMQClientAPIImpl().unlockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000, oneway);                log.warn("unlock messageQueue. group:{}, clientId:{}, mq:{}",                    this.consumerGroup,                    this.mQClientFactory.getClientId(),                    mq);            } catch (Exception e) {                log.error("unlockBatchMQ exception, " + mq, e);            }        }    }

进入这个方法,按brokerName查询broker地址在订阅信息中,org.apache.rocketmq.client.impl.factory.MQClientInstance#findBrokerAddressInSubscribe

public FindBrokerResult findBrokerAddressInSubscribe(        final String brokerName,        final long brokerId,        final boolean onlyThisBroker    ) {        String brokerAddr = null;        boolean slave = false;        boolean found = false;
//        获取broker的缓存信息        HashMap<Long/* brokerId */, String/* address */> map = this.brokerAddrTable.get(brokerName);        if (map != null && !map.isEmpty()) {            brokerAddr = map.get(brokerId);            slave = brokerId != MixAll.MASTER_ID;            found = brokerAddr != null;
            if (!found && !onlyThisBroker) {                Entry<Long, String> entry = map.entrySet().iterator().next();                brokerAddr = entry.getValue();                slave = entry.getKey() != MixAll.MASTER_ID;                found = true;            }        }
        if (found) {            return new FindBrokerResult(brokerAddr, slave, findBrokerVersion(brokerName, brokerAddr));        }
        return null;    }

进入这个方法,解锁批量消息队列,1s超时,org.apache.rocketmq.client.impl.MQClientAPIImpl#unlockBatchMQ

public void unlockBatchMQ(        final String addr,        final UnlockBatchRequestBody requestBody,        final long timeoutMillis,        final boolean oneway    ) throws RemotingException, MQBrokerException, InterruptedException {        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UNLOCK_BATCH_MQ, null);
//        json编码        request.setBody(requestBody.encode());
        if (oneway) {//           单途请求=》            this.remotingClient.invokeOneway(addr, request, timeoutMillis);        } else {//            同步执行            RemotingCommand response = this.remotingClient                .invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr), request, timeoutMillis);            switch (response.getCode()) {                case ResponseCode.SUCCESS: {                    return;                }                default:                    break;            }
            throw new MQBrokerException(response.getCode(), response.getRemark());        }    }

进入这个方法,单途请求,org.apache.rocketmq.remoting.netty.NettyRemotingClient#invokeOneway 前面介绍过了。

 

进入这个方法,同步执行,org.apache.rocketmq.remoting.netty.NettyRemotingClient#invokeSync 前面介绍过了。

 

往上返回到这个方法,消费者暂停取消,org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#resume

 public void resume() {        this.pause = false;//        负载均衡=》        doRebalance();        log.info("resume this consumer, {}", this.defaultMQPushConsumer.getConsumerGroup());    }

进入这个方法,负载均衡,org.apache.rocketmq.client.impl.consumer.RebalanceImpl#doRebalance 前面介绍过了。

往上返回到这个方法,org.apache.rocketmq.client.impl.ClientRemotingProcessor#resetOffset结束。

 

说在最后

本次解析仅代表个人观点,仅供参考。

 

加入技术微信群

钉钉技术群

转载于:https://my.oschina.net/u/3775437/blog/3095512

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值