Spark2.X弹性分布式数据集

一、三大弹性分布式数据集介绍

RDD、DataFrame、DataSet是Spark平台下的弹性分布式数据集,为高效处理超大型数据集提供便利。

RDD
优点:

  • 编译时类型安全,编译时就能检查出类型错误
  • 面向对象的编程风格,直接通过类名点的方式来操作数据

缺点:

  • 序列化和反序列化的性能开销,无论是集群间的通信、还是IO操作都需要对对象的结构和数据进行序列化和反序列化
  • GC的性能开销,频繁的创建和销毁对象,势必会增加GC

DataFrame
DataFrame引入了schema和off-heap

  • schema:RDD中每一个元素的结构都是一致的,在DataFrame中这个结构就存储在schema中. 因此,在通信和IO中,就只用序列化和反序列化数据,而结构的部分就可以省略了
  • off-heap:意味着DataFrame使用了「JVM堆以外」的内存,这些内存直接受操作系统管理(而不是JVM),Spark能够以二进制的形式序列化数据(不包括结构)到off-heap中,当要操作数据时,就直接操作off-heap内存

通过off-heap,spark一定程度上脱离了JVM的控制,也就极大程度上免于GC的困扰. 通过schema和off-heap,DataFrame解决了RDD的缺点,但是却丢了RDD的优点, DataFrame不是类型安全的, API也不是面向对象风格的。

DataSet
DataSet结合了RDD和DataFrame的优点,并带来的一个新的概念Encoder

当序列化数据时,Encoder产生字节码与off-heap进行交互,能够达到按需访问数据的效果,而不用反序列化整个对象。

二、Spark RDD概述与创建方式

学习之前可以先了解RDD编程指南:
http://spark.apache.org/docs/2.4.6/rdd-programming-guide.html

在较高级别上,每个Spark应用程序都包含一个驱动程序,该程序运行用户的main功能并在集群上执行各种并行操作。Spark提供的主要抽象是弹性分布式数据集(RDD),它是跨集群节点划分的元素的集合,可以并行操作。 通过从Hadoop文件系统(或任何其他Hadoop支持的文件系统)中的文件或驱动程序中现有的Scala集合开始并进行转换来创建RDD。用户还可以要求Spark将RDD 保留在内存中,以使其能够在并行操作中有效地重用。最后,RDD会自动从节点故障中恢复。

(1)连接Spark

默认情况下,Spark 2.4.6已构建并分发为可与Scala 2.12一起使用。(可以将Spark构建为与其他版本的Scala一起使用。)要在Scala中编写应用程序,您将需要使用兼容的Scala版本(例如2.12.X)。

要编写Spark应用程序,您需要在Spark上添加Maven依赖项。可通过Maven Central在以下位置获得Spark:

groupId = org.apache.spark
artifactId = spark-core_2.12
version = 2.4.6

另外,如果您想访问HDFS群集,则需要hadoop-client为您的HDFS版本添加依赖项 。

groupId = org.apache.hadoop
artifactId = hadoop-client
version = <your-hdfs-version>

最后,您需要将一些Spark类导入程序。添加以下行:

import org.apache.spark.SparkContext
import org.apache.spark.SparkConf

(2)初始化Spark

Spark程序必须做的第一件事是创建一个SparkContext对象,该对象告诉Spark如何访问集群。要创建一个,SparkContext您首先需要构建一个SparkConf对象,其中包含有关您的应用程序的信息。在spark2.0以后也可以通过初始化SparkSession来创建rdd和dataset。
每个JVM只能激活一个SparkContext。stop()在创建新的SparkContext之前,您必须先激活它。

val conf = new SparkConf().setAppName(appName).setMaster(master)
new SparkContext(conf)

该appName参数是您的应用程序显示在集群UI上的名称。 master是Spark,Mesos或YARN群集URL或特殊的“local”字符串,以本地模式运行。实际上,当在集群上运行时,您将不希望master在程序中进行硬编码,而是在其中启动应用程序spark-submit并在其中接收。但是,对于本地测试和单元测试,您可以传递“ local”以在内部运行Spark。

