sparkStreaming

spark_day04学习笔记

1、课程目标

  • 1、掌握sparkStreaming原理和架构
  • 2、掌握Dstream常用的操作
  • 3、掌握flume整合SparkStreaming
  • 4、掌握kafka整合SparkStreaming

2、sparkStreaming概述

2.1 sparkStreaming是什么

  • Spark Streaming makes it easy to build scalable fault-tolerant streaming applications
    • SparkStreaming可以非常容易的构建一个可扩展、具有容错机制的流式应用程序

2.2 sparkStreaming特性

  • 1、易用性
    • 可以像开发离线批处理一样去开发实时处理程序
    • 并且可以使用多种不同的语言进行代码开发
      • java
      • scala
      • python
  • 2、容错性
    • 可以保证数据被处理且只被处理一次
      • 恰好一次语义
  • 3、融合到spark体系
    • 可以把sparkstreaming实时处理与批处理和交互式查询结合使用

3、sparkStreaming原理

3.1 sparkStreaming计算原理

sparkStreaming是一个流式的批处理引擎,以某一时间间隔的批量处理,当时间间隔缩短到秒级,我们就可以看成是实时处理。

3.2 sparkStreaming计算流程

sparkStreaming是以某一时间的间隔批量处理,把源源不断产生的数据,按照时间间隔,划分成了很多个短小的作用,每一个作业中都封装一段数据,每一个批次的数据可以看成是一个Dstream.
Dstream内部封装了RDD,rdd内部有很多个分区,分区里面才是真正的数据,后期还是以分区为单位就是一个task线程去运行任务。
Dstream有很多大量的 transformation操作,我们对Dstream做了大量的转换操作,其本质是作用在内部的rdd上也做了类似于这些transformation操作。

3.3 sparkStreaming容错性

Dstream内部是封装了RDD,这里还是可以依赖于RDD自身的容错性:
可以使用血统lineage这层关系来进行重新计算恢复得到某些rdd部分丢失的分区数据。

val rdd2=rdd1.map(x=>(x,1)

要想恢复数据这里是需要这2个条件: 血统+数据源

对于sparkStreaming应用程序来说,在接受在数据之后,它会进行多份拷贝到其他机器,来保证数据源的安全性,后期方便进行数据恢复。

3.4 sparkStreaming实时性

storm是来一条数据就处理一条,实时性是比较高---->从数据源产生数据到最后经过处理之后看到结果数据时间很短。
sparkStreaming是以某一时间间隔的批量处理,数据并不是来一条就处理一条,实时性是比较低,延迟是比较高。


后期如果公司用到了实时处理场景,具体使用哪一个实时处理框架,还是要看一定业务需求:
(1)如果对实时性要求比较高,这里可以优先考虑storm
(2)如果对实时性的要求不是特别高,允许数据出现一定的延迟,这个时候可以优先考虑sparkStreaming实现。

4、Dstream

4.1 Dstream是什么

Discretized Stream是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark算子操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据。

4.2 Dstream操作分类

  • 1、transformation(转换)
    • 可以实现把一个Dstream转换生成一个新的Dstream,它也是延迟加载,不会立即触发任务的运行
      • 它是类似于rdd的transformation
  • 2、outputOperation (输出)
    • 它会触发任务的真正运行
      • 它是类似于rdd中的action

5、Dstream操作实战

  • 1、引入依赖
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.11</artifactId>
            <version>2.1.3</version>
        </dependency>

5.1 利用sparkStreaming接受socket数据实现单词统计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SItP9LJM-1593305789782)(基于单词统计来分析sparkStreaming处理流程.png)]

  • 1、代码开发
package cn.itcast.socket

import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}

//todo:利用sparkStreaming接受socket数据实现单词统计
object SparkStreamingSocket {
  def main(args: Array[String]): Unit = {
      //1、创建SparkConf
        val sparkConf: SparkConf = new SparkConf().setAppName("SparkStreamingSocket").setMaster("local[2]")

     //2、创建SparkContext
        val sc = new SparkContext(sparkConf)
        sc.setLogLevel("warn")

    //3、创建StreamingContext 需要2个参数:第一个是sparkContext对象,第二个是批处理的时间间隔
       val ssc = new StreamingContext(sc,Seconds(5))

    //4、接受socket数据
       val data: ReceiverInputDStream[String] = ssc.socketTextStream("node1",9999)

    //5、切分每一行获取所有的单词
      val words: DStream[String] = data.flatMap(_.split(" "))

    //6、每个单词计为1
      val wordAndOne: DStream[(String, Int)] = words.map((_,1))

    //7、相同单词出现的1累加
      val result: DStream[(String, Int)] = wordAndOne.reduceByKey(_+_)

    //8、打印结果数据
        result.print()

    //9、开启流式计算
      ssc.start()
      ssc.awaitTermination()
  }
}

