spark 常用算子大全(分类详细,图片解析)

spark 系列

spark 常用算子大全



前言

本篇博客将为大家带来 spark 常用算子的介绍。本节建议大家在有 scala 集合函数大全 的基础上去学习,会更加的轻松!


RDD 算子

RDD 算子分为 lazynon-lazy 两种

  • Transformation(lazy):也称转换操作、转换算子
  • Actions(non-lazy):立即执行,也称动作操作、动作算子

RDD 转换算子

对于转换操作,RDD 的所有转换都不会直接计算结果

  • 仅记录作用于 RDD 上的操作
  • 当遇到动作算子(Action)时才会进行真正计算

Transformation 操作是延迟计算的,也就是说从一个 RDD 转换生成另一个 RDD 的转换操作不是马上执行,需要等到有 Action 操作的时候才会真正触发运算。

Value类型

map(func)

作用:返回一个新的 RDD ,该 RDD 由每一个输入元素经过 func 函数转换后组成

详解

map 将原来 RDD 的每个数据项通过 map 中的用户自定义函数 f 映射转变为一个新的元素。源码中的map 算子相当于初始化一个 RDD,新RDD叫做 MappedRDD(this,sc.clean(f))。

如下图所示,每个方框表示一个 RDD 分区,左侧的分区经过用户自定义函数 f:T->U 映射为右侧的新 RDD 分区。但是,实际只有等到 Action算子触发后,这个 f 函数才会和其他函数在一个 stage 中对数据进行运算。第一个分区的数据记录 V1 输入 f,通过 f 转换输出为转换后的分区中的数据记录 V’1。

在这里插入图片描述
举例

scala> sc.makeRDD(1 to 10).map(_*2).collect
res1: Array[Int] = Array(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

mapPartitions(func)

作用:类似于 map ,但独立地在 RDD 的每一个分片上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是Iterator[T] => Iterator[U]假设有 N 个元素,有 M 个分区,那么 map 的函数的将被调用 N 次,而 mapPartitions 被调用 M 次,一个函数一次处理所有分区。

详解

mapPartitions 函数获取到每个分区的迭代器,在函数中通过这个分区整体的迭代器对整个分区的元素 进行操作。内部实现是生成 MapPartitionsRDD。

如下图所示,方框代表一个 RDD 分区。用户通过函数 f(iter)=>iter.filter(_>=3) 对分区中所有数据进行过滤,大于和等于 3 的数据保留。一个方块代表一个 RDD 分区,含有 1、 2、 3 的分区过滤只剩下元素 3。
在这里插入图片描述
举例

#表示划分为2个分区
scala> val rdd = sc.parallelize(1 to 6, 2)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[5] at parallelize at <console>:24

scala> rdd.partitions.size
res4: Int = 2

# items表示一个分区中的数据
scala> rdd.mapPartitions(items => items.filter(_>=3)).collect
res7: Array[Int] = Array(3, 4, 5, 6)

map()和mapPartition()的区别

  1. map():每次处理一条数据

  2. mapRartition(): 每次处理一个分区的数据,这个分区的数据处理完之后,原 RDD 中分区的数据才能释放,可能导致OOM。

  3. 开发指导:当内存空间较大的时候建议使用 mapPartition() ,以提高处理效率。

mapPartitionsWithIndex(func)

作用:类似于 mapPartitions ,但 func 带有一个整数参数表示分片的索引值,因此在类型为 T 的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U]

举例

scala> val rdd = sc.parallelize(1 to 6, 2)rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

scala> rdd.mapPartitionsWithIndex((index,items) => Iterator(index + ":" + items.toList)).collect
res0: Array[String] = Array(0:List(1, 2, 3), 1:List(4, 5, 6))

flatMap(func)

作用:类似于 map ,但是每一个输入元素可以被映射为0或多个输出元素(== 所以 func 应该返回一个序列,而不是单一元素 ==)

详解

将原来 RDD 中的每个元素通过函数 f 转换为新的元素,并将生成的 RDD 的每个集合中的元素合并为一个集合,内部创建 FlatMappedRDD(this,sc.clean(f))。

如下图所示,表示 RDD 的一个分区,进行 flatMap 函数操作,flatMap 中传入的函数为f:T->U,T 和 U 可以是任意的数据类型。将分区中的数据通过用户自定义函数 f 转换为新的数据。外部大方框可以认为是一个 RDD 分区,小方框代表一个集合。 V1、 V2、 V3 在一个集合作为 RDD 的一个数据项,可能存储为数组或其他容器,转换为V’1、 V’2、 V’3 后,将原来的数组或容器结合拆散,拆散的数据形成为 RDD 中的数据项。

在这里插入图片描述

举例

scala> val rdd = sc.parallelize(1 to 6, 2)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at <console>:24

scala> rdd.flatMap(1 to _).collect
res1: Array[Int] = Array(1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6)

glom

作用:将每一个分区形成一个数组,形成新的 RDD 类型是RDD[Array[T]]

详解

glom 函数将每个分区形成一个数组,内部实现是返回的 GlommedRDD。 下图中的每个方框代表一个RDD分区。 该图表示含有V1、 V2、 V3的分区通过函数 glom 形成一数组Array[(V1),(V2),(V3)]

在这里插入图片描述
举例

scala> val rdd = sc.parallelize(1 to 16,4)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[65] at parallelize at <console>:24
scala> rdd.glom().collect()
res25: Array[Array[Int]] = Array(Array(1, 2, 3, 4), Array(5, 6, 7, 8), Array(9, 10, 11, 12), Array(13, 14, 15, 16))

