spark 累加器的使用探索

spark 累加器的使用探索

 

1 spark不能在遍历rdd过程中修改全局map

2 spark 提供的累加器的使用

2.1 了解累加器

2.2 spark 提供的累加器的使用

2.3 完整代码

3 自定义累加器

3.1 如何自定义和使用累加器

3.2 自定义累加器和使用的完整代码

3.2.1 自定义累加器代码

3.2.2 使用自己累加器代码


 

1 spark不能在遍历rdd过程中修改全局map

引出问题

① 我定义了一个全局的map

val map2 = mutable.Map[String,Int]()

② 然后构造一个rdd数据源入下:

/**数据源:("门店id", 招募会员时间, 招募人数,"招募渠道") */
val lst = List(
  ("shopId001", 20200502, 2,"b"), //门店 shopId001,20200502这一天b渠道招募了2人
  ("shopId001", 20200503, 3,"c"), //门店 shopId001,20200503这一天c渠道招募了3人
  ("shopId001", 20200501, 1,"d"),
  ("shopId001", 20200504, 4,"e"),
  ("shopId002", 20200501, 2,"f"),
  ("shopId002", 20200502, 2,"b"),
  ("shopId002", 20200503, 3,"b")
)
val df: DataFrame = spark.createDataFrame(lst).toDF("shopId","regDate","count","channel")

③ 对数据使用.map()函数然后对全局的map2赋值

.map(r=>{
  val channel = r._4
  if(map1.containsKey(channel)){
    map2 += (channel->0)//符合条件的渠道作为key放到map里,值无所谓我不关注
  }
  println("rdd.map()函数中: "+map2)
  (r._1,r._2,r._3,channel)
})

④ 通过打印测试map2赋值的结果

具体代码如下

package ezr.bigdata.test

import org.apache.spark.sql.{DataFrame, SparkSession}

import scala.collection.JavaConversions._
import scala.collection.mutable


/**
 * @author liuchangfu@easyretailpro.com
 * @date 2020/5/22 17:53
 * @version 1.0
 */
object MyProblem {
  def main(args: Array[String]): Unit = {

    val spark: SparkSession = SparkSession
      .builder()
      .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
      .appName("Test")
      .master("local[*]")
      .getOrCreate()

    /**a,b,c,d,e表示招募会员的5个渠道*/
    val map1 = Map[String,Int]("a"->1,"b"->2,"c"->3,"d"->4,"e"->5)

    /**用于记录有多少渠道招募会员数大于等于1的全局变量*/
    val map2 = mutable.Map[String,Int]()
//    map2 += ("aa"->0)

    /**数据源:("门店id", 招募会员时间, 招募人数,"招募渠道") */
    val lst = List(
      ("shopId001", 20200502, 2,"b"), //门店 shopId001,20200502这一天b渠道招募了2人
      ("shopId001", 20200503, 3,"c"), //门店 shopId001,20200503这一天c渠道招募了3人
      ("shopId001", 20200501, 1,"d"),
      ("shopId001", 20200504, 4,"e"),
      ("shopId002", 20200501, 2,"f"),
      ("shopId002", 20200502, 2,"b"),
      ("shopId002", 20200503, 3,"b")
    )

    val df: DataFrame = spark.createDataFrame(lst).toDF("shopId","regDate","count","channel")
    df.show()
    //rdd 是每个门店每天招募会员的增量数据
    val rdd = df.rdd.map(r=>{
      val shopId = r.getAs[String]("shopId")
      val regDate = r.getAs[Int]("regDate")
      val count = r.getAs[Int]("count")
      val channel = r.getAs[String]("channel")
      (shopId,regDate,count,channel)
    }).map(r=>{
      val channel = r._4
      if(map1.containsKey(channel)){
        map2 += (channel->0)//符合条件的渠道作为key放到map里,值无所谓我不关注
      }
      println("rdd.map()函数中: "+map2)
      (r._1,r._2,r._3,channel)
    })

    /**上面的println("rdd.map()函数中: "+map2)打印结果可以看到:
     *rdd.map()函数中: Map(c -> 0)
     * rdd.map()函数中: Map(e -> 0)
     * rdd.map()函数中: Map(b -> 0)
     * rdd.map()函数中: Map(d -> 0, c -> 0)
     * rdd.map()函数中: Map(b -> 0)
     * rdd.map()函数中: Map(b -> 0)
     * rdd.map()函数中: Map(e -> 0)
     */

    /**添加action算子让spark lazzy  类型的transformation算子执行起来用于累计*/
    rdd.foreach(println)
    /**打印我需要的map*/
    println("map2: "+map2)

    /**
     * 打印结果:  map2: Map()
     */

    /**
     * 为什么map函数循环里可以赋值出来之后就没值了呢?
     * 答:
     *    在spark算子中引用的外部变量,其实是变量的副本,
     *    在算子中对其值进行修改,只是改变副本的值,外部的变量还是没有变。
     *    通俗易懂的讲就是foreach里的变量带不出来的,
     *    除非用map , 将结果作为rdd返回
     */
  }

}