5.2 利用sparkStreaming接受socket数据实现所有批次单词统计结果累加

  • 1、代码开发
    • updateStateByKey使用
package cn.itcast.socket

import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

//todo:利用sparkStreaming接受socket数据实现所有批次单词统计的结果累加
object SparkStreamingSocketTotal {

   //currentValues:获取当前批次相同的单词出现的所有的1,(hadoop,1)(hadoop,1)(hadoop,1)---->List(1,1,1)
   //historyValues:之前所有的批次每一个单词出现的总次数:Option类型:Some(有值) None(没有)
  def updateFunc(currentValues:Seq[Int], historyValues:Option[Int]):Option[Int] ={
               val newValues: Int = currentValues.sum + historyValues.getOrElse(0)
               Some(newValues)
  }

  def main(args: Array[String]): Unit = {
       //1、创建SparkConf
        val sparkConf: SparkConf = new SparkConf().setAppName("SparkStreamingSocketTotal").setMaster("local[2]")

      //2、创建SparkContext
        val sc = new SparkContext(sparkConf)
        sc.setLogLevel("warn")

     //3、创建StreamingContext
        val ssc = new StreamingContext(sc,Seconds(5))

         //设置checkpoint目录,用于保存每一个单词在之前批次中出现的总次数
        ssc.checkpoint("./socket")

     //4、接受socket数据
        val data: ReceiverInputDStream[String] = ssc.socketTextStream("node1",9999)

    //5、切分每一行获取所有的单词
      val words: DStream[String] = data.flatMap(_.split(" "))

    //6、每个单词计为1
      val wordAndOne: DStream[(String, Int)] = words.map((_,1))

   //7、相同单词出现的1累加
        val result: DStream[(String, Int)] = wordAndOne.updateStateByKey(updateFunc)

    //8、打印
      result.print()

    //9、开启流式计算
      ssc.start()
      ssc.awaitTermination()
  }
}

5.3 利用sparkStreaming接受socket数据,使用开窗函数实现单词统计

  • 1、代码开发
    • reduceByKeyAndWindow
package cn.itcast.socket

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}

//todo;利用sparkStreaming接受socket数据,使用开窗函数实现单词统计
object SparkStreamingSocketWindow {
  def main(args: Array[String]): Unit = {
    //1、创建SparkConf
    val sparkConf: SparkConf = new SparkConf().setAppName("SparkStreamingSocketWindow").setMaster("local[2]")

    //2、创建SparkContext
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、创建StreamingContext 需要2个参数:第一个是sparkContext对象,第二个是批处理的时间间隔
    val ssc = new StreamingContext(sc,Seconds(5))

    //4、接受socket数据
    val data: ReceiverInputDStream[String] = ssc.socketTextStream("node1",9999)

    //5、切分每一行获取所有的单词
    val words: DStream[String] = data.flatMap(_.split(" "))

    //6、每个单词计为1
    val wordAndOne: DStream[(String, Int)] = words.map((_,1))

    //7、相同单词出现的1累加
    //reduceFunc: (V, V) => V,  就是一个函数,自己写一些逻辑
    //windowDuration: Duration,  窗口长度:表示后期要计算多久的数据量
    //slideDuration: Duration    窗口的滑动时间:表示每隔多久计算一次
    val result: DStream[(String, Int)] = wordAndOne.reduceByKeyAndWindow((x:Int,y:Int)=>x+y,Seconds(5),Seconds(10))

    //8、打印结果数据
    result.print()

    //9、开启流式计算
    ssc.start()
    ssc.awaitTermination()
  }
}


5.4 利用sparkStreaming接受socket数据,实现一定时间内的热门词汇统计

  • 1、代码开发
    • transform
package cn.itcast.socket

import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}