groupBy(func)

作用:分组,按照传入函数的返回值进行分组。将相同的key对应的值放入一个迭代器

详解

将元素通过函数生成相应的 Key,数据就转化为 Key-Value 格式,之后将 Key 相同的元素分为一组。

函数实现如下:
1)将用户函数预处理:val cleanF = sc.clean(f)
2)对数据 map 进行函数操作,最后再进行 groupByKey 分组操作:this.map(t => (cleanF(t), t)).groupByKey(p),其中, p 确定了分区个数和分区函数,也就决定了并行化的程度。

下图中方框代表一个 RDD 分区,相同key 的元素合并到一个组。例如 V1 和 V2 合并为 V, Value 为 V1,V2。形成 V,Seq(V1,V2)。
在这里插入图片描述

举例

scala> val rdd = sc.parallelize(1 to 4)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[4] at parallelize at <console>:24

scala> val group = rdd.groupBy(_%2)
group: org.apache.spark.rdd.RDD[(Int, Iterable[Int])] = ShuffledRDD[6] at groupBy at <console>:25

scala> group.collect
res2: Array[(Int, Iterable[Int])] = Array((0,CompactBuffer(2, 4)), (1,CompactBuffer(1, 3)))

filter(func)

作用: 过滤。返回一个新的 RDD,该 RDD 由经过 func 函数计算后返回值为 true 的输入元素组成。返回值为 false 的元素将被过滤掉。 内部实现相当于生成 FilteredRDD(this,sc.clean(f))。

详解

下面代码为函数的本质实现:deffilter(f:T=>Boolean):RDD[T]=newFilteredRDD(this,sc.clean(f))

下图中每个方框代表一个 RDD 分区, T 可以是任意的类型。通过用户自定义的过滤函数 f,对每个数据项操作,将满足条件、返回结果为 true 的数据项保留。例如,过滤掉 V2 和 V3 保留了 V1,为区分命名为 V’1。

在这里插入图片描述
举例