引出的问题:

为什么map函数循环里可以赋值出来之后就没值了呢?
答:
   在spark算子中引用的外部变量,其实是变量的副本,在算子中对其值进行修改,只是改变副本的值,外部的变量还是没有变。
   通俗易懂的讲就是foreach里的变量带不出来的,

那么如何解决这个问题呢?

答:

    ①  除非用map , 将结果作为rdd返回

    ② 使用spark 的共享变量

         a 共享变量有2个“广播变量”和“累加器”

         b 广播变量是只读的不能再循环里继续赋值

         c 所以使用累加器是可以的

2 spark 提供的累加器的使用

    累加器(accumulator)是Spark中提供的一种分布式的变量机制,其原理类似于mapreduce,即分布式的改变,然后聚合这些改变。累加器的一个常见用途是在调试时对作业执行过程中的事件进行计数。

2.1 了解累加器

spark 2.x  提供三种累加器

       ①  Long 类型的累加器 :LongAccumulator

       ②  Double 类型的累加器:DoubleAccumulator

       ③  集合 类型的累加器:CollectionAccumulator

        当然也保留了几个spark1.x 时期的累加器,不过这些累加器已经废弃,不提倡使用,比如:Accumulable

但是同时spark2.x 又提供了 abstract class AccumulatorV2 可以自己实现满足自己想要的累加器

2.2 spark 提供的累加器的使用

 

① 定义四种累加器


    /**定义累加器*/
    val sc: SparkContext = spark.sparkContext
    /**Long 类型的累加器*/
    val myLong: LongAccumulator = sc.longAccumulator("myLong")
    /**Double 类型的累加器*/
    val myDouble: DoubleAccumulator = sc.doubleAccumulator("myDouble")
    /**集合 类型的累加器,CollectionAccumulator实际是list类型的*/
    val myList: CollectionAccumulator[String] = sc.collectionAccumulator("myList")

    /**用于记录有多少渠道招募会员数大于等于1的全局变量,注意这个使用了废弃的方法,不建议使用*/
    val accMap: Accumulable[mutable.HashMap[String, Int], (String, Int)] = sc.accumulableCollection(mutable.HashMap[String,Int]())
    

② 在遍历rdd的过程中对累加器赋值

.map(r=>{
      val channel: String = r._4
      if(map1.containsKey(channel)){
        //map2 += (channel->0)//符合条件的渠道作为key放到map里,值无所谓我不关注
        myLong.add(1)
        myDouble.add(1.1)
        myList.add(channel)
        accMap += channel->0
      }
      (r._1,r._2,r._3,channel)
    })

③ 接下来分下面4种情况打印累加器的结果

第一次

  /**1 添加action算子让spark lazzy  类型的transformation算子执行起来用于累计*/
//    rdd.foreach(println)
    println("myLong: "+myLong.value)
    println("myDouble: "+myDouble.value)
    println("myList: "+myList.value)


    /**如果把  rdd.foreach(println) 注释掉,打印结果就是如下这样的
     * 打印结果:
     *      myLong: 0
     *      myDouble: 0.0
     *      myList: []
     *
     * 这是因为:
     *  没有执行action算子,
     *  spark lazzy  类型的transformation算子并不执行,
     *  不会去循环每条数据然后使累加器有累加的机会
     *
     *
     */

第二次

