spark算子合集

在对rdd进行操作之前我们首先需要对rdd进行初步的了解rdd的五大特性

RDD 是 Resilient Distributed Dataset 也就是我们常说的弹性分布式数据集

至于何为弹性,何为分布式 就需要看我们接下来的内容了

特性一

有很多分区:每个rdd中都包含很多个分区 每个分区都会对应一个task 也就是说分区的数量决定了任务并行计算粒度 我们可以在创建rdd时来指定分区 也可以通过后续介绍的一些算子来进行分区的调整 而默认的分区数量便是我们在建立conf 中指定给的cpu核数量 每个分配的存储都是由 blockmananger实现的 每个分区都会被映射成为一个block excutor上的blockmananger通过 netty编写的网络接口与 blockmanagerMaster之间进行通信 提交block的信息 具体的存储体系会在后面的博客中写到

特性二

spark中rdd的数据都是以 分区为基本单位

特性三

与其他rdd之间的依赖关系 rdd之间的依赖关系在spark中是很明确的 rdd之间的依赖分为宽依赖和窄依赖 通俗来说宽依赖就会产生shuffle 当任务发生故障时可以通过找到rdd的依赖关系来从之前的rdd上读取数据而不用重跑所有的rdd 在hadoop中是通过多节点的备份来容灾 不过这种形式会带来io产生延迟 降低了任务的速度

特性四

对于k-v类型的数据我们可以指定分区器 而对于没有key的类型的数据使用的默认的none.partitioner

特性五

存储每个切片优先的列表 按照“移动数据不如移动计算的理念” 在任务调度时尽可能的将计算任务分配到其要处理的数据块的存储位置

rdd的简单介绍就到此为止了

我们可以通过

如何创建rdd

两个简单的

sparkcontext.parallelize()

sparkcontext.textFile()

sparkcontext.makerdd()

具体的创建方法在这就不多说了 重点是对rdd的操作

我们就对两组rdd数据进行操作

//数据的格式   姓名 身份证号 性别  身高  体重  学历  对象   
val arr1=Array(Array("langge","431122123334324562","man","168","63","本科","xuanmei"),Array("xuanmei","431129399488859000","woman","164","53","准研究生","langge"))
    val arr2=Array("xuanmei","431129399488859000","woman","164","53","准研究生","langge")
val arr3 =Array("langge","431122123334324562","man","168","63","本科","xuanmei")

转换算子

map

再常见不过的算子 就是对rdd每个元素都进行map作操作

例如对每个元素加上后缀

rdd1.map(str => {
      if (str(0)=="xuanmei") (str,"haonvhai")
      else if (str(0)=="langge") (str,"haonanhai")
      else str
    })
(("langge","431122123334324562","man","168","63","本科","xuanmei"),"haonanren"),(("xuanmei","431129399488859000","woman","164","53","研究生","langge"),"haonvren")

//小插曲
//在用下面这段代码做打印时出现了问题
    val result = rdd_transfom.map(str => {
      str match {
        case (array: Array[String], y) => for (i <- array) {
          print(i + "+" )
        }
          print(y)
          println()
      }
    })

//打印出来的结果相当奇怪langge+xuanmei+431122123334324562+431129399488859000+man+woman+168+164+63+53+本科+研究生+xuanmei+langge+haonvhaihaonanhai

认真看过前面rdd特性的可能知道spark人物都是多线程的 我们在做输出的时候无法保证顺序 只是的局限性我也不知如何对要打印的数据进行加锁 索性把分区改成 1 这样就不会出现多线程的问题了

线程数由分区数决定 分区数就由 所分配到的cpu的核心数决定 数据量不大索性改一改参数或者采用另外一种打印rdd的操作

解决1:
new  sparkconf.setAppname("lang").setMaster("local[1]") 

解决2:

    for(item<-result.collect()){
      println(item)
    }

langge+431122123334324562+man+168+63+本科+xuanmei+haonanhai
xuanmei+431129399488859000+woman+164+53+研究生+langge+haonvhai

这样数据的打印顺序就不会发生错误了

filter(function)

顾名思义 这个算子就是用来做数据的过滤的处理 同样以rdd1为例 我们过滤掉 学历为本科的只保留研究生

val rdd_transfom = rdd1.filter(str => {
      str.contains("研究生")
    })

xuanmei+431129399488859000+woman+164+53+研究生+langge

flatMap(function)

与map操作类似 不过是可以对应多个输出 比如 我们把rdd1进行扁平化操作 并且将结果的字符串长度打印出来

val rdd_transfom = rdd1.flatMap(row => {for (i <- row) yield i})

我们对数据进行扁平化操作  
6
18
3
3
2
2
7
7
18
5
3
2
3
6
返回的结果以及不是  array[]数组的长度  而是  string 的长度