scala> val rdd = sc.makeRDD(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[7] at makeRDD at <console>:24

scala> rdd.filter(_%2==0).collect
res3: Array[Int] = Array(2, 4, 6, 8, 10)

sample(withReplacement, fraction, seed)

作用 : 以指定的随机种子随机抽样出数量为 fraction 的数据,withReplacement 表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样,seed用于指定随机数生成器种子。

详情
sample 将 RDD 这个集合内的元素进行采样,获取所有元素的子集。用户可以设定是否有放回的抽样、百分比、随机种子,进而决定采样方式。内部实现是生成 SampledRDD(withReplacement, fraction, seed)
注意:

  • 有放回时,fraction可以大于1,代表元素被抽到的次数
  • 无放回时,fraction代表元素被抽到的概率(0-1)

下图中的每个方框是一个 RDD 分区。 通过 sample 函数, 采样 50% 的数据。V1、 V2、 U1、 U2、U3、U4 采样出数据 V1 和 U1、 U2 形成新的 RDD。

在这里插入图片描述
举例

// 1)创建RDD
scala> val rdd = sc.parallelize(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[20] at parallelize at <console>:24

// 2) 打印
scala> rdd.collect()
res15: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// 3)放回抽样
scala> var sample1 = rdd.sample(true,0.4,2)
sample1: org.apache.spark.rdd.RDD[Int] = PartitionwiseSampledRDD[21] at sample at <console>:26

// 4)打印放回抽样结果
scala> sample1.collect()
res16: Array[Int] = Array(1, 2, 2, 7, 7, 8, 9)

// 5)打印不放回抽样结果
scala> var sample2 = rdd.sample(false,0.2,3)
sample2: org.apache.spark.rdd.RDD[Int] = PartitionwiseSampledRDD[22] at sample at <console>:26

// 6)打印不放回抽样结果
scala> sample2.collect()
res17: Array[Int] = Array(1, 9)

takeSample

作用
takeSample()函数和上面的sample函数是一个原理,但是不使用相对比例采样,而是按设定的采样个数进行采样,同时返回结果不再是RDD,而是相当于对采样后的数据进行Collect(),返回结果的集合为单机的数组。

下图中左侧的方框代表分布式的各个节点上的分区,右侧方框代表单机上返回的结果数组。 通过takeSample对数据采样,设置为采样一份数据,返回结果为V1。

在这里插入图片描述
举例

scala> val rdd = sc.parallelize(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[9] at parallelize at <console>:24

scala> rdd.takeSample(true,3,3)
res4: Array[Int] = Array(6, 2, 2)

scala> rdd.takeSample(false,3,3)
res5: Array[Int] = Array(6, 4, 2)

distinct([numTasks]))

作用:对原 RDD 进行去重后返回一个新的 RDD。默认情况下,只有8个并行任务来操作,但是可以传入一个可选的 numTasks 参数改变它。

详解

下图中的每个方框代表一个RDD分区,通过 distinct 函数,将数据去重。 例如,重复数据V1、 V1去重后只保留一份V1。
在这里插入图片描述
举例

scala> val rdd = sc.parallelize(List(1,2,3,4,4,2,1))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[82] at parallelize at <console>:24

scala> rdd.distinct.collect
res51: Array[Int] = Array(4, 2, 1, 3)

coalesce(numPartitions)

作用:缩减分区数,用于大数据集过滤后,提高小数据集的执行效率。

举例

// 1)创建一个RDD
scala> val rdd = sc.parallelize(1 to 16,4)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[54] at parallelize at <console>:24

// 2)查看RDD的分区数
scala> rdd.partitions.size
res20: Int = 4

// 3)对RDD重新分区
scala> val coalesceRDD = rdd.coalesce(3)
coalesceRDD: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[55] at coalesce at <console>:26

// 4)查看新RDD的分区数
scala> coalesceRDD.partitions.size
res21: Int = 3

repartition(numPartitions)

作用:根据分区数,重新通过网络随机洗牌所有数据。

举例

// 1)创建一个RDD
scala> val rdd = sc.parallelize(1 to 16,4)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[56] at parallelize at <console>:24

// 2)查看RDD的分区数
scala> rdd.partitions.size
res22: Int = 4

// 3)对RDD重新分区
scala> val rerdd = rdd.repartition(2)
rerdd: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[60] at repartition at <console>:26

// 4)查看新RDD的分区数
scala> rerdd.partitions.size
res23: Int = 2

coalesce和repartition的区别

  1. coalesce 重新分区,可以选择是否进行 shuffle 过程。由参数shuffle: Boolean = false/true决定。

  2. repartition 实际上是调用的 coalesce,默认是进行 shuffle 的。源码如下:

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
  coalesce(numPartitions, shuffle = true)
}

repartitionAndSortWithinPartitions(partitioner)

repartitionAndSortWithinPartitions 函数是 repartition 函数的变种,与 repartition 函数不同的是,repartitionAndSortWithinPartitions 在给定的 partitioner 内部进行排序,性能比repartition 要高。

sortBy(func,[ascending], [numTasks])

作用:使用func先对数据进行处理,按照处理后的数据比较结果排序,默认为正序。

举例

scala> val rdd = sc.parallelize(List(1,2,3,4))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[21] at parallelize at <console>:24

scala> rdd.sortBy(x => x).collect()
res11: Array[Int] = Array(1, 2, 3, 4)

scala> rdd.sortBy(x => x%3).collect()
res12: Array[Int] = Array(3, 4, 1, 2)

pipe(command,[envVars])

作用:管道,针对每个分区,都执行一个 shell 脚本,返回输出的 RDD。

注意:shell 脚本需要集群中的所有节点都能访问到

举例

// 1)编写一个脚本
Shell脚本
#!/bin/sh
echo "AA"
while read LINE; do
   echo ">>>"${LINE}
done

// 2)创建一个只有一个分区的RDD
scala> val rdd = sc.parallelize(List("hi","Hello","how","are","you"),1)
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[50] at parallelize at <console>:24

// 3)将脚本作用该RDD并打印
scala> rdd.pipe("/opt/module/spark/pipe.sh").collect()
res18: Array[String] = Array(AA, >>>hi, >>>Hello, >>>how, >>>are, >>>you

// 4)创建一个有两个分区的RDD
scala> val rdd = sc.parallelize(List("hi","Hello","how","are","you"),2)
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[52] at parallelize at <console>:24

// 5)将脚本作用该RDD并打印
scala> rdd.pipe("/opt/module/spark/pipe.sh").collect()
res19: Array[String] = Array(AA, >>>hi, >>>Hello, AA, >>>how, >>>are, >>>you)

cache

cache 将 RDD 元素从磁盘缓存到内存。 相当于 persist(MEMORY_ONLY) 函数的功能。

下图中每个方框代表一个 RDD 分区,左侧相当于数据分区都存储在磁盘,通过 cache 算子将数据缓存在内存。
在这里插入图片描述

persist

persist 函数对 RDD 进行缓存操作。数据缓存在哪里依据 StorageLevel 这个枚举类型进行确定。 有以下几种类型的组合, DISK 代表磁盘,MEMORY 代表内存, SER 代表数据是否进行序列化存储。

下面为函数定义persist(newLevel:StorageLevel), StorageLevel 是枚举类型,代表存储模式,用户可以通过下图按需进行选择。其中列出persist 函数可以进行缓存的模式。例如,MEMORY_AND_DISK_SER代表数据可以存储在内存和磁盘,并且以序列化的方式存储,其他同理。

在这里插入图片描述
下图中方框代表 RDD 分区。 disk 代表存储在磁盘, mem 代表存储在内存。数据最初全部存储在磁盘,通过 persist(MEMORY_AND_DISK) 将数据缓存到内存,但是有的分区无法容纳在内存,将含有 V1、 V2、 V3 的RDD存储到磁盘,将含有U1,U2的RDD仍旧存储在内存。
在这里插入图片描述

双Value类型交互

union(otherDataset)

作用: 对 RDD 和参数 RDD 求并集后返回一个新的 RDD

详解

== 使用 union 函数时需要保证两个 RDD 元素的数据类型相同,返回的 RDD 数据类型和被合并的 RDD 元素数据类型相同,并不进行去重操作,保存所有元素。如果想去重
可以使用 distinct() ==。同时 Spark 还提供更为简洁的使用 union 的 API,通过 ++ 符号相当于 union 函数操作。

下图 中左侧大方框代表两个 RDD,大方框内的小方框代表 RDD 的分区。右侧大方框代表合并后的 RDD,大方框内的小方框代表分区。含有V1、V2、U1、U2、U3、U4的RDD和含有V1、V8、U5、U6、U7、U8的RDD合并所有元素形成一个RDD。V1、V1、V2、V8形成一个分区,U1、U2、U3、U4、U5、U6、U7、U8形成一个分区。
在这里插入图片描述

举例

scala> val rdd1 = sc.parallelize(1 to 5)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[54] at parallelize at <console>:24
scala> val rdd2 = sc.parallelize(5 to 10)
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[55] at parallelize at <console>:24
scala> rdd1.union(rdd2).collect
res44: Array[Int] = Array(1, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10)

subtract (otherDataset)

作用:计算差的一种函数,去除两个 RDD 中相同的元素,不同的 RDD 将保留下来。:计算差的一种函数,去除两个RDD中相同的元素,不同的RDD将保留下来。

详解

subtract 相当于进行集合的差操作,RDD 1去除RDD 1和RDD 2交集中的所有元素。
下图中左侧的大方框代表两个RDD,大方框内的小方框代表RDD的分区。 右侧大方框
代表合并后的RDD,大方框内的小方框代表分区。 V1在两个RDD中均有,根据差集运算规则,新RDD不保留,V2在第一个RDD有,第二个RDD没有,则在新RDD元素中包含V2。
在这里插入图片描述

举例

scala> val rdd = sc.parallelize(3 to 8)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[70] at
parallelize at <console>:24
scala> val rdd1 = sc.parallelize(1 to 5)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[71] at
parallelize at <console>:24
scala> rdd.subtract(rdd1).collect()
res27: Array[Int] = Array(8, 6, 7)

intersection(otherDataset)

作用对源RDD和参数RDD求交集后返回一个新的RDD(去重

在这里插入图片描述
举例

scala> val rdd1 = sc.parallelize(Array(1,1,2,3,4,5,4,5))
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

scala> val rdd2 = sc.parallelize(Array(1,2,1,4))
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at parallelize at <console>:24

scala> rdd1.intersection(rdd2).collect
res0: Array[Int] = Array(1, 2, 4)

cartesian(otherDataset)

作用:笛卡尔积(尽量避免使用)

详解

对两个 RDD 内的所有元素进行笛卡尔积操作。操作后,内部实现返回CartesianRDD。下图中左侧大方框代表两个 RDD,大方框内的小方框代表 RDD 的分区。右侧大方框代表合并后的 RDD,大方框内的小方框代表分区。例 如: V1 和 另 一 个 RDD 中 的 W1、 W2、 Q5 进 行 笛 卡 尔 积 运 算 形 成 (V1,W1)、(V1,W2)、 (V1,Q5)。
在这里插入图片描述

举例

scala> val rdd1 = sc.parallelize(1 to 3)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[47] at parallelize at <console>:24
scala> val rdd2 = sc.parallelize(2 to 5)
rdd2: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[48] at parallelize at <console>:24
scala> rdd1.cartesian(rdd2).collect()
res17: Array[(Int, Int)] = Array((1,2), (1,3), (1,4), (1,5), (2,2), (2,3), (2,4), (2,5), (3,2), (3,3), (3,4), (3,5))

zip(otherDataset)

作用:将两个 RDD 组合成 Key/Value 形式的RDD,这里默认两个 RDD 的partition 数量以及元素数量都相同,否则会抛出异常。

举例

// 1)创建第一个RDD
scala> val rdd1 = sc.parallelize(Array(1,2,3),3)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at parallelize at <console>:24

// 2)创建第二个RDD(与1分区数相同)
scala> val rdd2 = sc.parallelize(Array("a","b","c"),3)
rdd2: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[2] at parallelize at <console>:24

// 3)第一个RDD组合第二个RDD并打印
scala> rdd1.zip(rdd2).collect
res1: Array[(Int, String)] = Array((1,a), (2,b), (3,c))

// 4)第二个RDD组合第一个RDD并打印
scala> rdd2.zip(rdd1).collect
res2: Array[(String, Int)] = Array((a,1), (b,2), (c,3))

// 5)创建第三个RDD(与1,2分区数不同)
scala> val rdd3 = sc.parallelize(Array("a","b","c"),2)
rdd3: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[5] at parallelize at <console>:24

// 6)第一个RDD组合第三个RDD并打印(报异常)
scala> rdd1.zip(rdd3).collect
java.lang.IllegalArgumentException: Can't zip RDDs with unequal numbers of partitions: List(3, 2)
  at org.apache.spark.rdd.ZippedPartitionsBaseRDD.getPartitions(ZippedPartitionsRDD.scala:57)
  at org.apache.spark.rdd.RDD$$anonfun$partitions$2.apply(RDD.scala:252)
  at org.apache.spark.rdd.RDD$$anonfun$partitions$2.apply(RDD.scala:250)
  at scala.Option.getOrElse(Option.scala:121)
  at org.apache.spark.rdd.RDD.partitions(RDD.scala:250)
  at org.apache.spark.SparkContext.runJob(SparkContext.scala:1965)
  at org.apache.spark.rdd.RDD$$anonfun$collect$1.apply(RDD.scala:936)
  at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:151)
  at org.apache.spark.rdd.RDDOperationScope$.withScope(RDDOperationScope.scala:112)
  at org.apache.spark.rdd.RDD.withScope(RDD.scala:362)
  at org.apache.spark.rdd.RDD.collect(RDD.scala:935)
  ... 48 elided

Key-Value 类型

partitionBy

作用:对 pairRDD 进行分区操作,如果原有的 partionRDD 和现有的partionRDD 是一致的话就不进行分区, 否则会生成 ShuffleRDD,即会产生shuffle 过程。

详解

artitionBy 函数对 RDD 进行分区操作。函数定义如下:partitionBy(partitioner:Partitioner)如果原有 RDD 的分区器和现有分区器(partitioner)一致,则不重分区,如果不一致,则相当于根据分区器生成一个新的ShuffledRDD。

下图中的方框代表RDD分区。 通过新的分区策略将原来在不同分区的V1、 V2数据都合并到了一个分区。

在这里插入图片描述
举例

scala> val rdd = sc.parallelize(Array((1,"aaa"),(2,"bbb"),(3,"ccc"),(4,"ddd")),4)
rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[90] at parallelize at <console>:24
scala> rdd.collect
res55: Array[(Int, String)] = Array((1,aaa), (2,bbb), (3,ccc), (4,ddd))
scala> val rdd1 = rdd.partitionBy(new org.apache.spark.HashPartitioner(2))
rdd1: org.apache.spark.rdd.RDD[(Int, String)] = ShuffledRDD[91] at partitionBy at <console>:26
scala> rdd1.collect
res56: Array[(Int, String)] = Array((2,bbb), (4,ddd), (1,aaa), (3,ccc))

groupByKey

作用:groupByKey也是对每个key进行操作,但只生成一个sequence。
在这里插入图片描述

举例

// 1)创建一个pairRDD
scala> val words = Array("one", "two", "two", "three", "three", "three")
words: Array[String] = Array(one, two, two, three, three, three)
scala> val wordPairsRDD = sc.parallelize(words).map(word => (word, 1))
wordPairsRDD: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[4] at map at <console>:26

// 2)将相同key对应值聚合到一个sequence中
scala> val group = wordPairsRDD.groupByKey()
group: org.apache.spark.rdd.RDD[(String, Iterable[Int])] = ShuffledRDD[5] at groupByKey at <console>:28

// 3)打印结果
scala> group.collect()
res1: Array[(String, Iterable[Int])] = Array((two,CompactBuffer(1, 1)), (one,CompactBuffer(1)), (three,CompactBuffer(1, 1, 1)))

// 4)计算相同key对应值的相加结果
scala> group.map(t => (t._1, t._2.sum))
res2: org.apache.spark.rdd.RDD[(String, Int)] = MapPartitionsRDD[6] at map at <console>:31

// 5)打印结果
scala> res2.collect()
res3: Array[(String, Int)] = Array((two,2), (one,1), (three,3))

reduceByKey(func, [numTasks])

作用:在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,reduce任务的个数可以通过第二个可选的参数来设置。

详解

reduceByKey 是比 combineByKey 更简单的一种情况,只是两个值合并成一个值,( Int, Int V)to (Int, Int C),比如叠加。所以 createCombiner ,reduceBykey 很简单,就是直接返回 v,而 mergeValue和 mergeCombiners 逻辑是相同的,没有区别。
函数实现:

def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)]
= {
combineByKey[V]((v: V) => v, func, func, partitioner)
}

