Solr4.7源码分析-启动篇(二)

加载完solr.xml后,再回到createCoreContainer方法:

  protected CoreContainer createCoreContainer() {
    SolrResourceLoader loader = new SolrResourceLoader(SolrResourceLoader.locateSolrHome());
    // 加载$solrhome/solr.xml作为ConfigSolr
    ConfigSolr config = loadConfigSolr(loader);
    CoreContainer cores = new CoreContainer(loader, config);
    cores.load();
    return cores;
  }
开始实例化CoreContainer,然后用这个实例来加载cores,load方法比较长,具体在注释中分析:
  /**
   * Load the cores defined for this CoreContainer
   */
  public void load()  {

    log.info("Loading cores into CoreContainer [instanceDir={}]", loader.getInstanceDir());

    // add the sharedLib to the shared resource loader before initializing cfg based plugins
    // 这个cfg是solr.xml的属性封装的对象,取sharedLib属性。sharedLib: path to a lib directory that will be shared across all cores
    String libDir = cfg.getSharedLibDirectory();
    if (libDir != null) {
      File f = FileUtils.resolvePath(new File(solrHome), libDir);
      log.info("loading shared library: " + f.getAbsolutePath());
      loader.addToClassLoader(libDir, null, false);
      loader.reloadLuceneSPI();
    }

    // solr.xml中配置的shardHandlerFactory
    shardHandlerFactory = ShardHandlerFactory.newInstance(cfg.getShardHandlerFactoryPluginInfo(), loader);
   
    // updateShardHandler实例
    updateShardHandler = new UpdateShardHandler( cfg);

    // 取transientCacheSize属性,默认是int最大值,如果不是int最大值,则不起作用
    solrCores.allocateLazyCores(cfg .getTransientCacheSize(), loader);

    logging = LogWatcher.newRegisteredLogWatcher(cfg.getLogWatcherConfig(), loader);

    // shareSchema属性,如果为true则共享IndexSchema
    shareSchema = cfg.hasSchemaCache();

    if (shareSchema) {
      indexSchemaCache = new ConcurrentHashMap<String,IndexSchema>();
    }
   
    hostName = cfg.getHost();
    log.info("Host Name: " + hostName);

    // 初始化zookeeper
    zkSys.initZooKeeper( this, solrHome, cfg);

    collectionsHandler = createHandler(cfg.getCollectionsHandlerClass(), CollectionsHandler.class);
    infoHandler        = createHandler(cfg.getInfoHandlerClass(), InfoHandler.class);
    coreAdminHandler   = createHandler(cfg.getCoreAdminHandlerClass(), CoreAdminHandler.class);

    containerProperties = cfg.getSolrProperties( "solr");

    // setup executor to load cores in parallel
    // do not limit the size of the executor in zk mode since cores may try and wait for each other.
    ExecutorService coreLoadExecutor = Executors.newFixedThreadPool(
        ( zkSys.getZkController() == null ? cfg.getCoreLoadThreadCount() : Integer. MAX_VALUE ),
        new DefaultSolrThreadFactory( "coreLoadExecutor") );

    try {
      CompletionService<SolrCore> completionService = new ExecutorCompletionService<SolrCore>(
          coreLoadExecutor);

      Set<Future<SolrCore>> pending = new HashSet<Future<SolrCore>>();

      // 遍历$solrhome寻找有core.properties的文件,
      List<CoreDescriptor> cds = coresLocator.discover(this );
      checkForDuplicateCoreNames(cds);

      for (final CoreDescriptor cd : cds) {

        final String name = cd.getName();
        try {

          if (cd.isTransient() || ! cd.isLoadOnStartup()) {
            // Store it away for later use. includes non-transient but not
            // loaded at startup cores.
            solrCores.putDynamicDescriptor(name, cd);
          }
          if (cd.isLoadOnStartup()) { // The normal case

            Callable<SolrCore> task = new Callable<SolrCore>() {
              @Override
              public SolrCore call() {
                SolrCore c = null;
                try {
                  if ( zkSys.getZkController() != null) {
                    preRegisterInZk(cd);
                  }
                  // 基于descriptor创建一个未注册的core
                  c = create(cd);
                  // 注册core
                  registerCore(cd.isTransient(), name, c, false, false );
                } catch (Exception e) {
                  SolrException. log(log, null, e);
                  try {
              /*    if (isZooKeeperAware()) {
                    try {
                      zkSys.zkController.unregister(name, cd);
                    } catch (InterruptedException e2) {
                      Thread.currentThread().interrupt();
                      SolrException.log(log, null, e2);
                    } catch (KeeperException e3) {
                      SolrException.log(log, null, e3);
                    }
                  }*/
                  } finally {
                    if (c != null) {
                      c.close();
                    }
                  }           
                }
                return c;
              }
            };
            pending.add(completionService.submit(task));

          }
        } catch (Exception e) {
          SolrException. log(log, null, e);
        }
      }

      while (pending != null && pending.size() > 0) {
        try {

          Future<SolrCore> future = completionService.take();
          if (future == null) return;
          pending.remove(future);

          try {
            SolrCore c = future.get();
            // track original names
            if (c != null) {
              solrCores.putCoreToOrigName(c, c.getName());
            }
          } catch (ExecutionException e) {
            SolrException. log(SolrCore.log, "Error loading core", e);
          }

        } catch (InterruptedException e) {
          throw new SolrException(SolrException.ErrorCode .SERVICE_UNAVAILABLE,
              "interrupted while loading core", e);
        }
      }

      // Start the background thread
      backgroundCloser = new CloserThread( this, solrCores, cfg );
      backgroundCloser.start();

    } finally {
      if (coreLoadExecutor != null) {
        ExecutorUtil.shutdownNowAndAwaitTermination(coreLoadExecutor);
      }
    }
   
    if (isZooKeeperAware()) {
      // register in zk in background threads
      Collection<SolrCore> cores = getCores();
      if (cores != null) {
        for (SolrCore core : cores) {
          try {
            zkSys.registerInZk(core, true);
          } catch (Throwable t) {
            SolrException. log(log, "Error registering SolrCore", t);
          }
        }
      }
    }
  }