关于spark rdd可以参考这篇文章:
https://blog.csdn.net/weixin_45366499/article/details/108676602

三、Spark RDD五大特性

spark rdd的五大特性:
官方文档给出的解释

  • A list of partitions
  • A function for computing each split
  • A list of dependencies on other RDDs
  • Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
  • Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)

中文翻译

  • 一个数据集被拆分成partition,这是进行并行计算的基础

partition 是 RDD 的基本组成单位,对于 RDD 来说,每个 partition 都会被一个Task处理,并决定并行计算的粒度,用户可以在创建 RDD 时指定 RDD 的 partition 个数,如果没有指定,那么就会采用默认值,默认值就是程序所分配到的CPU core的数目每个 partition 的存储是由BlockManager 实现的,每个 partition 都会被逻辑映射成BlockManager 的一个 Block ,而这个 Block 会被一个 Task 负责计算。

  • 作用在 RDD 的函数,会作用到每一个 partition

Spark中的 RDD 的计算是以 partition 为单位的,每个 RDD 都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。

  • RDD之间的依赖,依赖还具体分为宽依赖和窄依赖,但并不是所有的RDD都有依赖。

RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。

重点要掌握前面三个特性,可以一句话总结:spark以RDD为核心的抽象弹性分布式数据集,这个数据集是以分区的方式运行在cluster node节点上,并且每一个分区都有一个计算任务对数据进行计算,每一个RDD数据之间相互依赖,这样依赖关系能够保证我们的数据RDD故障自动恢复。

  • 当我们的rdd是key-value的这种结构的时候进行partitioner的时候,所用到的算法是HashPartitioner

key-value型的RDD是根据哈希来分区的,类似于mapreduce当中的paritioner接口,控制key分到哪个reduce。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个基于范围的RangePartitioner。只有对于key-value的RDD,才会有Partitioner,非key-value的RDD的Partitioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDDShuffle输出时的分片数量。

  • 每一分片的优先计算位置,比如HDFS的block的所在位置应该是优先计算的位置。
    对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。

四、Spark RDD操作方式及使用

关于DRR的详细操作请看这篇博客,这里只做大概讲解:
https://blog.csdn.net/weixin_45366499/article/details/108676602
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Spark RDD操作方式的综合使用
准备数据集

val rdd1 = sc.textFile("file:///opt/datas/stu.txt")
val rdd2 = sc.textFile("file:///opt/datas/stu1.txt")

合并数据集操作

val allRdd = rdd1.union(rdd2)

将数据按空格分开

val lines = allRdd.flatMap(x => x.split(" ")).collect
lines: Array[String] = Array(java, python, hadoop, spring, python, hadoop, java, c, c++, hbase, spark, scala, scala, python, java, linux, unix, java, php, mysql, hive, hue, java, python, hadoop, spring, python, hadoop, java, c, c++, hbase, spark, scala, scala, python, java, linux, unix, java, php, mysql, hive, hue, java, python, spring, javascirpt, mapreduce, java, hello, world, python, aikfk, caizhengjie)

将数据分成元祖对

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).collect
lines: Array[(String, Int)] = Array((java,1), (python,1), (hadoop,1), (spring,1), (python,1), (hadoop,1), (java,1), (c,1), (c++,1), (hbase,1), (spark,1), (scala,1), (scala,1), (python,1), (java,1), (linux,1), (unix,1), (java,1), (php,1), (mysql,1), (hive,1), (hue,1), (java,1), (python,1), (hadoop,1), (spring,1), (python,1), (hadoop,1), (java,1), (c,1), (c++,1), (hbase,1), (spark,1), (scala,1), (scala,1), (python,1), (java,1), (linux,1), (unix,1), (java,1), (php,1), (mysql,1), (hive,1), (hue,1), (java,1), (python,1), (spring,1), (javascirpt,1), (mapreduce,1), (java,1), (hello,1), (world,1), (python,1), (aikfk,1), (caizhengjie,1))