下图中的方框代表 RDD 分区。通过用户自定义函数 (A,B) => (A + B) 函数,将相同 key 的数据 (V1,2) 和 (V1,1) 的 value 相加运算,结果为( V1,3)。
在这里插入图片描述
举例

// 1)创建一个pairRDD
scala> val rdd = sc.parallelize(List(("female",1),("male",5),("female",5),("male",2)))
rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[46] at parallelize at <console>:24

// 2)计算相同key对应值的相加结果
scala> val reduce = rdd.reduceByKey((x,y) => x+y)
reduce: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[47] at reduceByKey at <console>:26

// 3)打印结果
scala> reduce.collect()
res29: Array[(String, Int)] = Array((female,6), (male,7))

reduceByKey和groupByKey的区别

  1. reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。

  2. groupByKey:按照key进行分组,直接进行shuffle。

  3. 开发指导:reduceByKey比groupByKey更建议使用。但是需要注意是否会影响业务逻辑。

aggregateByKey

参数(zeroValue:U,[partitioner: Partitioner]) (seqOp: (U, V) => U,combOp: (U, U) => U)

参数描述:
(1)zeroValue: 给每一个分区的每一个key一个初始值。
(2)seqOp: 函数用于在每一个分区中用初始值逐步迭代value
(3)combOp:函数用于合并每个分区中的结果。

