【spark床头书系列】Spark Sparkcontext原理用法示例源码详解

Spark Sparkcontext原理用法示例源码详解点击这里免费看全文

原理

SparkContext是Spark应用程序与Spark集群交互的主要入口点。它负责管理与集群的连接,并提供执行任务、创建RDD(弹性分布式数据集)、广播变量和累加器等功能。

以下是SparkContext的主要原理:

  1. 初始化:当应用程序创建一个SparkContext对象时,它会初始化配置并设置与集群的连接。这包括创建与集群通信所需的网络通道、启动调度程序等。

  2. 任务调度:SparkContext负责将应用程序中定义的任务划分为可执行的任务集,并将其发送到集群上的执行者进行处理。它使用任务调度程序(TaskScheduler)来管理任务的调度和分配,以及处理任务失败和重新执行失败的任务。

  3. RDD管理:SparkContext负责管理应用程序中的RDD。它跟踪RDD的依赖关系,将RDD划分为一组分区,并在集群上进行数据分片和分发。它还负责缓存和持久化RDD以提高性能。

  4. 数据序列化:SparkContext使用数据序列化来在集群中传输数据。它使用Java序列化或更高效的Kryo序列化来将数据转换为字节流,并在集群节点之间传输。

  5. 资源管理:SparkContext负责管理集群上的计算资源,包括内存、CPU和磁盘等。它通过与集群管理器(如Standalone、YARN或Mesos)进行交互,向集群请求和分配资源。

  6. 广播变量和累加器:SparkContext允许应用程序创建广播变量和累加器。广播变量是可以高效地在集群节点之间广播的只读变量,而累加器是用于在集群节点上进行计数和聚合操作的可变变量。

  7. 错误处理和容错性:SparkContext负责监控集群中任务的执行情况,并处理任务失败和其他错误。它会尝试重新执行失败的任务,并在必要时重启失败的执行者,以确保应用程序的容错性。

总之,SparkContext是Spark应用程序与Spark集群之间通信和协调的核心组件。它提供了与集群交互所需的功能,包括任务调度、RDD管理、数据序列化、资源管理、广播变量和累加器等。通过使用SparkContext,应用程序可以利用Spark的分布式计算能力来处理大规模的数据并实现高性能的数据处理和分析。

示例

累加器和广播

  1. 累加器示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("AccumulatorExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val accum = sc.longAccumulator("MyAccumulator")
val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))

rdd.foreach {
    num =>
  accum.add(num)
}

println(accum.value) // 输出结果为15

输出结果:

15
  1. 广播变量示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("BroadcastExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val data = Array(1, 2, 3, 4, 5)
val broadcastData = sc.broadcast(data)

val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))
val result = rdd.map {
    num =>
  num * broadcastData.value.sum
}

result.foreach(println)

输出结果:

15
30
45
60
75

以上是累加器和广播变量的示例代码。累加器用于在并行操作中进行累加,而广播变量用于在工作节点之间共享变量。根据实际需求,可以使用这两个功能来处理和共享数据。

文件和资源管理

  1. 添加文件示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("AddFileExample").setMaster("local[*]")
val sc = new SparkContext(conf)

sc.addFile("/path/to/file.txt")
  1. 递归添加文件示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("AddFileRecursiveExample").setMaster("local[*]")
val sc = new SparkContext(conf)

sc.addFile("/path/to/directory", recursive = true)
  1. 添加JAR包示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("AddJarExample").setMaster("local[*]")
val sc = new SparkContext(conf)

sc.addJar("/path/to/myjar.jar")
  1. 添加Spark监听器示例:
import org.apache.spark.{
   SparkConf, SparkContext}
import org.apache.spark.scheduler.{
   SparkListener, SparkListenerApplicationStart, SparkListenerApplicationEnd}

val conf = new SparkConf().setAppName("AddSparkListenerExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val listener = new SparkListener {
   
  override def onApplicationStart(applicationStart: SparkListenerApplicationStart): Unit = {
   
    println("Spark application started")
  }

  override def onApplicationEnd(applicationEnd: SparkListenerApplicationEnd): Unit = {
   
    println("Spark application ended")
  }
}

sc.addSparkListener(listener)