其中通过遍历solrhome文件夹下的所有文件夹,寻找core.properties文件,每找到一个配置文件视为找到一个core,并终止此文件夹的寻找,因此core.properties文件不能在嵌套的文件夹中出现,找到了core.properties文件夹,里面配置的属性封装为CoreDescriptor实例,同时将properties属性配置的文件作为扩展配置文件一并加载(默认solrcore.properties),solrcore.properties的作用可见https://cwiki.apache.org/confluence/display/solr/Configuring+solrconfig.xml#Configuringsolrconfig.xml-SubstitutingPropertiesinSolrConfigFiles
下面具体看几个相关的重要方法,以注释的形式分析:
先是coresLocator.discover(this)方法,coresLocator有两个子类:CorePropertiesLocator和SolrXMLCoresLocator,此处使用的是CorePropertiesLocator:
  @Override
  public List<CoreDescriptor> discover(CoreContainer cc) {
    logger.info( "Looking for core definitions underneath {}", rootDirectory.getAbsolutePath());
    List<CoreDescriptor> cds = Lists. newArrayList();
    discoverUnder( rootDirectory, cds, cc );
    logger.info( "Found {} core definitions", cds.size ());
    return cds;
  }



  private void discoverUnder(File root, List<CoreDescriptor> cds, CoreContainer cc ) {
    if (!root.exists())
      return;
    for (File child : root.listFiles()) {
      File propertiesFile = new File(child, PROPERTIES_FILENAME);
      if (propertiesFile.exists()) {
        // 里面读property文件,返回时,new了一个CoreDescriptor
        CoreDescriptor cd = buildCoreDescriptor(propertiesFile, cc );
        logger.info( "Found core {} in {}", cd.getName(), cd.getInstanceDir());
        cds.add(cd);
        // 这里continue了,这就是为什么不支持嵌套的原因:https://cwiki.apache.org/confluence/display/solr/Format+of+solr.xml
        continue;
      }
      if (child.isDirectory())
        discoverUnder(child, cds, cc);
    }
  }