mapPartitions(function)

这个名字就写的非常清楚了 对rdd中的每个分区做一次map操作

例如我们将每个元素的第一个字符串都转成大写

val result = rdd1.mapPartitions(it => {
      it.map(person => {
        person match {
          case array: Array[String] => {
            val tmp= array(0)=array(0).toUpperCase
            array
          }
        }
      })
    })


LANGGE
431122123334324562
man
168
63
本科
xuanmei
XUANMEI
431129399488859000
woman
164
53
研究生
langge

mapPartitionsWithIndex(function)

就是一个mappartition的升级版 给我们提供了一个分区的索引

比如我们将名字后面加上分区索引

val result = rdd1.mapPartitionsWithIndex((index,it) => {
      it.map(person => {
        person match {
          case array: Array[String] => {
            val tmp= array(0)=array(0).toUpperCase +"    分区号码    " +   index.toString
            array
          }
        }
      })
    })

map和mappartition的区别

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

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

所以mappartitions适合在内存空间足够的情况下使用

sample(withReplacement, fraction, seed)

此算子就是抽样元素

第一个参数表示 是否放回抽样

第二个参数表示 抽样的比例

第三个参数表示 随机数的种子 我们打开源码可以发现实际上默认用的是时间戳做随机数种子我们在这就使用默认的

def sample(
      withReplacement: Boolean,
      fraction: Double,
      seed: Long = Utils.random.nextLong): RDD[T]

在这里我们将rdd1中的数据扁平化处理后 不放回随机取中间百分之50的数元素

val result = rdd1.flatMap(row => {for (i <- row) yield i}).sample(false,0.5)

man
168
本科
xuanmei
431129399488859000
164
53

distinct([numTasks])

对rdd中的元素进行去重操作 可以通过参数来指定任务的数量 默认与分区数相同

这里我们对数据进行扁平化操作后对相同的数据进行了去重操作

val result = rdd1.flatMap(row => {for (i <- row) yield i}).distinct(2)

53
langge
man
woman
431129399488859000
168
研究生
本科
xuanmei
164
63
431122123334324562

glom()

将每一个分区的元素合并成一个数组,形成新的 RDD

在这里我们扁平化之后再将其进行glom操作 设置分区数为4

val result = rdd1.flatMap(row => {for (i <- row) yield i}).glom()



langge431122123334324562man16863本科xuanmei

xuanmei431129399488859000woman16453研究生langge

中间有两个空格是因为只有两行数据  再运行时虽然指定了 4个分区 但是还有两个分区没有数据

coalesce(numPartitions)

一个用来减少分区数目的算子 这个就不需要代码展示 相当简单 指定一个数 就可以将分区减小到指定 注意只能减少

第二个参数可以指定是否发生shuffle 会产生宽依赖也就是会发生shuffle

repartition(numPartitions)

与上面一个算子类似 区别是 这个算子可以增加也可以减少分区 不过会产生shuffle 我们可以使用rdd.partitions.length 来查看分区数

sortByKey([ascending], [numTasks])

wow名字就很清楚 按照key来排序 我们这里可以对数据进行扁平化后 再将每个元素hash 按照hashcode来排序

val result = rdd1.flatMap(row => {for (i <- row) yield i}).sortBy(x => x.hashCode,true)


xuanmei
xuanmei
431122123334324562
langge
langge
53
63
164
168
man
本科
研究生
woman
431129399488859000

pipe(command,[envVars])

针对每个分区,把 RDD 中的每个数据通过管道传递给shell命令或脚本,返回输出的RDD。一个分区执行一次这个命令. 如果只有一个分区, 则执行一次命令

这里不方便展示就不做编码了

union(otherDataSet)

两个rdd求并集

union(Seq(first) ++ rest)
可以看到底层的代码其实就是对两个rdd进行了 ++ 也就是集合之间的++ 

val result = rdd3.union(rdd2)

langge
431122123334324562
man
168
63
本科
xuanmei

xuanmei
431129399488859000
woman
164
53
本科
langge

intersection(otherDataSet)

两个rdd求交集

val result = rdd3.intersection(rdd2)

本科
xuanmei
langge

subtract (otherDataset)

两个rdd求差集

val result = rdd3.subtract(rdd2)

168
431122123334324562
man
63

cartesian(otherDataSet)

两个rdd做笛卡尔积

val result = rdd3.cartesian(rdd2)

//结果太多了在此只做部分展示
(langge,xuanmei)
(langge,431129399488859000)
(langge,woman)
(langge,164)
(langge,53)
(langge,本科)
(langge,langge)
(431122123334324562,xuanmei)
(431122123334324562,431129399488859000)
(431122123334324562,woman)
(431122123334324562,164)
(431122123334324562,53)
(431122123334324562,本科)
(431122123334324562,langge)