作用:在 kv 对的 RDD 中,按 key 将 value 进行分组合并,合并时,将每个 value 和初始值作为 seq 函数的参数,进行计算,返回的结果作为一个新的 kv 对,然后再将结果按照 key 进行合并,最后将每个分组的 value 传递给 combine 函数进行计算(先将前两个 value 进行计算,将返回结果和下一个 value 传给combine函数,以此类推),将key与计算结果作为一个新的kv对输出。

举例
在这里插入图片描述

// 1)创建一个pairRDD
scala> val rdd = sc.parallelize(List(("a",3),("a",2),("c",4),("b",3),("c",6),("c",8)),2)
rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at <console>:24

// 2)取出每个分区相同key对应值的最大值,然后相加
scala> val agg = rdd.aggregateByKey(0)(math.max(_,_),_+_)
agg: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[1] at aggregateByKey at <console>:26

// 3)打印结果
scala> agg.collect()
res0: Array[(String, Int)] = Array((b,3), (a,3), (c,12))

foldByKey

参数(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]

作用:aggregateByKey的简化操作,seqop和combop相同

举例

// 1)创建一个pairRDD
scala> val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[91] at parallelize at <console>:24

// 2)计算相同结果key对应值的相加结果
scala> val agg = rdd.foldByKey(0)(_+_)
agg: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[92] at foldByKey at <console>:26