//todo:利用sparkStreaming接受socket数据实现一定时间内热门词汇统计
object SparkStreamingSocketWindowHotWords {
  def main(args: Array[String]): Unit = {

       //1、创建SparkConf
        val sparkConf: SparkConf = new SparkConf().setAppName("SparkStreamingSocketWindowHotWords").setMaster("local[2]")

      //2、创建SparkContext
      val sc = new SparkContext(sparkConf)
     sc.setLogLevel("warn")

     //3、创建StreamingContext
     val ssc = new StreamingContext(sc,Seconds(5))

    //4、接受socket数据
        val data: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.72.110",9999)

   //5、实现单词统计
      val result: DStream[(String, Int)] = data.flatMap(_.split(" ")).map((_,1)).reduceByKeyAndWindow((x:Int,y:Int)=>x+y,Seconds(10),Seconds(10))
庆华   庆华   亚明  亚明  亚明   郭冰   卡卡  卡卡  卡卡  卡卡 
庆华,2
亚明,3
郭冰,1
卡卡,4    
排序  求前三    
DStream  = 一个时间段内的rdd
sortby
    卡卡,4
    亚明,3
    庆华,2
    郭冰,1
take
    卡卡,4
    亚明,3
    庆华,2 

   //6、按照单词出现的次数降序
    val finalResult: DStream[(String, Int)] = result.transform(rdd => {
      //通过rdd封装的排序方法进行操作
      val sortRDD: RDD[(String, Int)] = rdd.sortBy(_._2, false)
      //取出出现次数最多的前3位
      val top3: Array[(String, Int)] = sortRDD.take(3)
      println("---------------top3------------start")
      top3.foreach(println)
      println("---------------top3---------------end")

    })

    //7、打印
    finalResult.print()


    ssc.start()
    ssc.awaitTermination()
  }
}

6、sparkStreaming整合flume

6.1 拉模式(优化考虑拉模式)

需要把spark-streaming-flume-sink_2.11-2.1.3.jar放入到flume的lib目录下
同时需要把flume自带的scala2.10这个jar包替换成scala2.11jar包
  • 1、修改flume配置
    • vim flume-poll-spark.conf
a1.sources = r1
a1.sinks = k1
a1.channels = c1

#source
a1.sources.r1.channels = c1
a1.sources.r1.type = spooldir
a1.sources.r1.spoolDir = /root/data
a1.sources.r1.fileHeader = true


#channel
a1.channels.c1.type =memory
a1.channels.c1.capacity = 20000
a1.channels.c1.transactionCapacity=5000


#sinks
a1.sinks.k1.channel = c1
a1.sinks.k1.type = org.apache.spark.streaming.flume.sink.SparkSink
a1.sinks.k1.hostname=node1
a1.sinks.k1.port = 8888
a1.sinks.k1.batchSize= 2000
  • 2、启动flume
bin/flume-ng agent -n a1 -c conf -f conf/flume-poll-spark.conf -Dflume.root.logger=info,console
  • 3、代码开发

    • 1、引入依赖
            <dependency>
                <groupId>org.apache.spark</groupId>
                <artifactId>spark-streaming-flume_2.11</artifactId>
                <version>2.1.3</version>
            </dependency>
    
    • 2、代码开发
    package cn.itcast.flume
    
    import java.net.InetSocketAddress
    
    import org.apache.spark.storage.StorageLevel
    import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
    import org.apache.spark.streaming.{Seconds, StreamingContext}
    import org.apache.spark.{SparkConf, SparkContext}
    import org.apache.spark.streaming.flume.{FlumeUtils, SparkFlumeEvent}
    
    //todo:利用sparkStreaming整合flume----poll拉模式
    object SparkStreamingFlumePoll {
      def main(args: Array[String]): Unit = {
        //1、创建SparkConf
        val sparkConf: SparkConf = new SparkConf().setAppName("SparkStreamingFlumePoll").setMaster("local[2]")
    
        //2、创建SparkContext
        val sc = new SparkContext(sparkConf)
        sc.setLogLevel("warn")
    
        //3、创建StreamingContext
        val ssc = new StreamingContext(sc,Seconds(5))
    
        //4、通过poll拉模式进行整合
        val pollingStream: ReceiverInputDStream[SparkFlumeEvent] = FlumeUtils.createPollingStream(ssc,"node1",8888)
    
        //拉取多个flume收集到的数据
    //      val addresses=List(new InetSocketAddress("node1",8888),new InetSocketAddress("node2",8888),new InetSocketAddress("node3",8888))
    //      val stream: ReceiverInputDStream[SparkFlumeEvent] = FlumeUtils.createPollingStream(ssc,addresses,StorageLevel.MEMORY_AND_DISK_SER_2)
    //
        //5、获取flume中数据
          //flume中数据传输的最小单元:Event  一个event对象中包括 {"headers":xxxx,"body":xxxx}
        val data: DStream[String] = pollingStream.map(x=>new String(x.event.getBody.array()))
    
        //6、切分每一行
          val words: DStream[String] = data.flatMap(_.split(" "))
    
        //7、每个单词计为1
        val wordAndOne: DStream[(String, Int)] = words.map((_,1))
    
        //8、相同的单词出现的1累加
          val result: DStream[(String, Int)] = wordAndOne.reduceByKey(_+_)
    
        //9、打印
          result.print()
    
    ssc.start()
    ssc.awaitTermination()
    

    }

    }