/** 2 添加action算子让spark lazzy  类型的transformation算子执行起来用于累计*/
    rdd.foreach(println)
    println("myLong: "+myLong.value)
    println("myDouble: "+myDouble.value)
    println("myList: "+myList.value)
    println("map3: "+accMap.value)
 

    /**
     * 打印结果:
     *      myLong: 6
     *      myDouble: 6.6
     *      myList: [b, b, b, c, d, e]
     *      accMap: Map(e -> 0, b -> 0, d -> 0, c -> 0)
     */

第三次

/** 3 再添加一个action算子让spark lazzy  类型的transformation算子执行了第二次累加计算*/
    rdd.cache()  //cache()是action算子
    println("myLong: "+myLong.value)
    println("myDouble: "+myDouble.value)
    println("myList: "+myList.value)


    /**
     * 打印结果:
     *      myLong: 12
     *      myDouble: 13.2
     *      myList: [b, b, b, c, d, e, b, b, b, e, c, d]
     *
     *  可见累加器会导致一个问题就是累加超出了实际的计数值了
     *  这是因为每次执行action算子RDD里面的数据就会重复去去一次计算一次上面使用了2次action算子
     *  所以计算结果是实际的2倍了,所以使用的时候要避免这样的问题
     *  如何避免呢?
     *    答:
     *      在使用累加器进行累加操作后得到的RDD身上进行cache()
     *      把结果缓存到内存中这样就不会去重复计算了累加器的累加过程也不会重复执行多次了
     *
     *      下面进行测试:
     *        按照上面结果和说明可以知道到这个位置累加器的结果是
     *        myLong: 12
     *        myDouble: 13.2
     *        myList: [b, b, b, c, d, e, b, b, b, e, c, d]
     *        由于我上面使用的action算子是cache()
     *        所以下面再使用action算子的时候就 不会再去取数据计算了,因此累加器的结果不会再继续类加。
     *
     *
     */

第四次

/** 4 再添加一个action算子,因为上面使用了cache()所以这次使用action算子不会在累加*/
    println(rdd.take(3).toList)
    println("myLong: "+myLong.value)
    println("myDouble: "+myDouble.value)
    println("myList: "+myList.value)

    /**
     * 打印结果:
     *      myLong: 12
     *      myDouble: 13.2
     *      myList: [b, b, b, c, d, e, b, b, b, e, c, d]
     *
     *  可见第4次使用action算子的时候没有继续对累加器进行累加所以cache()可以避免多次重复累加的问题
     */

2.3 完整代码

package ezr.bigdata.test

import java.util

import com.google.common.collect.Sets
import org.apache.spark.{Accumulable, SparkContext}
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.util.{CollectionAccumulator, DoubleAccumulator, LongAccumulator}

import scala.collection.mutable
import scala.collection.JavaConversions._


/**
 * @author liuchangfu@easyretailpro.com
 * @date 2020/5/22 17:53
 * @version 1.0
 *          累加器的使用
 *          Spark内置了三种类型的Accumulator,
 *          分别是LongAccumulator用来累加整数型,
 *          DoubleAccumulator用来累加浮点型,
 *          CollectionAccumulator用来累加集合元素。
 */