将数据按key合并

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).reduceByKey((a,b) => (a+b)).collect
lines: Array[(String, Int)] = Array((hive,2), (php,2), (python,8), (aikfk,1), (linux,2), (hue,2), (unix,2), (spark,2), (caizhengjie,1), (hadoop,4), (spring,3), (hbase,2), (scala,4), (mapreduce,1), (mysql,2), (hello,1), (java,10), (world,1), (c++,2), (javascirpt,1), (c,2))

筛选数据,保留value>1的

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).reduceByKey((a,b) => (a+b)).filter(x =>(x._2 >1)).collect
lines: Array[(String, Int)] = Array((hive,2), (php,2), (python,8), (linux,2), (hue,2), (unix,2), (spark,2), (hadoop,4), (spring,3), (hbase,2), (scala,4), (mysql,2), (java,10), (c++,2), (c,2))

key value掉换位置

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).reduceByKey((a,b) => (a+b)).filter(x =>(x._2 >1)).map(x => (x._2,x._1)).collect
lines: Array[(Int, String)] = Array((2,hive), (2,php), (8,python), (2,linux), (2,hue), (2,unix), (2,spark), (4,hadoop), (3,spring), (2,hbase), (4,scala), (2,mysql), (10,java), (2,c++), (2,c))

按key的降序排序

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).reduceByKey((a,b) => (a+b)).filter(x =>(x._2 >1)).map(x => (x._2,x._1)).sortByKey(false).collect
lines: Array[(Int, String)] = Array((10,java), (8,python), (4,hadoop), (4,scala), (3,spring), (2,hive), (2,php), (2,linux), (2,hue), (2,unix), (2,spark), (2,hbase), (2,mysql), (2,c++), (2,c))

再次掉换key value掉换位置

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).reduceByKey((a,b) => (a+b)).filter(x =>(x._2 >1)).map(x => (x._2,x._1)).sortByKey(false).map(x =>(x._2,x._1)).collect
lines: Array[(String, Int)] = Array((java,10), (python,8), (hadoop,4), (scala,4), (spring,3), (hive,2), (php,2), (linux,2), (hue,2), (unix,2), (spark,2), (hbase,2), (mysql,2), (c++,2), (c,2))

输出打印

scala> lines.foreach(println)
(java,10)
(python,8)
(hadoop,4)
(scala,4)
(spring,3)
(hive,2)
(php,2)
(linux,2)
(hue,2)
(unix,2)
(spark,2)
(hbase,2)
(mysql,2)
(c++,2)
(c,2)

将结果写入hdfs中

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).reduceByKey((a,b) => (a+b)).filter(x =>(x._2 >1)).map(x => (x._2,x._1)).sortByKey(false).map(x =>(x._2,x._1)).saveAsTextFile("hdfs://node1:8020/user/datas/wordcount/")

查看结果

bin/hdfs dfs -text /user/datas/wordcount/par*
20/09/20 09:04:35 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
(java,10)
(python,8)
(hadoop,4)
(scala,4)
(spring,3)
(hive,2)
(php,2)
(linux,2)
(hue,2)
(unix,2)
(spark,2)
(hbase,2)
(mysql,2)
(c++,2)
(c,2)

通过查看网页,我们可知rdd被分成了四个区
在这里插入图片描述

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).reduceByKey((a,b) => (a+b)).filter(x =>(x._2 >1)).map(x => (x._2,x._1)).sortByKey(false).map(x =>(x._2,x._1))
scala> lines.getNumPartitions
res11: Int = 4

我们也可以自己指定分区

lines.repartitions(3)

五、 DataFrame创建方式及功能使用

  • 在Spark中, DataFrame是一 种以RDD为基础的分布式数据集,类似于传统数据库中的二维表格。
  • DataFrame与RDD的主要区别在于,前者带有schema元信息,即DataFrame所表示的二维表数据集的每一列都带有 名称和类型。
  • 使得SparkSQL得以洞察更多的结构信息,从而对藏于DataFrame背后的数据源以及作用于DataFrame之.上的变换进行了针对性的优化,最终达到大幅提升运行时效率的目标。
  • 反观RDD ,由于无从得知所存数据元素的具体内部结构, Spark Core只能在stage层面进行简单、通用的流水线优化。