6.2 推模式

  • 1、修改flume配置

    
    

#push mode
a1.sources = r1
a1.sinks = k1
a1.channels = c1
#source
a1.sources.r1.channels = c1
a1.sources.r1.type = spooldir
a1.sources.r1.spoolDir = /root/data
a1.sources.r1.fileHeader = true
#channel
a1.channels.c1.type =memory
a1.channels.c1.capacity = 20000
a1.channels.c1.transactionCapacity=5000
#sinks
a1.sinks.k1.channel = c1
a1.sinks.k1.type = avro
a1.sinks.k1.hostname=192.168.26.39
a1.sinks.k1.port = 8888
a1.sinks.k1.batchSize= 2000

* 2、启动flume

bin/flume-ng agent -n a1 -c conf -f conf/flume-push-spark.conf -Dflume.root.logger=info,console



* 3、代码开发

​~~~scala
package cn.itcast.flume

import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.flume.{FlumeUtils, SparkFlumeEvent}

//todo:利用sparkStreaming整合flume----推模式
object SparkStreamingFlumePush {
  def main(args: Array[String]): Unit = {

    //1、创建SparkConf
    val sparkConf: SparkConf = new SparkConf().setAppName("SparkStreamingFlumePush").setMaster("local[2]")

    //2、创建SparkContext
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、创建StreamingContext
    val ssc = new StreamingContext(sc,Seconds(5))

    //4、实现推模式整合   这里的hostname是sparkStreaming应用程序所在的地址
      val stream: ReceiverInputDStream[SparkFlumeEvent] = FlumeUtils.createStream(ssc,"192.168.26.39",8888)

    //5、获取flume中数据
    //flume中数据传输的最小单元:Event  一个event对象中包括 {"headers":xxxx,"body":xxxx}
    val data: DStream[String] = stream.map(x=>new String(x.event.getBody.array()))

    //6、切分每一行
    val words: DStream[String] = data.flatMap(_.split(" "))

    //7、每个单词计为1
    val wordAndOne: DStream[(String, Int)] = words.map((_,1))

    //8、相同的单词出现的1累加
    val result: DStream[(String, Int)] = wordAndOne.reduceByKey(_+_)

    //9、打印
    result.print()


    ssc.start()
    ssc.awaitTermination()
  }
}

7、sparkStreaming整合kafka

7.1 KafkaUtils.createStream

  • 1、描述

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bKglqFxR-1593305789784)(嗲一套api逻辑.png)]

这里使用了kafka消费者高级api,使用了receiver接受器去接受数据,消息的偏移保存在zk中,默认这种方式会导致数据的丢失,可以开启wal日志之后,把接受到的数据同步写入到分布式文件系统中,保证数据源端的安全,后期可以通过血统+数据源去恢复某些丢失的数据。

但是这种机制它保证不了数据被处理且只被处理一次,也就是说在这种情况下会出现数据的重复处理。
  • 2、代码开发
package cn.itcast.kafka

import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.kafka.KafkaUtils

import scala.collection.immutable

//todo:sparkStreaming整合kafka----基于receiver接受器接受数据,使用消费者高级api(就是把消息的偏移量保存在zk中)
object SparkStreamingKafkaReceiver {
  def main(args: Array[String]): Unit = {
    //1、创建SparkConf
    val sparkConf: SparkConf = new SparkConf()
                                .setAppName("SparkStreamingKafkaReceiver")
                                .setMaster("local[4]")
                               //默认数据会丢失:原因:数据源端的安全性没有得到保障,导致后期通过血统进行恢复的时候拿不到原始的数据,进而恢复不了
                               //开启WAL日志 把最开始接受到的数据同步的写入到分布式文件系统中,保障数据源端的安全性
                                .set("spark.streaming.receiver.writeAheadLog.enable","true")

    //2、创建SparkContext
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、创建StreamingContext
    val ssc = new StreamingContext(sc,Seconds(5))

      //设置checkpoint目录,用于保存接受到的原始数据
        ssc.checkpoint("./spark-receiver")

    //4、设置zk地址
    val zkQuorum="node1:2181,node2:2181,node3:2181"

    //5、设置消费者组id
    val groupId="spark-receiver"

    //6、设置topic相关信息  key:表示topic的名称  value:表示每一个receiver接收器使用多少个线程消费数据
    val topics=Map("itcast" -> 1 )
    //(String, String) Dstream内部每一个元素是一个元组,元组有2位,第一个String表示消息的key,第二个String表示消息的内容
    //val stream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc,zkQuorum,groupId,topics)

    //使用多个receiver接收器去接受数据
    val totalDstream: immutable.IndexedSeq[ReceiverInputDStream[(String, String)]] = (1 to 3).map(x => {
      val stream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc, zkQuorum, groupId, topics)
      stream
    })