object AccumulatorTest1 {
  def main(args: Array[String]): Unit = {

    val spark: SparkSession = SparkSession
      .builder()
      .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
      .appName("Test")
      .master("local[*]")
      .getOrCreate()

    /**a,b,c,d,e表示招募会员的5个渠道*/
    val map1 = Map[String,Int]("a"->1,"b"->2,"c"->3,"d"->4,"e"->5)


//    map2 += ("aa"->0)

    /**数据源:("门店id", 招募会员时间, 招募人数,"招募渠道") */
    val lst = List(
      ("shopId001", 20200502, 2,"b"), //门店 shopId001,20200502这一天b渠道招募了2人
      ("shopId001", 20200503, 3,"c"), //门店 shopId001,20200503这一天c渠道招募了3人
      ("shopId001", 20200501, 1,"d"),
      ("shopId001", 20200504, 4,"e"),
      ("shopId002", 20200501, 2,"f"),
      ("shopId002", 20200502, 2,"b"),
      ("shopId002", 20200503, 3,"b")
    )

    /**定义累加器*/
    val sc: SparkContext = spark.sparkContext
    /**Long 类型的累加器*/
    val myLong: LongAccumulator = sc.longAccumulator("myLong")
    /**Double 类型的累加器*/
    val myDouble: DoubleAccumulator = sc.doubleAccumulator("myDouble")
    /**集合 类型的累加器*/
    val myList: CollectionAccumulator[String] = sc.collectionAccumulator("myList")

    /**用于记录有多少渠道招募会员数大于等于1的全局变量,注意这个使用了废弃的方法,不建议使用*/
    val accMap: Accumulable[mutable.HashMap[String, Int], (String, Int)] = sc.accumulableCollection(mutable.HashMap[String,Int]())

    val df: DataFrame = spark.createDataFrame(lst).toDF("shopId","regDate","count","channel")
    df.show()
    //rdd 是每个门店每天招募会员的增量数据
    val rdd = df.rdd.map(r=>{
      val shopId = r.getAs[String]("shopId")
      val regDate = r.getAs[Int]("regDate")
      val count = r.getAs[Int]("count")
      val channel = r.getAs[String]("channel")
      (shopId,regDate,count,channel)
    }).map(r=>{
      val channel: String = r._4
      if(map1.containsKey(channel)){
        //map2 += (channel->0)//符合条件的渠道作为key放到map里,值无所谓我不关注
        myLong.add(1)
        myDouble.add(1.1)
        myList.add(channel)
        accMap += channel->0
      }
      (r._1,r._2,r._3,channel)
    })

    /**1 添加action算子让spark lazzy  类型的transformation算子执行起来用于累计*/
//    rdd.foreach(println)
    println("myLong: "+myLong.value)
    println("myDouble: "+myDouble.value)
    println("myList: "+myList.value)


    /**如果把  rdd.foreach(println) 注释掉,打印结果就是如下这样的
     * 打印结果:
     *      myLong: 0
     *      myDouble: 0.0
     *      myList: []
     *
     * 这是因为:
     *  没有执行action算子,
     *  spark lazzy  类型的transformation算子并不执行,
     *  不会去循环每条数据然后使累加器有累加的机会
     *
     *
     */

    /** 2 添加action算子让spark lazzy  类型的transformation算子执行起来用于累计*/
    rdd.foreach(println)
    println("myLong: "+myLong.value)
    println("myDouble: "+myDouble.value)
    println("myList: "+myList.value)
    println("accMap: "+accMap.value)

    /**
     * 打印结果:
     *      myLong: 6
     *      myDouble: 6.6
     *      myList: [b, b, b, c, d, e]
     */


    /** 3 再添加一个action算子让spark lazzy  类型的transformation算子执行了第二次累加计算*/
    rdd.cache()  //cache()是action算子
    println("myLong: "+myLong.value)
    println("myDouble: "+myDouble.value)
    println("myList: "+myList.value)


    /**
     * 打印结果:
     *      myLong: 12
     *      myDouble: 13.2
     *      myList: [b, b, b, c, d, e, b, b, b, e, c, d]
     *
     *  可见累加器会导致一个问题就是累加超出了实际的计数值了
     *  这是因为每次执行action算子RDD里面的数据就会重复去去一次计算一次上面使用了2次action算子
     *  所以计算结果是实际的2倍了,所以使用的时候要避免这样的问题
     *  如何避免呢?
     *    答:
     *      在使用累加器进行累加操作后得到的RDD身上进行cache()
     *      把结果缓存到内存中这样就不会去重复计算了累加器的累加过程也不会重复执行多次了
     *
     *      下面进行测试:
     *        按照上面结果和说明可以知道到这个位置累加器的结果是
     *        myLong: 12
     *        myDouble: 13.2
     *        myList: [b, b, b, c, d, e, b, b, b, e, c, d]
     *        由于我上面使用的action算子是cache()
     *        所以下面再使用action算子的时候就 不会再去取数据计算了,因此累加器的结果不会再继续类加。
     *
     *
     */


    /** 4 再添加一个action算子,因为上面使用了cache()所以这次使用action算子不会在累加*/
    println(rdd.take(3).toList)
    println("myLong: "+myLong.value)
    println("myDouble: "+myDouble.value)
    println("myList: "+myList.value)

    /**
     * 打印结果:
     *      myLong: 12
     *      myDouble: 13.2
     *      myList: [b, b, b, c, d, e, b, b, b, e, c, d]
     *
     *  可见第4次使用action算子的时候没有继续对累加器进行累加所以cache()可以避免多次重复累加的问题
     */

  }

}