之后是CoreDescriptor的构造函数和里面加载solrcore.properties的方法:
  /**
   * Create a new CoreDescriptor.
   * @param container       the CoreDescriptor's container
   * @param name            the CoreDescriptor's name
   * @param instanceDir     a String containing the instanceDir
   * @param coreProps       a Properties object of the properties for this core
   * @param params          additional params
   */
  public CoreDescriptor(CoreContainer container, String name, String instanceDir,
                        Properties coreProps, SolrParams params) {

    // CoreDescriptor里还有个包含自己的final的CoreContainer的引用
    this.coreContainer = container;

    originalCoreProperties.setProperty( CORE_NAME, name);
    originalCoreProperties.setProperty( CORE_INSTDIR, instanceDir);

    Properties containerProperties = container.getContainerProperties();
    name = PropertiesUtil.substituteProperty(checkPropertyIsNotEmpty(name , CORE_NAME),
                                             containerProperties);
    instanceDir = PropertiesUtil.substituteProperty(checkPropertyIsNotEmpty(instanceDir , CORE_INSTDIR),
                                                    containerProperties);

    // 默认配置属性
    coreProperties.putAll( defaultProperties);
    coreProperties.put( CORE_NAME, name);
    coreProperties.put( CORE_INSTDIR, instanceDir);
    coreProperties.put( CORE_ABS_INSTDIR, convertToAbsolute(instanceDir, container.getCoreRootDirectory()));

    for (String propname : coreProps.stringPropertyNames()) {

      String propvalue = coreProps.getProperty(propname);

      // 除了那些规定好的属性名外,其他的属性名视为用户自定义的属性(即"User defined properties from core.properties")
      if (isUserDefinedProperty(propname))
        originalExtraProperties.put(propname, propvalue);
      else
        originalCoreProperties.put(propname, propvalue);

      if (!requiredProperties.contains(propname))   // Required props are already dealt with
        coreProperties.setProperty(propname,
            PropertiesUtil. substituteProperty(propvalue, containerProperties));
    }

    // 加载solrcore.properties
    loadExtraProperties();
    // 构建可替换的属性,其实就是把那几个标准属性(standardPropNames)都加上前缀“solr.core.”,然后放substitutableProperties里。
    // substitutableProperties:The properties for this core, substitutable by resource loaders
    buildSubstitutableProperties();

    // TODO maybe make this a CloudCoreDescriptor subclass?
    if (container.isZooKeeperAware()) {
      // 如果是cloud模式,在new个CloudDescriptor实例
      cloudDesc = new CloudDescriptor( name, coreProperties, this );
      if (params != null) {
        cloudDesc.setParams( params);
      }
    }
    else {
      cloudDesc = null;
    }
  }




  /**
   * Load properties specified in an external properties file.
   *
   * The file to load can be specified in a {@code properties} property on
   * the original Properties object used to create this CoreDescriptor.  If
   * this has not been set, then we look for {@code conf/solrcore.properties}
   * underneath the instance dir.
   *
   * File paths are taken as read from the core's instance directory
   * if they are not absolute.
   */
  protected void loadExtraProperties() {
    // properties属性,不存在就取默认的conf\solrcore.properties
    String filename = coreProperties .getProperty(CORE_PROPERTIES, DEFAULT_EXTERNAL_PROPERTIES_FILE );
    // 绝对路径生成file
    File propertiesFile = resolvePaths(filename);
    if (propertiesFile.exists()) {
      FileInputStream in = null;
      try {
        in = new FileInputStream(propertiesFile);
        Properties externalProps = new Properties();
        externalProps.load( new InputStreamReader(in, "UTF-8"));
        // 文件存在,读进来,都放coreProperties,solrcore.properties和core.properties放一起了。
        coreProperties.putAll(externalProps);
      } catch (IOException e) {
        String message = String. format(Locale.ROOT, "Could not load properties from %s: %s:" ,
            propertiesFile.getAbsoluteFile(), e.toString());
        throw new SolrException(SolrException.ErrorCode .SERVER_ERROR, message);
      } finally {
        IOUtils. closeQuietly(in);
      }
    }
  }