zip(otherDataset)

对两个rdd做拉链 前提的rdd的分区数和字段数都需一致

val result = rdd3.zip(rdd2)

(langge,xuanmei)
(431122123334324562,431129399488859000)
(man,woman)
(168,164)
(63,53)
(本科,本科)
(xuanmei,langge)

后面的数据大多数都是需要k-v类型的我在此将数据进行加后缀后处理

partitionBy(partitioner)

按照key来分区

对数据进行加后缀之后 再用hash分区器分区 当传入的分区器与原rdd分区器一致时 会直接返回原rdd

val result = rdd3.map(str => {
  var i =0;
  i+=1
  (str,i)
}).partitionBy(new org.apache.spark.HashPartitioner(3))

cogroup(otherDataSet,[numTasks])

将(K,A)和(K,B)两种类型的rdd合并形成 (K,((Iterable,Iterable)))类型的rdd

我们对rdd做两次不同的处理 分别加上不同类型的后缀

val result= rdd2.map((_, Random.nextInt(7))).cogroup(rdd2.map((_, "a")))

(53,(CompactBuffer(3),CompactBuffer(a)))
(431129399488859000,(CompactBuffer(4),CompactBuffer(a)))
(本科,(CompactBuffer(2),CompactBuffer(a)))
(xuanmei,(CompactBuffer(4),CompactBuffer(a)))
(164,(CompactBuffer(3),CompactBuffer(a)))
(langge,(CompactBuffer(4),CompactBuffer(a)))
(woman,(CompactBuffer(5),CompactBuffer(a)))

groupByKey([numTasks])

按照key了分组

在此我们对数据加后缀并且 分区

val result = rdd3
      .union(rdd2)
      .map(str => {(str,1)})
      .groupByKey()
      
(431129399488859000,CompactBuffer(1))
(168,CompactBuffer(1))
(本科,CompactBuffer(1, 1))
(xuanmei,CompactBuffer(1, 1))
(164,CompactBuffer(1))
(63,CompactBuffer(1))
(431122123334324562,CompactBuffer(1))

reduceByKey(function,[numTasks])

按照key来聚合

在此我们对数据加后缀 然后聚合

val result = rdd3
      .union(rdd2)
      .map(str => {(str,1)})
      .reduceByKey((x,y) => x + y)


(53,1)
(langge,2)
(man,1)
(woman,1)
(431129399488859000,1)
(168,1)
(本科,2)
(xuanmei,2)
(164,1)
(63,1)
(431122123334324562,1)

aggregateByKey(zeroValue)(seqOp, combOp, [numTasks])、

名字长 参数多 一看这个算子就不简单

zeroValue 对每个分区的聚合结果附上初始值

seqOp 单个分区所做操作的function

combOp 对每个分区做完第一个操作之后的结果做操作的操作

numTasks 可选择传入的任务数

在这我们对数据进行加入随机后缀 然后分区内取最大 分区间求和

val result = rdd3
      .union(rdd2)
      .map(str => {(str,Random.nextInt(15))})
      .aggregateByKey(0)( (x,y)=>{
        Math.max(x,y)
      },(x,y) => {
        x+y
      })
为了方便观看 不在这做缩写了

(53,7)
(langge,7)
(man,1)
(woman,8)
(431129399488859000,12)
(168,1)
(本科,8)
(xuanmei,8)
(164,0)
(63,8)
(431122123334324562,6)

foldByKey(zeroValue)(Op ,[numTasks])

理解了上面那个这就很简单 这就是上一个的简化版 不过分区间的操作和分区内的操作变成了同一个

val result = rdd3
  .union(rdd2)
  .map(str => {(str,Random.nextInt(15))})
  .foldByKey(0)( (x,y) => {
    x+y
  })
  
  
(53,14)
(langge,15)
(man,9)
(woman,7)
(431129399488859000,8)
(168,0)
(本科,10)
(xuanmei,3)
(164,9)
(63,3)
(431122123334324562,11)

join(otherDataSet,[numTasks])

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

其实就是内连接

加后缀然后内连接

val result= rdd2.map((_,1)).join(rdd3.map((_,2)))

(本科,(1,2))
(xuanmei,(1,2))
(langge,(1,2))

combineByKey(createCombiner, mergeValue, mergeCombiners)

一个用来对(k,v) 类型的数据做操作的算子

createCombiner 当第一次遇见一个k时 对这个k所对应的 v 做的操作

mergeValue 当在此遇见这个key时 传入当前的这个key所对应的数值和在此遇见的数值

mergeCombiners 一个分区处理完之后 对所有分区做集合的操作

例如

分区1