3 自定义累加器

 

3.1 如何自定义和使用累加器

①  类继承extends AccumulatorV2[String, String]

        第一个为输入类型,第二个为输出类型

②  覆写抽象方法:

/**isZero: 返回此累加器是否为零值。例如,对于计数器累加器,0是零值;对于列表累加器,Nil是零值。*/
  override def isZero: Boolean = ???
  /**创建此累加器的新副本。*/
  override def copy(): AccumulatorV2[Nothing, Nothing] = ???
  /**重置此累加器,该累加器为零值。即调用' isZero '必须返回true。*/
  override def reset(): Unit = ???
  /**接受输入并进行累积。*/
  override def add(v: Nothing): Unit = ???
  /**将另一个相同类型的累加器合并到这个累加器中并更新它的状态,也就是说,这个累加器应该是就地合并的。*/
  override def merge(other: AccumulatorV2[Nothing, Nothing]): Unit = ???
  /**定义此累加器的当前值,并返回该值*/
  override def value: Nothing = ???

③ 注册一个自己的累加器

    /**声明我自己的累加器*/
    val myAccMap = new MyMapAccumulator()
    /**注册自己的累加器*/
    sc.register(myAccMap,"myAccMap")

④ 使用自己的累加器

.map(r=>{
      val channel: String = r._4
      if(map1.containsKey(channel)){
        //map2 += (channel->0)//符合条件的渠道作为key放到map里,值无所谓我不关注
        myAccMap.add((channel,1)) //累加器累加
      }
      (r._1,r._2,r._3,channel)
    })

⑤ 打印自己累加器累加结果 

/**1 添加action算子让spark lazzy  类型的transformation算子执行起来用于累计*/
    rdd.foreach(println)
    println("myAccMap: "+myAccMap.value) //打印累加结果

/**
  *  myAccMap: {b=1, c=1, d=1, e=1}
  */

3.2 自定义累加器和使用的完整代码

3.2.1 自定义累加器代码

 

package ezr.bigdata.test

import java.util
import java.util.Collections

import org.apache.spark.util.AccumulatorV2
import scala.collection.JavaConversions._

/**
 * @author liuchangfu@easyretailpro.com
 * @date 2020/5/22 22:03
 * @version 1.0
 *
 *
 *          自定义累加器
 *          自定义累加器类型的功能在1.X版本中就已经提供了,但是使用起来比较麻烦,
 *          在2.0版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2
 *          来提供更加友好的自定义类型累加器的实现方式。官方同时给出了一个实现的示例:CollectionAccumulator类,
 *          这个类允许以集合的形式收集spark应用执行过程中的一些信息。
 *          例如,我们可以用这个类收集Spark处理数据时的一些细节,当然,
 *          由于累加器的值最终要汇聚到driver端,为了避免 driver端的outofmemory问题,
 *          需要对收集的信息的规模要加以控制,不宜过大。
 *          实现自定义类型累加器需要继承AccumulatorV2并至少覆写下例中出现的方法,
 *          下面这个累加器可以用于在程序运行过程中收集一些文本类信息,最终以Set[String]的形式返回。
 *
 * 
 * Tuple2[String,Int]:输入值
 * java.util.Map[String,Int]:输出值,这里定义成了java.util.Map[String,Int]
 */
class MyMapAccumulator extends AccumulatorV2[Tuple2[String,Int],java.util.Map[String,Int]]{ //第一个为输入类型,第二个为输出类型

  /**我要构造的HashMap数据集*/
  private val _myMap: util.HashMap[String, Int] = new java.util.HashMap[String,Int]()

  /**isZero: 返回此累加器是否为零值。例如,对于计数器累加器,0是零值;对于列表累加器,Nil是零值。*/
  override def isZero: Boolean = _myMap.isEmpty