DataFrame和RDD的对比图
在这里插入图片描述
RDD转换DataFrame:
准备数据集

val rdd1 = sc.textFile("file:///opt/datas/stu.txt")
val rdd2 = sc.textFile("file:///opt/datas/stu1.txt")
val allRdd = rdd1.union(rdd2)

将RDD转换为DataFrame,使用toDF

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).reduceByKey((a,b) => (a+b)).filter(x =>(x._2 >1)).map(x => (x._2,x._1)).sortByKey(false).map(x =>(x._2,x._1)).toDF
lines: org.apache.spark.sql.DataFrame = [_1: string, _2: int]

默认列名是 _1 _2

scala> lines.printSchema
root
 |-- _1: string (nullable = true)
 |-- _2: integer (nullable = false)

设置Schema为key value

val lines = allRdd.flatMap(x => x.split(" ")).map(x => (x,1)).reduceByKey((a,b) => (a+b)).filter(x =>(x._2 >1)).map(x => (x._2,x._1)).sortByKey(false).map(x =>(x._2,x._1)).toDF("key","value")
lines: org.apache.spark.sql.DataFrame = [key: string, value: int]
scala> lines.printSchema
root
 |-- key: string (nullable = true)
 |-- value: integer (nullable = false)

显示key

scala> lines.select("key").show
+------+
|   key|
+------+
|  java|
|python|
|hadoop|
| scala|
|spring|
|  hive|
|   php|
| linux|
|   hue|
|  unix|
| spark|
| hbase|
| mysql|
|   c++|
|     c|
+------+

DataSet转换DataFrame:
准备数据集

val dataSet = spark.read.textFile("file:///opt/datas/stu.txt").flatMap(x => x.split(" " )).map(x => (x,1))
dataSet: org.apache.spark.sql.Dataset[(String, Int)] = [_1: string, _2: int]

查看DataSet的数据格式

scala> dataSet.show
+------+---+
|    _1| _2|
+------+---+
|  java|  1|
|python|  1|
|hadoop|  1|
|spring|  1|
|python|  1|
|hadoop|  1|
|  java|  1|
|     c|  1|
|   c++|  1|
| hbase|  1|
| spark|  1|
| scala|  1|
| scala|  1|
|python|  1|
|  java|  1|
| linux|  1|
|  unix|  1|
|  java|  1|
|   php|  1|
| mysql|  1|
+------+---+
only showing top 20 rows

将DataSet转化为DataFrame

val dataSet = spark.read.textFile("file:///opt/datas/stu.txt").flatMap(x => x.split(" " )).map(x => (x,1)).toDF("key","value")

这时已经转化为DataFrame的格式

dataSet: org.apache.spark.sql.DataFrame = [key: string, value: int]

DataFrame操作

dataSet.select("key","value").groupBy("key").count.sort($"count".desc).show
+------+-----+
|   key|count|
+------+-----+
|  java|    4|
|python|    3|
| scala|    2|
|hadoop|    2|
|  unix|    1|
|spring|    1|
| mysql|    1|
| spark|    1|
| linux|    1|
|   hue|    1|
|   c++|    1|
| hbase|    1|
|     c|    1|
|  hive|    1|
|   php|    1|
+------+-----+

六、DataSet创建方式及功能使用

DataSet与RDD相似,但是它们不是使用Java序列化或Kryo,而是使用专用的Encoder对对象进行序列化以进行网络处理或传输。

DataSet创建方式有两种:一种是直接通过sparkSession对象读取外部数据创建,另一种是通过RDD转换。

(1)创建DataSet方式一

通过sparkSession对象读取外部数据创建DataSet:

val dataSet = spark.read.textFile("file:///opt/datas/stu.txt")
dataSet: org.apache.spark.sql.Dataset[String] = [value: string]

查看Schema信息

scala> dataSet.printSchema
root
 |-- value: string (nullable = true)

查看一下数据

scala> val dataSet = spark.read.textFile("file:///opt/datas/stu.txt").show
+--------------------+
|               value|
+--------------------+
|  java python hadoop|
|spring python had...|
|   hbase spark scala|
|  scala python java |
| linux unix java php|
|      mysql hive hue|
+--------------------+
val lines = dataSet.flatMap(x => x.split(" ")).map(x => (x,1))

对DataSet操作

scala> lines.select("_1","_2").groupBy("_1").count.show
+------+-----+
|    _1|count|
+------+-----+
|  unix|    1|
| hbase|    1|
|spring|    1|
| mysql|    1|
| scala|    2|
| spark|    1|
|   hue|    1|
|   c++|    1|
|     c|    1|
| linux|    1|
|  java|    4|
|   php|    1|
|hadoop|    2|
|python|    3|
|  hive|    1|
+------+-----+

自定义对象的映射

case class Person(username:String,usercount:Int)
defined class Person
val lines = dataSet.flatMap(x => x.split(" ")).map(x => (x,1)).map(x => Person(x._1,x._2))

这时列名就改成了刚才我们自定义的

scala> lines.show
+--------+---------+
|username|usercount|
+--------+---------+
|    java|        1|
|  python|        1|
|  hadoop|        1|
|  spring|        1|
|  python|        1|
|  hadoop|        1|
|    java|        1|
|       c|        1|
|     c++|        1|
|   hbase|        1|
|   spark|        1|
|   scala|        1|
|   scala|        1|
|  python|        1|
|    java|        1|
|   linux|        1|
|    unix|        1|
|    java|        1|
|     php|        1|
|   mysql|        1|
+--------+---------+
only showing top 20 rows
scala> lines.select("username","usercount").groupBy("username").count.show
+--------+-----+
|username|count|
+--------+-----+
|    unix|    1|
|   hbase|    1|
|  spring|    1|
|   mysql|    1|
|   scala|    2|
|   spark|    1|
|     hue|    1|
|     c++|    1|
|       c|    1|
|   linux|    1|
|    java|    4|
|     php|    1|
|  hadoop|    2|
|  python|    3|
|    hive|    1|
+--------+-----+
scala> lines.select("username","usercount").groupBy("username").count.sort($"count".desc).show
+--------+-----+
|username|count|
+--------+-----+
|    java|    4|
|  python|    3|
|   scala|    2|
|  hadoop|    2|
|   hbase|    1|
|     hue|    1|
|    unix|    1|
|     c++|    1|
|  spring|    1|
|   mysql|    1|
|   spark|    1|
|       c|    1|
|   linux|    1|
|     php|    1|
|    hive|    1|
+--------+-----+
scala> lines.select("username","usercount").groupBy("username").count.sort($"count".desc).toDF("username","usercount").show
+--------+---------+
|username|usercount|
+--------+---------+
|    java|        4|
|  python|        3|
|   scala|        2|
|  hadoop|        2|
|   hbase|        1|
|  spring|        1|
|   spark|        1|
|       c|        1|
|     hue|        1|
|     c++|        1|
|   mysql|        1|
|   linux|        1|
|     php|        1|
|    unix|        1|
|    hive|        1|
+--------+---------+

(2)创建DataSet方式二

通过RDD转换成DataSet

scala> val rdd1 = sc.textFile("file:///opt/datas/stu.txt")
rdd1: org.apache.spark.rdd.RDD[String] = file:///opt/datas/stu.txt MapPartitionsRDD[187] at textFile at <console>:24
scala> val dataSet2 = rdd1.toDS
dataSet2: org.apache.spark.sql.Dataset[String] = [value: string]

这时就将RDD转换成了DataSet,

(3)创建DataSet方式三

通过DataFrame转换成DataSet