// 3)打印结果
scala> agg.collect()
res61: Array[(Int, Int)] = Array((3,14), (1,9), (2,3))

combineByKey[C]

参数:(createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C)

参数描述:
(1)createCombiner : combineByKey() 会遍历分区中的所有元素,因此每个元素的键要么还没有遇到过,要么就和之前的某个元素的键相同。如果这是一个新的元素,combineByKey()会使用一个叫作createCombiner()的函数来创建那个键对应的累加器的初始值。
(2)mergeValue:如果这是一个在处理当前分区之前已经遇到的键,它会使用mergeValue()方法将该键的累加器对应的当前值与这个新的值进行合并。
(3)mergeCombiners:由于每个分区都是独立处理的, 因此对于同一个键可以有多个累加器。如果有两个或者更多的分区都有对应同一个键的累加器, 就需要使用用户提供的 mergeCombiners() 方法将各个分区的结果进行合并。

作用:对相同K,把V合并成一个集合。

举例
在这里插入图片描述

// 1)创建一个pairRDD
scala> val input = sc.parallelize(Array(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)),2)
input: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[52] at parallelize at <console>:26

// 2)将相同key对应的值相加,同时记录该key出现的次数,放入一个二元组
scala> val combine = input.combineByKey((_,1),(acc:(Int,Int),v)=>(acc._1+v,acc._2+1),(acc1:(Int,Int),acc2:(Int,Int))=>(acc1._1+acc2._1,acc1._2+acc2._2))
combine: org.apache.spark.rdd.RDD[(String, (Int, Int))] = ShuffledRDD[5] at combineByKey at <console>:28

// 3)打印合并后的结果
scala> combine.collect
res5: Array[(String, (Int, Int))] = Array((b,(286,3)), (a,(274,3)))

// 4)计算平均值
scala> val result = combine.map{case (key,value) => (key,value._1/value._2.toDouble)}
result: org.apache.spark.rdd.RDD[(String, Double)] = MapPartitionsRDD[54] at map at <console>:30

// 5)打印结果
scala> result.collect()
res33: Array[(String, Double)] = Array((b,95.33333333333333), (a,91.33333333333333))

sortByKey([ascending], [numTasks])

作用:在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD

举例

// 1)创建一个pairRDD
scala> val rdd = sc.parallelize(Array((3,"aa"),(6,"cc"),(2,"bb"),(1,"dd")))
rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[14] at parallelize at <console>:24

// 2)按照key的正序
scala> rdd.sortByKey(true).collect()
res9: Array[(Int, String)] = Array((1,dd), (2,bb), (3,aa), (6,cc))

// 3)按照key的倒序
scala> rdd.sortByKey(false).collect()
res10: Array[(Int, String)] = Array((6,cc), (3,aa), (2,bb), (1,dd))

mapValue

作用:针对于(K,V)形式的类型只对V进行操作

下图中的方框代表 RDD 分区。 a=>a+2 代表对 (V1,1) 这样的 Key Value 数据对,数据只对 Value 中的 1 进行加 2 操作,返回结果为 3。
在这里插入图片描述
举例

// 1)创建一个pairRDD
scala> val rdd3 = sc.parallelize(Array((1,"a"),(1,"d"),(2,"b"),(3,"c")))
rdd3: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[67] at parallelize at <console>:24

// 2)对value添加字符串"|||"
scala> rdd3.mapValues(_+"|||").collect()
res26: Array[(Int, String)] = Array((1,a|||), (1,d|||), (2,b|||), (3,c|||))

join(otherDataset, [numTasks])

作用:在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD

详解
join 对两个需要连接的 RDD 进行 cogroup函数操作,将相同 key 的数据能够放到一个分区,在 cogroup 操作之后形成的新 RDD 对每个key 下的元素进行笛卡尔积的操作,返回的结果再展平,对应 key 下的所有元组形成一个集合。最后返回 RDD[(K, (V, W))]。
下 面 代 码 this.cogroup(other,partitioner).f latMapValues{case(vs,ws) => for(v<-vs;w<-ws)yield(v,w) }为 join 的 函 数 实 现, 本 质 是通 过 cogroup 算 子 先 进 行 协 同 划 分, 再 通 过flatMapValues 将合并的数据打散。

下图是对两个 RDD 的 join 操作示意图。大方框代表 RDD,小方框代表 RDD 中的分区。函数对相同 key 的元素,如 V1 为 key 做连接后结果为 (V1,(1,1)) 和 (V1,(1,2))。
在这里插入图片描述

举例

// 1)创建第一个pairRDD
scala> val rdd = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c")))
rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[32] at parallelize at <console>:24

// 2)创建第二个pairRDD
scala> val rdd1 = sc.parallelize(Array((1,4),(2,5),(3,6)))
rdd1: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[33] at parallelize at <console>:24

// 3)join操作并打印结果
scala> rdd.join(rdd1).collect()
res13: Array[(Int, (String, Int))] = Array((1,(a,4)), (2,(b,5)), (3,(c,6)))

leftOutJoin和rightOutJoin