  /**第一个为输入类型,第二个为输出类型*/
  /**创建此累加器的新副本。*/
  override def copy(): AccumulatorV2[Tuple2[String,Int],java.util.Map[String,Int]] = {
    val newAcc = new MyMapAccumulator()
    _myMap.synchronized {
      newAcc._myMap.putAll(_myMap)
    }
    newAcc
  }
  /**重置此累加器,该累加器为零值。即调用' isZero '必须返回true。*/
  override def reset(): Unit = _myMap.clear()
  /**接受输入并进行累积。*/
  override def add(v:Tuple2[String,Int]): Unit = _myMap.put(v._1,v._2)
  /**将另一个相同类型的累加器合并到这个累加器中并更新它的状态,也就是说,这个累加器应该是就地合并的。*/
  override def merge(other: AccumulatorV2[Tuple2[String,Int],java.util.Map[String,Int]]): Unit = {
    other match {
      case oth:MyMapAccumulator => _myMap.putAll(oth.value)
    }
  }
  /**定义此累加器的当前值,并返回该值*/
  override def value:java.util.Map[String,Int] = {
    java.util.Collections.unmodifiableMap(_myMap)
  }
}

3.2.2 使用自己累加器代码

package ezr.bigdata.test

import java.util

import com.google.common.collect.Sets
import org.apache.spark.{Accumulable, SparkContext}
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.util.{CollectionAccumulator, DoubleAccumulator, LongAccumulator}

import scala.collection.mutable
import scala.collection.JavaConversions._


/**
 * @author liuchangfu@easyretailpro.com
 * @date 2020/5/22 17:53
 * @version 1.0
 *          累加器的使用
 *          Spark内置了三种类型的Accumulator,
 *          分别是LongAccumulator用来累加整数型,
 *          DoubleAccumulator用来累加浮点型,
 *          CollectionAccumulator用来累加集合元素。
 */
object AccumulatorTest2 {
  def main(args: Array[String]): Unit = {

    val spark: SparkSession = SparkSession
      .builder()
      .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
      .appName("Test")
      .master("local[*]")
      .getOrCreate()

    /**a,b,c,d,e表示招募会员的5个渠道*/
    val map1 = Map[String,Int]("a"->1,"b"->2,"c"->3,"d"->4,"e"->5)


//    map2 += ("aa"->0)

    /**数据源:("门店id", 招募会员时间, 招募人数,"招募渠道") */
    val lst = List(
      ("shopId001", 20200502, 2,"b"), //门店 shopId001,20200502这一天b渠道招募了2人
      ("shopId001", 20200503, 3,"c"), //门店 shopId001,20200503这一天c渠道招募了3人
      ("shopId001", 20200501, 1,"d"),
      ("shopId001", 20200504, 4,"e"),
      ("shopId002", 20200501, 2,"f"),
      ("shopId002", 20200502, 2,"b"),
      ("shopId002", 20200503, 3,"b")
    )

    val sc = spark.sparkContext
    /**声明我自己的累加器*/
    val myAccMap = new MyMapAccumulator()
    /**注册自己的累加器*/
    sc.register(myAccMap,"myAccMap")

    val df: DataFrame = spark.createDataFrame(lst).toDF("shopId","regDate","count","channel")
    df.show()
    //rdd 是每个门店每天招募会员的增量数据
    val rdd = df.rdd.map(r=>{
      val shopId = r.getAs[String]("shopId")
      val regDate = r.getAs[Int]("regDate")
      val count = r.getAs[Int]("count")
      val channel = r.getAs[String]("channel")
      (shopId,regDate,count,channel)
    }).map(r=>{
      val channel: String = r._4
      if(map1.containsKey(channel)){
        //map2 += (channel->0)//符合条件的渠道作为key放到map里,值无所谓我不关注
        myAccMap.add((channel,1)) //累加器累加
      }
      (r._1,r._2,r._3,channel)
    })

    /**1 添加action算子让spark lazzy  类型的transformation算子执行起来用于累计*/
    rdd.foreach(println)
    println("myAccMap: "+myAccMap.value) //打印累加结果

    /**
      *  myAccMap: {b=1, c=1, d=1, e=1}
      */
  }

}

结束

 

 

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读