找到了core之后,开始多线程创建core,创建core时还是从本地文件加载:createFromLocal,创建时,先加载solrconfig.xml,封装为SolrConfig对象,solrconfig.xml中定义了许多配置参数,这些都是作为PluginInfo保存在一个LinkedHashMap里;之后,再加载schema.xml,封装为IndexSchema对象;最后返回一个新的SolrCore实例,在新建SolrCore实例的时候做了很多工作,先看下createFromLocal方法:
  // Helper method to separate out creating a core from local configuration files. See create()
  private SolrCore createFromLocal(String instanceDir, CoreDescriptor dcore) {
    SolrResourceLoader solrLoader = null;

    SolrConfig config = null;
    solrLoader = new SolrResourceLoader(instanceDir, loader.getClassLoader(), dcore.getSubstitutableProperties());
    try {
      // SolrConfig,封装了solrconfig.xml里面的属性
      config = new SolrConfig(solrLoader, dcore.getConfigName(), null);
    } catch (Exception e) {
      log.error("Failed to load file {}", new File(instanceDir, dcore.getConfigName()).getAbsolutePath());
      throw new SolrException(ErrorCode.SERVER_ERROR,
          "Could not load config file " + new File(instanceDir, dcore.getConfigName()).getAbsolutePath(),
          e);
    }

    // 开始加载schema.xml
    IndexSchema schema = null;
    if (indexSchemaCache != null) {
      final String resourceNameToBeUsed = IndexSchemaFactory.getResourceNameToBeUsed(dcore.getSchemaName(), config);
      File schemaFile = new File(resourceNameToBeUsed);
      if (!schemaFile.isAbsolute()) {
        schemaFile = new File(solrLoader.getConfigDir(), schemaFile.getPath());
      }
      if (schemaFile.exists()) {
        String key = schemaFile.getAbsolutePath()
            + ":"
            + new SimpleDateFormat("yyyyMMddHHmmss", Locale.ROOT).format(new Date(
            schemaFile.lastModified()));
        schema = indexSchemaCache.get(key);
        if (schema == null) {
          log.info("creating new schema object for core: " + dcore.getName());
          schema = IndexSchemaFactory.buildIndexSchema(dcore.getSchemaName(), config);
          indexSchemaCache.put(key, schema);
        } else {
          log.info("re-using schema object for core: " + dcore.getName());
        }
      }
    }

    if (schema == null) {
      schema = IndexSchemaFactory.buildIndexSchema(dcore.getSchemaName(), config);
    }

    SolrCore core = new SolrCore(dcore.getName(), null, config, schema, dcore);

    if (core.getUpdateHandler().getUpdateLog() != null) {
      // always kick off recovery if we are in standalone mode.
      core.getUpdateHandler().getUpdateLog().recoverFromLog();
    }
    return core;
  }