    //通过ssc调用union方法,把多个Dstream的结果合并在一起生成一个新的Dstream
     val unionDstream: DStream[(String, String)] = ssc.union(totalDstream)

    //7、获取kafka中的topic数据
      val data: DStream[String] = unionDstream.map(x=>x._2)

    //8、切分每一行
    val words: DStream[String] = data.flatMap(_.split(" "))

    //9、每个单词计为1
    val wordAndOne: DStream[(String, Int)] = words.map((_,1))

    //10、相同的单词出现的1累加
    val result: DStream[(String, Int)] = wordAndOne.reduceByKey(_+_)

    //11、打印
    result.print()


    ssc.start()
    ssc.awaitTermination()
  }
}

7.2 KafkaUtils.creataDirectStream

  • 1、代码开发
package cn.itcast.kafka

import kafka.serializer.StringDecoder
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.kafka.KafkaUtils

//todo:sparkStreaming整合kafka---- 不在基于receiver接收器接收数据,直接连接上kafka集群,把对应的数据获取得到
 //消息的偏移量不在由zk去维护,这里客户端程序自己去保存这个保存
object SparkStreamingKafkaDirect {
  def main(args: Array[String]): Unit = {
    //1、创建SparkConf
    val sparkConf: SparkConf = new SparkConf()
                                  .setAppName("SparkStreamingKafkaDirect")
                                  .setMaster("local[4]")


    //2、创建SparkContext
    val sc = new SparkContext(sparkConf)
    sc.setLogLevel("warn")

    //3、创建StreamingContext
    val ssc = new StreamingContext(sc,Seconds(5))

    //设置一个checkpoint目录,可以保存消息的偏移量
    ssc.checkpoint("./spark-direct")

    //4、指定kafka集群配置信息
      val kafkaParams=Map("bootstrap.servers" ->"node1:9092,node2:9092,node3:9092","group.id" ->"spark-direct")

    //5、指定topic名称
      val topics=Set("itcast")
      val kafkaDstream: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String,String,StringDecoder,StringDecoder](ssc,kafkaParams,topics)

    //6、获取topic的数据
       val data: DStream[String] = kafkaDstream.map(_._2)

    //7、切分每一行
    val words: DStream[String] = data.flatMap(_.split(" "))

    //8、每个单词计为1
    val wordAndOne: DStream[(String, Int)] = words.map((_,1))

    //9、相同的单词出现的1累加
    val result: DStream[(String, Int)] = wordAndOne.reduceByKey(_+_)

    //10、打印
    result.print()


    ssc.start()
    ssc.awaitTermination()
  }
}

8、程序打成jar包提交到集群中运行

spark-submit --master spark://node1:7077 --class cn.itcast.kafka.SparkStreamingKafkaDirect --executor-memory 1g --total-executor-cores 4  spark_class18-1.0-SNAPSHOT.jar



后期再实际工作中,前期都是基于idea开发代码,然后在本地运行,测试下代码处理逻辑对不对,如果有问题,及时修改,如果没有问题,就把代码改造一下,然后提交到集群中运行。

任务在实际运行的时候,需要向master申请资源,这个资源到底申请多少是合理的?
对于sparkStreaming流式处理来说,可以找到一个比较理想的状态?
   就是当前批次时间之内就把该批次数据处理完成。

第一个5s的批次的数据来了:            ---------------------------------------> 需要1分钟
第二个5s的批次的数据来了:         ---------------------------------------> 需要1分钟
第三个5s的批次的数据来了:       ---------------------------------------> 需要1分钟
....
数据会出现大量的积压,最后数据处理的延迟是非常高。


下面进行测试,先给定一些资源,然后把程序提交到spark集群中运行,随后访问spark集群的web管理界面
  master主机名:8080  可以观察到任务运行的状态
  
  
  测试参数
  --executor-memory 10g --total-executor-cores 20
  --executor-memory 10g --total-executor-cores 30
  --executor-memory 15g --total-executor-cores 30
  --executor-memory 15g --total-executor-cores 40
  

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值