LeftOutJoin(左外连接)和RightOutJoin(右外连接)相当于在join的基础上先判断一侧的RDD元素是否为空,如果为空,则填充为空。 如果不为空,则将数据进行连接运算,并返回结果。

cogroup(otherDataset, [numTasks])

cogroup是先RDD内部分组,在RDD之间分组

作用:在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD。

举例
在这里插入图片描述

scala> val rdd = sc.parallelize(Array((1,"a"),(2,"b"),(3,"c")))
rdd: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[37] at parallelize at <console>:24

scala> val rdd1 = sc.parallelize(Array((1,4),(2,5),(3,6)))
rdd1: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[38] at parallelize at <console>:24

scala> rdd.cogroup(rdd1).collect()
res14: Array[(Int, (Iterable[String], Iterable[Int]))] = Array((1,(CompactBuffer(a),CompactBuffer(4))),(2,(CompactBuffer(b),CompactBuffer(5))),(3,(CompactBuffer(c),CompactBuffer(6))))

scala> val rdd2 = sc.parallelize(Array((4,4),(2,5),(3,6)))
rdd2: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[41] at parallelize at <console>:24

scala> rdd.cogroup(rdd2).collect()
res15: Array[(Int, (Iterable[String], Iterable[Int]))] = Array((4,(CompactBuffer(),CompactBuffer(4))),(1,(CompactBuffer(a),CompactBuffer())),(2,(CompactBuffer(b),CompactBuffer(5))),(3,(CompactBuffer(c),CompactBuffer(6))))

scala> val rdd3 = sc.parallelize(Array((1,"a"),(1,"d"),(2,"b"),(3,"c")))
rdd3: org.apache.spark.rdd.RDD[(Int, String)] = ParallelCollectionRDD[44] at parallelize at <console>:24

scala> rdd3.cogroup(rdd2).collect()
res16: Array[(Int, (Iterable[String], Iterable[Int]))] = Array((4,(CompactBuffer(),CompactBuffer(4))), (1,(CompactBuffer(d,a),CompactBuffer())), (2,(CompactBuffer(b),CompactBuffer(5))),(3,(CompactBuffer(c),CompactBuffer(6))))

RDD 动作算子

  • 本质上动作算子通过 SparkContext 执行提交作业操作,触发 RDD DAG(有向无环图)的执行
  • 所有的动作算子都是急迫型(non-lazy),RDD 遇到 Action 就会立即计算

Action 算子会触发 Spark 提交作业(Job),并将数据输出 Spark系统。

reduce(func)

作用:通过 func 函数聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据。

详解

reduce函数相当于对RDD中的元素进行reduceLeft函数的操作。 函数实现如下Some(iter.reduceLeft(cleanF))

reduceLeft先对两个元素<K,V>进行reduce函数操作,然后将结果和迭代器取出的下一个元素<k,V>进行reduce函数操作,直到迭代器遍历完所有元素,得到最后结果。在RDD中,先对每个分区中的所有元素<K,V>的集合分别进行reduceLeft。 每个分区形成的结果相当于一个元素<K,V>,再对这个结果集合进行reduceleft操作。

例如,用户自定义函数f:(A,B)=>(A._1+”@”+B._1,A._2+B._2),下图中的方框代表一个RDD分区,通过用户自定函数f将数据进行reduce运算。 示例最后的返回结果为V1@[1]V2U!@U2@U3@U4,12
在这里插入图片描述
举例

scala> val rdd1 = sc.makeRDD(1 to 10,2)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[85] at makeRDD at <console>:24

scala> rdd1.reduce(_+_)
res50: Int = 55

scala> val rdd2 = sc.makeRDD(Array(("a",1),("a",3),("c",3),("d",5)))
rdd2: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[86] at makeRDD at <console>:24

scala> rdd2.reduce((x,y)=>(x._1 + y._1,x._2 + y._2))
res51: (String, Int) = (adca,12)

reduceByKeyLocally

实现的是先reduce再collectAsMap的功能,先对RDD的整体进行reduce操作,然后再收集所有结果返回为一个HashMap。

collect()

作用collect 相当于 toArray, toArray 已经过时不推荐使用, collect 将分布式的 RDD 返回为一个单机的 scala Array 数组。在这个数组上运用 scala 的函数式操作。

下图中左侧方框代表 RDD 分区,右侧方框代表单机内存中的数组。通过函数操作,将结果返回到 Driver 程序所在的节点,以数组形式存储。

在这里插入图片描述
举例