应用程序信息和配置

  1. 获取应用程序名称示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("GetAppNameExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val appName = sc.appName
println(s"Application name: $appName")

输出结果:

Application name: GetAppNameExample
  1. 获取应用程序尝试ID示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("GetApplicationAttemptIdExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val appAttemptId = sc.applicationAttemptId
println(s"Application attempt ID: $appAttemptId")

输出结果:

Application attempt ID: Some(appattempt_123456789_0001_0)
  1. 获取应用程序ID示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("GetApplicationIdExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val appId = sc.applicationId
println(s"Application ID: $appId")

输出结果:

Application ID: application_123456789_0001
  1. 获取应用程序部署模式示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("GetDeployModeExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val deployMode = sc.deployMode
println(s"Deploy mode: $deployMode")

输出结果:

Deploy mode: client
  1. 获取Spark配置对象示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("GetConfExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val sparkConf = sc.getConf
println(s"Spark configurations: $sparkConf")

输出结果:

Spark configurations: org.apache.spark.SparkConf@12345678
  1. 设置调用站点信息示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("SetCallSiteExample").setMaster("local[*]")
val sc = new SparkContext(conf)

sc.setCallSite("My call site information")

val callSite = sc.getCallSite()
println(s"Call site: $callSite")

输出结果:

Call site: My call site information

文件和数据源

  1. 从文本文件创建RDD示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("TextFileExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd = sc.textFile("/path/to/textfile.txt")
rdd.foreach(println)
  1. 从键值对形式的文本文件创建RDD示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("WholeTextFilesExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd = sc.wholeTextFiles("/path/to/directory")
rdd.foreach {
    case (filename, content) =>
  println(s"File: $filename\n$content")
}
  1. 使用Hadoop InputFormat创建RDD示例:
import org.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("HadoopInputFormatExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd = sc.hadoopFile("/path/to/hdfsfile", classOf[KeyValueTextInputFormat], classOf[String], classOf[String])
rdd.foreach {
    case (key, value) =>
  println(s"Key: $key, Value: $value")
}
  1. 使用新版Hadoop InputFormat创建RDD示例:
import org.apache.spark.{
   SparkConf, SparkContext}
import org.apache.spark.rdd.NewHadoopRDD

val conf = new SparkConf().setAppName("NewAPIHadoopFileExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd = sc.newAPIHadoopFile("/path/to/hdfsfile", classOf[NewTextInputFormat], classOf[LongWritable], classOf[Text])
val result = rdd.map {
    case (key, value) =>
  (key.get(), value.toString)
}
result.foreach(println)
  1. 从对象文件创建RDD示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("ObjectFileExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd = sc.objectFile("/path/to/objectfile")
rdd.foreach(println)

任务和作业管理

  1. 取消所有作业示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("CancelAllJobsExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))
rdd.foreachPartition {
    iter =>
  // 模拟长时间运行的任务
  Thread.sleep(1000)
}

sc.cancelAllJobs()

输出结果(部分输出):

Job cancelled!
  1. 取消指定ID的作业示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("CancelJobExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))
val jobId = sc.runJob(rdd, (iter: Iterator[Int]) => {
   
  // 模拟长时间运行的任务
  Thread.sleep(1000)
  iter.sum
}).head

sc.cancelJob(jobId)

输出结果(部分输出):

Job cancelled!
  1. 取消指定ID和原因的作业示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("CancelJobWithReasonExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))
val jobId = sc.runJob(rdd, (iter: Iterator[Int]) => {
   
  // 模拟长时间运行的任务
  Thread.sleep(1000)
  iter.sum
}).head

sc.cancelJob(jobId, "Cancelled by user")

输出结果(部分输出):

Job cancelled! Reason: Cancelled by user
  1. 取消指定分组ID的作业示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("CancelJobGroupExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd1 = sc.parallelize(Seq(1, 2, 3, 4, 5))
val rdd2 = sc.parallelize(Seq(6, 7, 8, 9, 10))

sc.setJobGroup("myJobGroup", "My job group description")
rdd1.foreachPartition {
    iter =>
  // 模拟长时间运行的任务
  Thread.sleep(1000)
}

sc.cancelJobGroup("myJobGroup")

输出结果(部分输出):

Job group myJobGroup cancelled!

其他常用方法

  1. 获取或创建SparkContext示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("GetOrCreateExample").setMaster("local[*]")

// 第一种方式:获取或创建默认的SparkContext
val sc1 = SparkContext.getOrCreate(conf)

// 第二种方式:通过传递SparkConf对象来获取或创建SparkContext
val sc2 = SparkContext.getOrCreate()

// 输出两个SparkContext的APP名称
println(s"SparkContext 1 app name: ${
     sc1.appName}")
println(s"SparkContext 2 app name: ${
     sc2.appName}")

输出结果:

SparkContext 1 app name: GetOrCreateExample
SparkContext 2 app name: Spark shell
  1. 停止SparkContext示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("StopSparkContextExample").setMaster("local[*]")
val sc = new SparkContext(conf)

val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))
rdd.foreach(println)

sc.stop()

输出结果(部分输出):

1
2
3
4
5
...
  1. 设置日志级别示例:
import org.apache.spark.{
   SparkConf, SparkContext}

val conf = new SparkConf().setAppName("SetLogLevelExample").setMaster("local[*]")
val sc = new SparkContext(conf)

sc.setLogLevel("WARN")

val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))
rdd.foreach(println)

输出结果(无日志输出)。

用法总结

根据方法的功能和用途,可以将这些方法进行分类总结:

累加器和广播变量相关的方法:

  • accumulable[R, T](initialValue: R)(implicit param: AccumulableParam[R, T]):创建一个可累加的变量。
  • accumulable[R, T](initialValue: R, name: String)(implicit param: AccumulableParam[R, T]):创建一个可累加的变量,并指定名称。
  • accumulator[T](initialValue: T)(implicit param: AccumulatorParam[T]): Accumulator[T]:创建一个累加器。
  • accumulator[T](initialValue: T, name: String)(implicit param: AccumulatorParam[T]):创建一个累加器,并指定名称。
  • broadcast[T: ClassTag](value: T): Broadcast[T]:广播一个值到所有工作节点。

文件和资源管理相关的方法:

  • addFile(path: String): Unit:添加文件到Spark集群上的每个节点。
  • addFile(path: String, recursive: Boolean): Unit:添加文件到Spark集群上的每个节点,支持递归添加。
  • addJar(path: String):添加JAR包到Spark集群上的每个节点。
  • addSparkListener(listener: SparkListenerInterface):添加Spark监听器。

应用程序信息和配置相关的方法:

  • appName: String:获取应用程序名称。
  • applicationAttemptId: Option[String]:获取应用程序尝试ID。
  • applicationId: String:获取应用程序ID。
  • deployMode: String:获取应用程序部署模式。
  • getConf: SparkConf:获取Spark配置对象。
  • setCallSite(shortCallSite: String):设置当前调用站点信息。
  • setCheckpointDir(directory: String):设置检查点目录。
  • setJobDescription(value: String):设置作业描述。
  • setJobGroup(groupId: String, description: String, interruptOnCancel: Boolean = false):设置作业分组。

文件和数据源相关的方法:

  • binaryFiles(path: String, minPartitions: Int = defaultMinPartitions): RDD[(String, PortableDataStream)]:从包含多个二进制文件的目录中创建一个键值对形式的RDD。
  • binaryRecords(path: String, recordLength: Int): RDD[Array[Byte]]:从一个二进制文件中读取记录,并返回一个字节数组的RDD。
  • hadoopFile[K, V, F <: InputFormat[K, V]](path: String):使用Hadoop InputFormat创建一个RDD。
  • newAPIHadoopFile[K, V, F <: NewInputFormat[K, V]](path: String):使用新版Hadoop InputFormat创建一个RDD。
  • objectFile[T: ClassTag](path: String): RDD[T]:从对象文件中读取数据,返回一个RDD。

任务和作业管理相关的方法:

  • cancelAllJobs(): Unit:取消所有正在运行的作业。
  • cancelJob(jobId: Int): Unit:取消指定ID的作业。
  • cancelJob(jobId: Int, reason: String): Unit:取消指定ID的作业,并指定取消原因。
  • cancelJobGroup(groupId: String):取消指定分组ID的作业。
  • cancelStage(stageId: Int): Unit:取消指定ID的阶段。
  • cancelStage(stageId: Int, reason: String): Unit:取消指定ID的阶段,并指定取消原因。
  • runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U]:运行一个作业,返回作业结果。

其他常用方法:

  • getOrCreate(): SparkContext:获取或创建一个SparkContext对象。
  • stop(): Unit:停止SparkContext对象。

以上是对这些方法的简要说明,具体使用时可以参考官方文档和示例代码。

中文源码

/**
  * Spark功能的主要入口点。SparkContext表示与Spark集群的连接,并可用于在该集群上创建RDD、累加器和广播变量。
  *
  * 每个JVM只能有一个活动的SparkContext。在创建新的SparkContext之前,您必须“stop()”活动的SparkContext。
  * 这个限制可能会被移除;详见SPARK-2243了解更多细节。
  *
  * @param config 描述应用程序配置的Spark Config对象。此配置中的任何设置都会覆盖默认配置以及系统属性。
  */
class SparkContext(config: SparkConf) extends Logging {
   

  // 创建SparkContext的调用位置。
  private val creationSite: CallSite = Utils.getCallSite()

  // 如果为true,则在存在多个活动的SparkContext时记录警告而不是抛出异常
  private val allowMultipleContexts: Boolean =
    config.getBoolean("spark.driver.allowMultipleContexts", false)

  // 为了防止同时存在多个活动的SparkContext,将此上下文标记为已开始构建。
  // 注意:这必须放置在SparkContext构造函数的开头。
  SparkContext.markPartiallyConstructed(this, allowMultipleContexts)

  val startTime = System.currentTimeMillis()

  private[spark] val stopped: AtomicBoolean = new AtomicBoolean(false)

  private[spark] def assertNotStopped(): Unit = {
   
    if (stopped.get()) {
   
      val activeContext = SparkContext.activeContext.get()
      val activeCreationSite =
        if (activeContext == null) {
   
          "(没有活动的SparkContext。)"
        } else {
   
          activeContext.creationSite.longForm
        }
      throw new IllegalStateException(
        s"""无法在已停止的SparkContext上调用方法。
           |此已停止的SparkContext是在以下位置创建的:
           |
           |${
     creationSite.longForm}
           |
           |当前活动的SparkContext是在以下位置创建的:
           |
           |$activeCreationSite
         """.stripMargin)
    }
  }

  /**
    * 创建一个从系统属性中加载设置的SparkContext(例如,通过./bin/spark-submit启动)。
    */
  def this() = this(new SparkConf())

  /**
    * 允许直接设置常见Spark属性的另一种构造函数
    *
    * @param master 要连接的集群URL(例如,mesos://host:port,spark://host:port,local[4])。
    * @param appName 你的应用程序的名称,在集群Web UI上显示
    * @param conf 指定其他Spark参数的[[org.apache.spark.SparkConf]]对象
    */
  def this(master: String, appName: String, conf: SparkConf) =
    this(SparkContext.updatedConf(conf, master, appName))

  /**
    * 允许直接设置常见Spark属性的另一种构造函数
    *
    * @param master 要连接的集群URL(例如,mesos://host:port,spark://host:port,local[4])。
    * @param appName 你的应用程序的名称,在集群Web UI上显示。
    * @param sparkHome 安装Spark的集群节点上的位置。
    * @param jars 发送到集群的JAR文件的集合。这些可以是本地文件系统或HDFS、HTTP、HTTPS或FTP URL上的路径。
    * @param environment 设置在工作节点上的环境变量。
    */
  def this(
      master: String,
      appName: String,
      sparkHome: String = null,
      jars: Seq[String] = Nil,
      environment: Map[String, String] = Map()) = {
   
    this(SparkContext.updatedConf(new SparkConf(), master, appName, sparkHome, jars, environment))
  }

  // 当Java代码直接访问SparkContext时需要以下构造函数。请参考SI-4278

  /**
    * 允许直接设置常见Spark属性的另一种构造函数
    *
    * @param master 要连接的集群URL(例如,mesos://host:port,spark://host:port,local[4])。
    * @param appName 你的应用程序的名称,在集群Web UI上显示。
    */
  private[spark] def this(master: String, appName: String) =
    this(master, appName, null, Nil, Map())

  /**
    * 允许直接设置常见Spark属性的另一种构造函数
    *
    * @param master 要连接的集群URL(例如,mesos://host:port,spark://host:port,local[4])。
    * @param appName 你的应用程序的名称,在集群Web UI上显示。
    * @param sparkHome 安装Spark的集群节点上的位置。
    */
  private[spark] def this(master: String, appName: String, sparkHome: String) =
    this(master, appName, sparkHome, Nil, Map())

  /**
    * 允许直接设置常见Spark属性的另一种构造函数
    *
    * @param master 要连接的集群URL(例如,mesos://host:port,spark://host:port,local[4])。
    * @param appName 你的应用程序的名称,在集群Web UI上显示。
    * @param sparkHome 安装Spark的集群节点上的位置。
    * @param jars 发送到集群的JAR文件的集合。这些可以是本地文件系统或HDFS、HTTP、HTTPS或FTP URL上的路径。
    */
  private[spark] def this(master: String, appName: String, sparkHome: String, jars: Seq[String]) =
    this(master, appName, sparkHome, jars, Map())

  // 在Spark驱动程序日志中记录Spark版本
  logInfo(s"正在运行Spark版本$SPARK_VERSION")

  /* ------------------------------------------------------------------------------------- *
   | 私有变量。这些变量保持上下文的内部状态,不可被外部访问。它们是可变的,因为我们希望在构造函数运行时提前将它们初始化为某个中性值,
   | 这样在构造函数仍在运行时调用“stop()”是安全的。
   * ------------------------------------------------------------------------------------- */

  private var _conf: SparkConf = _
  private var _eventLogDir: Option[URI] = None
  private var _eventLogCodec: Option[String] = None
  private var _listenerBus: LiveListenerBus = _
  private var _env: SparkEnv = _
  private var _statusTracker: SparkStatusTracker = _
  private var _progressBar: Option[ConsoleProgressBar] = None
  private var _ui: Option[SparkUI] = None
  private var _hadoopConfiguration: Configuration = _
  private var _executorMemory: Int = _
  private var _schedulerBackend: SchedulerBackend = _
  private var _taskScheduler: TaskScheduler = _
  private var _heartbeatReceiver: RpcEndpointRef = _
  @volatile private var _dagScheduler: DAGScheduler = _
  private var _applicationId: String = _
  private var _applicationAttemptId: Option[String] = None
  private var _eventLogger: Option[EventLoggingListener] = None
  private var _executorAllocationManager: Option[ExecutorAllocationManager] = None
  private var _cleaner: Option[ContextCleaner] = None
  private var _listenerBusStarted: Boolean = false
  private var _jars: Seq[String] = _
  private var _files: Seq[String] = _
  private var _shutdownHookRef: AnyRef = _
  private var _statusStore: AppStatusStore = _

  /* ------------------------------------------------------------------------------------- *
   | 访问器和公共字段。这些提供对上下文的内部状态的访问。
   * ------------------------------------------------------------------------------------- */

  private[spark] def conf: SparkConf = _conf

  /**
    * 返回此SparkContext配置的副本。配置在运行时“不能”更改。
    */
  def getConf: SparkConf = conf.clone()

  def jars: Seq[String] = _jars
  def files: Seq[String] = _files
  def master: String = _conf.get("spark.master")
  def deployMode: String = _conf.getOption("spark.submit.deployMode").getOrElse("client")
  def appName: String = _conf.get("spark.app.name")

  private[spark] def isEventLogEnabled: Boolean = _conf.getBoolean("spark.eventLog.enabled", false)
  private[spark] def eventLogDir: Option[URI] = _eventLogDir
  private[spark] def eventLogCodec: Option[String] = _eventLogCodec

  def isLocal: Boolean = Utils.isLocalMaster(_conf)

  /**
    * @return 如果上下文已停止或正在停止中,则返回true。
    */
  def isStopped: Boolean = stopped.get()

  private[spark] 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

BigDataMLApplication

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

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

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

打赏作者

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

抵扣说明:

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

余额充值