(53,3) 第一次遇见 key=53 调用createCombiner 转换成 53,(3,1) 再一次遇见 53,6 调用mergeValue转换成

53,(9,2)

分区2

(53,5) 第一次遇见 key=53 调用createCombiner 转换成 53,(5,1) 再一次遇见 53,3 调用mergeValue转换成

53,(8,2)

最后分区之间的合并 合并成为 (53,(17,4)) (举例假数据 并非成分生成)

val result= rdd2
  .union(rdd3)
  .map((_,Random.nextInt(16)))
  .combineByKey( (_,1),
    (x :(Int,Int),y) => (x._1+y,x._2+1),
    (m:(Int,Int),n: (Int,Int) ) => { ((m._1+n._1),(m._2+n._2))})
    
(53,(12,1))
(langge,(18,2))
(man,(1,1))
(woman,(13,1))
(431129399488859000,(13,1))
(168,(13,1))
(本科,(19,2))
(xuanmei,(13,2))
(164,(5,1))
(63,(2,1))
(431122123334324562,(12,1))

sortbykey

按照key排序

val result= rdd2
  .union(rdd3)
  .map((_,Random.nextInt(16)))
    .sortByKey()
    
    
(164,3)
(168,6)
(431122123334324562,2)
(431129399488859000,0)
(53,0)
(63,10)
(langge,6)
(langge,13)
(man,0)
(woman,1)
(xuanmei,2)
(xuanmei,10)
(本科,13)
(本科,15)

mapvalues()

只对(k,v)中的v做操作

val result= rdd3
  .map((_,Random.nextInt(16)))
    .mapValues(value =>  {
      val tmp=value+999
      tmp
    })
    
(langge,1001)
(431122123334324562,1014)
(man,1000)
(168,1005)
(63,1005)
(本科,1003)
(xuanmei,1000)

行动算子

区别与行动算子与转换算子的最大区别 rdd是懒执行只有遇到行动算子 前面的算子才会执行

reduce(function)

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

val result= rdd2
      .union(rdd3)
      .map((_,Random.nextInt(16)))
    println(result.reduce((x, y) => {
      (x._1, x._2 + y._2)
    }))


(xuanmei,105)  不管key是什么反正聚合就完事了

collect()

以数组的形式返回 RDD 中的所有元素.

所有的数据都会被拉到 driver 端, 所以要慎用

我们可以for遍历这个数组并且打印

val result= rdd2
      .union(rdd3)
      .map((_,Random.nextInt(16)))

    val collect = result.collect
    for (kv <- collect) println(kv)
    
    
(xuanmei,5)
(431129399488859000,9)
(woman,3)
(164,12)
(53,13)
(本科,7)
(langge,10)
(langge,12)
(431122123334324562,5)
(man,8)
(168,4)
(63,3)
(本科,3)
(xuanmei,13)

count()

做计数的算子

val result= rdd2
  .union(rdd3)
  .map((_,Random.nextInt(16)))

println(result.count())

14

first()

就相当于take(1)

takeSample(withReplacement, num, [seed])

与之前的sample函数类似不过区别是中间的num需要传入 int类型数值

val result= rdd2
      .union(rdd3)
      .map((_,Random.nextInt(16)))

    for (elem <- result.takeSample(false,5)) {println(elem)}


(本科,3)
(man,11)
(431122123334324562,4)
(xuanmei,3)
(woman,4)

take(n)

返回 RDD 中前 n 个元素组成的数组

val result= rdd2
  .union(rdd3)
  .map((_,Random.nextInt(16)))

for (elem <- result.take(4)) {println(elem)}

(xuanmei,9)
(431129399488859000,7)
(woman,7)
(164,11)

takeOrdered(n,[ordering])

返回排序后的前 n 个元素, 默认是升序排列

val result= rdd2
  .union(rdd3)
  .map((_,Random.nextInt(16)))

for (elem <- result.takeOrdered(4)) {println(elem)}

(164,0)
(168,4)
(431122123334324562,10)
(431129399488859000,0)

aggregate

与之前的与其名字相似的算子效果类似 在此不做赘述

saveAsTextFile(path)

以text文件的形式 保存rdd

saveAsObjectFile(path)

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

countByKey()

也是计数 不过是按照key来计数了

foreach(function)

可以理解为转换算子中的map 一般用来做打印操作 不过操作是再excutor端做操作 而不是 deiver端

重点记录:

RDD checkpoints存在的意义

RDDs 之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。

为此,RDD 支持checkpoint 将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint 后的 RDD 不需要知道它的父 RDDs 了,它可以从 checkpoint 处拿到数据。

reducebykey 与 groupbykey 的选择

如何使用groupbykey也是为了聚合我们就尽量再不影响业务的情况下选择 reducebykey 因为 后者再shuffle前会进行预聚合的操作