solrconfig.xml的加载是在实例化SolrConfig时来完的,里面的Handler,Component等属性片段都是以plugin的形式封装的:
   /** Creates a configuration instance from a resource loader, a configuration name and a stream.
   * If the stream is null, the resource loader will open the configuration stream.
   * If the stream is not null, no attempt to load the resource will occur (the name is not used).
   *@param loader the resource loader
   *@param name the configuration name
   *@param is the configuration stream
   */
  public SolrConfig(SolrResourceLoader loader, String name, InputSource is)
  throws ParserConfigurationException, IOException, SAXException {
    // 父类Config的构造方法,这个Config主要属性有Document,prefix,name,SolrResourceLoader等。prefix是第四个参数传入的"/config/"
    super(loader, name, is, "/config/“);
    // 里面吧所有的lib标签里的jar加入到SolrResourceLoader
    initLibs();
    luceneMatchVersion = getLuceneVersion("luceneMatchVersion");
    String indexConfigPrefix;

    // Old indexDefaults and mainIndex sections are deprecated and fails fast for luceneMatchVersion=>LUCENE_40.
    // For older solrconfig.xml's we allow the old sections, but never mixed with the new <indexConfig>
    boolean hasDeprecatedIndexConfig = (getNode("indexDefaults", false) != null) || (getNode("mainIndex", false) != null);
    boolean hasNewIndexConfig = getNode("indexConfig", false) != null; 
    if(hasDeprecatedIndexConfig){
      if(luceneMatchVersion.onOrAfter(Version.LUCENE_40)) {
        throw new SolrException(ErrorCode.FORBIDDEN, "<indexDefaults> and <mainIndex> configuration sections are discontinued. Use <indexConfig> instead.");
      } else {
        // Still allow the old sections for older LuceneMatchVersion's
        if(hasNewIndexConfig) {
          throw new SolrException(ErrorCode.FORBIDDEN, "Cannot specify both <indexDefaults>, <mainIndex> and <indexConfig> at the same time. Please use <indexConfig> only.");
        }
        log.warn("<indexDefaults> and <mainIndex> configuration sections are deprecated and will fail for luceneMatchVersion=LUCENE_40 and later. Please use <indexConfig> instead.");
        defaultIndexConfig = new SolrIndexConfig(this, "indexDefaults", null);
        mainIndexConfig = new SolrIndexConfig(this, "mainIndex", defaultIndexConfig);
        indexConfigPrefix = "mainIndex";
      }
    } else {
      defaultIndexConfig = mainIndexConfig = null;
      indexConfigPrefix = "indexConfig";
    }
    nrtMode = getBool(indexConfigPrefix+"/nrtMode", true);
    // Parse indexConfig section, using mainIndex as backup in case old config is used
    // indexConfig里面的配置被封装为SolrIndexConfig
    indexConfig = new SolrIndexConfig(this, "indexConfig", mainIndexConfig);
  
    booleanQueryMaxClauseCount = getInt("query/maxBooleanClauses", BooleanQuery.getMaxClauseCount());
    log.info("Using Lucene MatchVersion: " + luceneMatchVersion);

    // Warn about deprecated / discontinued parameters
    // boolToFilterOptimizer has had no effect since 3.1 
    if(get("query/boolTofilterOptimizer", null) != null)
      log.warn("solrconfig.xml: <boolTofilterOptimizer> is currently not implemented and has no effect.");
    if(get("query/HashDocSet", null) != null)
      log.warn("solrconfig.xml: <HashDocSet> is deprecated and no longer recommended used.");

// TODO: Old code - in case somebody wants to re-enable. Also see SolrIndexSearcher#search()
//    filtOptEnabled = getBool("query/boolTofilterOptimizer/@enabled", false);
//    filtOptCacheSize = getInt("query/boolTofilterOptimizer/@cacheSize",32);
//    filtOptThreshold = getFloat("query/boolTofilterOptimizer/@threshold",.05f);
   
    useFilterForSortedQuery = getBool("query/useFilterForSortedQuery", false);
    queryResultWindowSize = Math.max(1, getInt("query/queryResultWindowSize", 1));
    queryResultMaxDocsCached = getInt("query/queryResultMaxDocsCached", Integer.MAX_VALUE);
    enableLazyFieldLoading = getBool("query/enableLazyFieldLoading", false);

   
    filterCacheConfig = CacheConfig.getConfig(this, "query/filterCache");
    queryResultCacheConfig = CacheConfig.getConfig(this, "query/queryResultCache");
    documentCacheConfig = CacheConfig.getConfig(this, "query/documentCache");
    CacheConfig conf = CacheConfig.getConfig(this, "query/fieldValueCache”);
    // fieldValueCache如果不指定,会生成一个默认的
    if (conf == null) {
      Map<String,String> args = new HashMap<String,String>();
      args.put("name","fieldValueCache");
      args.put("size","10000");
      args.put("initialSize","10");
      args.put("showItems","-1");
      conf = new CacheConfig(FastLRUCache.class, args, null);
    }
    fieldValueCacheConfig = conf;
    unlockOnStartup = getBool(indexConfigPrefix+"/unlockOnStartup", false);
    useColdSearcher = getBool("query/useColdSearcher",false);
    dataDir = get("dataDir", null);
    if (dataDir != null && dataDir.length()==0) dataDir=null;

    userCacheConfigs = CacheConfig.getMultipleConfigs(this, "query/cache");

    // autowarming时将旧缓存移到新缓存时所用
    org.apache.solr.search.SolrIndexSearcher.initRegenerators(this);

    hashSetInverseLoadFactor = 1.0f / getFloat("//HashDocSet/@loadFactor",0.75f);
    hashDocSetMaxSize= getInt("//HashDocSet/@maxSize",3000);

    // http缓存
    httpCachingConfig = new HttpCachingConfig(this);
   
    Node jmx = getNode("jmx", false);
    if (jmx != null) {
      jmxConfig = new JmxConfiguration(true,
                                       get("jmx/@agentId", null),
                                       get("jmx/@serviceUrl", null),
                                       get("jmx/@rootName", null));
                                          
    } else {
      jmxConfig = new JmxConfiguration(false, null, null, null);
    }
     maxWarmingSearchers = getInt("query/maxWarmingSearchers",Integer.MAX_VALUE);

     // 各种片段都作为plugin封装,然后每个的plugin的class属性所对应实现或继承的Interface或abstract类的名称为key存储在一个map里。
     // 这些属性一般都会从借口或父类继承一个init方法,在后面实例化时把xml中配置的属性init到实例里。
     loadPluginInfo(SolrRequestHandler.class,"requestHandler",
                    REQUIRE_NAME, REQUIRE_CLASS, MULTI_OK);
     loadPluginInfo(QParserPlugin.class,"queryParser",
                    REQUIRE_NAME, REQUIRE_CLASS, MULTI_OK);
     loadPluginInfo(QueryResponseWriter.class,"queryResponseWriter",
                    REQUIRE_NAME, REQUIRE_CLASS, MULTI_OK);
     loadPluginInfo(ValueSourceParser.class,"valueSourceParser",
                    REQUIRE_NAME, REQUIRE_CLASS, MULTI_OK);
     loadPluginInfo(TransformerFactory.class,"transformer",
                    REQUIRE_NAME, REQUIRE_CLASS, MULTI_OK);
     loadPluginInfo(SearchComponent.class,"searchComponent",
                    REQUIRE_NAME, REQUIRE_CLASS, MULTI_OK);

     // TODO: WTF is up with queryConverter???
     // it aparently *only* works as a singleton? - SOLR-4304
     // and even then -- only if there is a single SpellCheckComponent
     // because of queryConverter.setAnalyzer
     loadPluginInfo(QueryConverter.class,"queryConverter",
                    REQUIRE_NAME, REQUIRE_CLASS);

     // this is hackish, since it picks up all SolrEventListeners,
     // regardless of when/how/why they are used (or even if they are 
     // declared outside of the appropriate context) but there's no nice 
     // way around that in the PluginInfo framework
     loadPluginInfo(SolrEventListener.class, "//listener",
                    REQUIRE_CLASS, MULTI_OK);

     loadPluginInfo(DirectoryFactory.class,"directoryFactory",
                    REQUIRE_CLASS);
     loadPluginInfo(IndexDeletionPolicy.class,indexConfigPrefix+"/deletionPolicy",
                    REQUIRE_CLASS);
     loadPluginInfo(CodecFactory.class,"codecFactory",
                    REQUIRE_CLASS);
     loadPluginInfo(IndexReaderFactory.class,"indexReaderFactory",
                    REQUIRE_CLASS);
     loadPluginInfo(UpdateRequestProcessorChain.class,"updateRequestProcessorChain",
                    MULTI_OK);
     loadPluginInfo(UpdateLog.class,"updateHandler/updateLog");
     loadPluginInfo(IndexSchemaFactory.class,"schemaFactory",
                    REQUIRE_CLASS);

     updateHandlerInfo = loadUpdatehandlerInfo();
    
     multipartUploadLimitKB = getInt(
         "requestDispatcher/requestParsers/@multipartUploadLimitInKB", 2048 );
    
     formUploadLimitKB = getInt(
         "requestDispatcher/requestParsers/@formdataUploadLimitInKB", 2048 );
    
     enableRemoteStreams = getBool(
         "requestDispatcher/requestParsers/@enableRemoteStreaming", false );
 
     // Let this filter take care of /select?xxx format
     handleSelect = getBool(
         "requestDispatcher/@handleSelect", true );
    
     addHttpRequestToContext = getBool(
         "requestDispatcher/requestParsers/@addHttpRequestToContext", false );

    solrRequestParsers = new SolrRequestParsers(this);
    Config.log.info("Loaded SolrConfig: " + name);
  }
schema.xml的加载没有特殊的地方,如果缓存中没有,就通过IndexSchemaFactory.buildIndexSchema加载,到此每个core的solrconfig.xml和schema.xml就加载完了。
(待续)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值