// 1)创建一个RDD
scala> val rdd = sc.parallelize(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

// 2)将结果收集到Driver端
scala> rdd.collect
res0: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

collectAsMap

collectAsMap对(K,V)型的RDD数据返回一个单机HashMap。 对于重复K的RDD元素,后面的元素覆盖前面的元素。

下图中的左侧方框代表RDD分区,右侧方框代表单机数组。 数据通过collectAsMap函数返回给Driver程序计算结果,结果以HashMap形式存储。
在这里插入图片描述

count()

作用:返回RDD中元素的个数

详解

内部函数实现为:defcount():Long=sc.runJob(this,Utils.getIteratorSize_).sum
  
下图中,返回数据的个数为 5。一个方块代表一个 RDD 分区。
在这里插入图片描述
举例

// 1)创建一个RDD
scala> val rdd = sc.parallelize(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

// 2)统计该RDD的条数
scala> rdd.count
res1: Long = 10

first

作用:返回RDD中的第一个元素,类似于take(1)

举例

// 1)创建一个RDD
scala> val rdd = sc.parallelize(1 to 10)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24

// 2)统计该RDD的条数
scala> rdd.first
res2: Int = 1

take(n)

作用:返回一个由RDD的前n个元素组成的数组

举例

// 1) 创建一个RDD
scala> val rdd = sc.parallelize(Array(2,5,4,6,8,3))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at <console>:24

// 2)统计该RDD的条数
scala> rdd.take(3)
res10: Array[Int] = Array(2, 5, 4)

takeOrdered(n)

作用:返回该RDD排序后的前n个元素组成的数组

举例

// 1)创建一个RDD
scala> val rdd = sc.parallelize(Array(2,5,4,6,8,3))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[2] at parallelize at <console>:24

// 2)统计该RDD的条数
scala> rdd.takeOrdered(3)
res18: Array[Int] = Array(2, 3, 4)

aggregate

参数(zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)

作用:aggregate 函数将每个分区里面的元素通过 seqOp 和初始值进行聚合,然后用 combine 函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致。

详解

aggregate先对每个分区的所有元素进行aggregate操作,再对分区的结果进行fold操作。aggreagate与fold和reduce的不同之处在于,aggregate相当于采用归并的方式进行数据聚集,这种聚集是并行化的。 而在fold和reduce函数的运算过程中,每个分区中需要进行串行处理,每个分区串行计算完结果,结果再按之前的方式进行聚集,并返回最终聚集结果。
函数的定义如下:aggregate[B](z: B)(seqop: (B,A) => B,combop: (B,B) => B): B

下图通过用户自定义函数rdd.aggregate(”V0@”,2)((A,B)=>(A._1+”@”+B._1,A._2+B._2)),(A,B)=>(A._1+”@”+B_1,A._@+B_.2))对RDD 进行aggregate的聚集操作,图中的每个方框代表一个RDD分区。
  
最后,介绍两个计算模型中的两个特殊变量。
广播(broadcast)变量:其广泛用于广播Map Side Join中的小表,以及广播大变量等场景。 这些数据集合在单节点内存能够容纳,不需要像RDD那样在节点之间打散存储。
Spark运行时把广播变量数据发到各个节点,并保存下来,后续计算可以复用。 相比Hadoo的distributed cache,广播的内容可以跨作业共享。 Broadcast的底层实现采用了BT机制。

在这里插入图片描述

举例

// 1)创建一个RDD
scala> var rdd1 = sc.makeRDD(1 to 10,2)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[88] at makeRDD at <console>:24

// 2)将该RDD所有元素相加得到结果
scala> rdd.aggregate(0)(_+_,_+_)
res22: Int = 55

saveAsTextFile(path)

作用:将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本。

下面为 saveAsTextFile 函数的内部实现,其内部通过调用 saveAsHadoopFile 进行实现:this.map(x => (NullWritable.get(), new Text(x.toString))).saveAsHadoopFile[TextOutputFormat[NullWritable, Text]](path)将 RDD 中的每个元素映射转变为 (null, x.toString),然后再将其写入 HDFS。

下图中左侧方框代表 RDD 分区,右侧方框代表 HDFS 的 Block。通过函数将RDD 的每个分区存储为 HDFS 中的一个 Block。
在这里插入图片描述

saveAsSequenceFile(path)

作用:将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。

saveAsObjectFile(path)

作用:用于将RDD中的元素序列化成对象,存储到文件中。

saveAsObjectFile将分区中的每10个元素组成一个Array,然后将这个Array序列化,映射为(Null,BytesWritable(Y))的元素,写入HDFS为SequenceFile的格式。下面代码为函数内部实现map(x=>(NullWritable.get(),new BytesWritable(Utils.serialize(x))))
 
下图中的左侧方框代表RDD分区,右侧方框代表HDFS的Block。 通过函数将RDD的每个分区存储为HDFS上的一个Block。
在这里插入图片描述

countByKey()

作用:针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。

举例

// 1)创建一个PairRDD
scala> val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[95] at parallelize at <console>:24

// 2)统计每种key的个数
scala> rdd.countByKey
res63: scala.collection.Map[Int,Long] = Map(3 -> 2, 1 -> 3, 2 -> 1)

foreach(func)

作用:在数据集的每一个元素上,运行函数func进行更新。

举例

// 1)创建一个RDD
scala> var rdd = sc.makeRDD(1 to 5,2)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[107] at makeRDD at <console>:24

// 2)对该RDD每个元素进行打印
scala> rdd.foreach(println(_))
3
4
5
1
2

fold

fold和reduce的原理相同,但是与reduce不同,相当于每个reduce时,迭代器取的第一个元素是zeroValue。

下图中通过下面的用户自定义函数fold((”V0@”,2))( (A,B)=>(A._1+”@”+B._1,A._2+B._2))进行fold运算,图中的一个方框代表一个RDD分区。 读者可以参照reduce函数理解。

在这里插入图片描述

top

top可返回最大的k个元素。 函数定义如下top(num:Int)(implicit ord:Ordering[T]):Array[T]

相近函数说明如下。

  • top返回最大的k个元素。
  • take返回最小的k个元素。
  • takeOrdered返回最小的k个元素,并且在返回的数组中保持元素的顺序。
  • first相当于top(1)返回整个RDD中的前k个元素,可以定义排序的方式Ordering[T]。
    返回的是一个含前k个元素的数组。

本次分享就到这里了,不足之处欢迎大家指点!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值