scala> val dataSet2 = rdd1.toDF.toJSON
dataSet2: org.apache.spark.sql.Dataset[String] = [value: string]

通过DataFrame转换成DataSet,查看数据发现每一行是json的格式

scala> dataSet2.show
+--------------------+
|               value|
+--------------------+
|{"value":"java py...|
|{"value":"spring ...|
|{"value":"hbase s...|
|{"value":"scala p...|
|{"value":"linux u...|
|{"value":"mysql h...|
+--------------------+

可以再通过这种方式将DataSet转换为DataFrame

scala> spark.read.json(dataSet2)
res23: org.apache.spark.sql.DataFrame = [value: string]
scala> spark.read.json(dataSet2).show
+--------------------+
|               value|
+--------------------+
|  java python hadoop|
|spring python had...|
|   hbase spark scala|
|  scala python java |
| linux unix java php|
|      mysql hive hue|
+--------------------+

还有一种方式可以通过DataFrame转换成DataSet

scala> val rdd1 = sc.textFile("file:///opt/datas/stu.txt")
scala> val df = rdd1.flatMap(x => x.split(" ")).toDF
df: org.apache.spark.sql.DataFrame = [value: string]
scala> val dataSet = df.as[String]
dataSet: org.apache.spark.sql.Dataset[String] = [value: string]
scala> dataSet.map(x => (x,1)).show
+------+---+
|    _1| _2|
+------+---+
|  java|  1|
|python|  1|
|hadoop|  1|
|spring|  1|
|python|  1|
|hadoop|  1|
|  java|  1|
|     c|  1|
|   c++|  1|
| hbase|  1|
| spark|  1|
| scala|  1|
| scala|  1|
|python|  1|
|  java|  1|
| linux|  1|
|  unix|  1|
|  java|  1|
|   php|  1|
| mysql|  1|
+------+---+
scala> dataSet.map(x => (x,1))
res28: org.apache.spark.sql.Dataset[(String, Int)] = [_1: string, _2: int]

此时我们发现它的列名是_1,_2,我们可以将它的列名改为我们自定义对象

scala> case class Person(username:String,usercount:Int)
defined class Person
scala> val df = rdd1.flatMap(x => x.split(" ")).map(x => (x,1)).toDF("username","usercount")
df: org.apache.spark.sql.DataFrame = [username: string, usercount: int]
scala> val dataSet = df.as[Person]
dataSet: org.apache.spark.sql.Dataset[Person] = [username: string, usercount: int]

这时列名就换过来了

scala> dataSet.show
+--------+---------+
|username|usercount|
+--------+---------+
|    java|        1|
|  python|        1|
|  hadoop|        1|
|  spring|        1|
|  python|        1|
|  hadoop|        1|
|    java|        1|
|       c|        1|
|     c++|        1|
|   hbase|        1|
|   spark|        1|
|   scala|        1|
|   scala|        1|
|  python|        1|
|    java|        1|
|   linux|        1|
|    unix|        1|
|    java|        1|
|     php|        1|
|   mysql|        1|
+--------+---------+

下面就可以对DataSet做一些操作

scala> dataSet.select("username","usercount").groupBy("username").count.show
+--------+-----+
|username|count|
+--------+-----+
|    unix|    1|
|   hbase|    1|
|  spring|    1|
|   mysql|    1|
|   scala|    2|
|   spark|    1|
|     hue|    1|
|     c++|    1|
|       c|    1|
|   linux|    1|
|    java|    4|
|     php|    1|
|  hadoop|    2|
|  python|    3|
|    hive|    1|
+--------+-----+

七、数据集之间的对比与转换(总结)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
8、互相转换
在这里插入图片描述
9、DataSet和DataFrame的分组和排序
在这里插入图片描述


以上内容仅供参考学习,如有侵权请联系我删除!
如果这篇文章对您有帮助,左下角的大拇指就是对博主最大的鼓励。
您的鼓励就是博主最大的动力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

<一蓑烟雨任平生>

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

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

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

打赏作者

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

抵扣说明:

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

余额充值