RDD、DataFrame与DataSet|Spark常用算子

本文详细介绍了Spark中的RDD、DataFrame和DataSet的区别与联系,包括它们的特性和转换方法。此外,还对比了Spark常用算子,如map、flatMap、mapPartition、reduce、reduceByKey、groupByKey、join、distinct、combineByKey、aggregateByKey等的用法和性能考虑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


  DataFrame是spark1.3.0版本提出来的,spark1.6.0版本又引入了DateSet的。DataFrame、DataSet是基于RDD的,三者之间可以通过简单的API调用进行无缝切换。

RDD、DataFrame与DataSet区别

RDD

  RDD弹性分布式数据集。与DataFrame,DataSet不同,RDD不支持Spark SQL。

DataFrame

  在Spark中,DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库中的二维表格。与RDD和Dataset不同,DataFrame每一行的类型固定为Row,只有通过解析才能获取各个字段的值,每一列的值没法直接访问。DataFrame与Dataset均支持SparkSQL操作,比如select、groupby等

testDF.foreach{
  line =>
    val col1=line.getAs[String]("col1")
    val col2=line.getAs[String]("col2")
}

DataFrame与RDD的主要区别:DataFrame带有schema元信息,即DataFrame所表示的二维表数据集的每一列都带有名称和类型。

DataSet

  Dataset是一个由特定领域的对象组成强类型(typedrel)集合,可以使用函数(DSL)或关系运算(SQL)进行并行的转换操作。 每个Dataset 还有一个称为“DataFrame”的无类型(untypedrel)视图,它是[[Row]]的数据集。

RDD和Dataset
区别:
  Dataset不使用Java序列化或Kryo,而是使用专用的Encoder编码器来序列化对象以便通过网络进行处理或传输。虽然Encoder编码器和标准序列化都负责将对象转换为字节,但Encoder编码器是动态生成的代码,并使用一种格式,允许Spark执行许多操作,如过滤,排序和散列,而无需将字节反序列化为对象

Dataset和DataFrame
区别:
  Dataset是强类型typedrel的,会在编译的时候进行类型检测;而DataFrame是弱类型untypedrel的,在执行的时候进行类型检测;Dataset是通过Encoder进行序列化,支持动态的生成代码,直接在bytes的层面进行排序,过滤等的操作;而DataFrame是采用可选的java的标准序列化或是kyro进行序列化

联系:
  DataFrame是Dataset中每一个元素为Row类型的特殊情况。DataFrame和Dataset实质上都是一个逻辑计划,并且是懒加载的,都包含着scahema信息,只有到数据要读取的时候,才会将逻辑计划进行分析和优化,并最终转化为RDD。

RDD、DataFrame与DataSet转化

DataFrame/Dataset转RDD:

val rdd1=testDF.rdd
val rdd2=testDS.rdd

RDD转DataFrame:

import spark.implicits._
val testDF = rdd.map {line=>
      (line._1,line._2)
    }.toDF("col1","col2")

一般用元组把一行的数据写在一起,然后在toDF中指定字段名

RDD转Dataset:

import spark.implicits._
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS = rdd.map {line=>
      Coltest(line._1,line._2)
    }.toDS

可以注意到,定义每一行的类型(case class)时,已经给出了字段名和类型,后面只要往case class里面添加值即可

Dataset转DataFrame:

//把case class封装成Row
import spark.implicits._
val testDF = testDS.toDF

DataFrame转Dataset:

import spark.implicits._
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS = testDF.as[Coltest]

这种方法就是在给出每一列的类型后,使用as方法,转成Dataset,这在数据类型是DataFrame又需要针对各个字段处理时极为方便

Spark常用算子比较

map与flatmap

  • map算子会对每一条输入进行指定的操作,然后为每一条输入返回一个对象
  • flatMap算子是将RDD里的每一个元素执行自定义函数f,这时这个元素的结果转换成iterator,最后将这些再拼接成一个新的RDD

mapPartition与map

  map是对rdd中的每一个元素进行操作。mapPartitions则是对rdd中的每个分区的迭代器进行操作,这个分区的数据处理完后,原RDD中分区的数据才能释放,可能导致OOM。
  当内存空间较大的时候建议使用mapPartition,以提高处理效率。如果在map过程中需要频繁创建额外的对象(例如将rdd中的数据通过jdbc写入数据库,map需要为每个元素创建一个链接而mapPartition为每个partition创建一个链接),则mapPartitions效率比map高的多。

reduce、reduceByKey与groupByKey

  reduceByKey是transformation算子;reduce是action算子,聚合RDD中的所有元素。
  reduceByKey用于对每个key对应的多个value进行merge操作,最重要的是它能够在本地先进行merge操作,并且merge操作可以通过函数自定义。groupByKey也是对每个key进行操作,但只生成一个sequence,groupByKey本身不能自定义操作函数。如果需要对sequence进行aggregation操作,那么选择reduceByKey更好。这是因为groupByKey不能自定义函数,需要先用groupByKey生成RDD,然后才能对此RDD通过map进行自定义函数操作。
  reduceByKey默认在map端进行合并,即在shuffle前进行合并,如果合并了一些数据,那在shuffle时进行溢写则减少了磁盘IO,所以reduceByKey会快一些。当采用groupByKey时,由于它不接收函数,spark只能先将所有的键值对(key-value pair)都移动,这样的后果是集群节点之间的开销很大,导致传输延时。在对大数据进行复杂计算时,reduceByKey优于groupByKey。

join

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

补充:join时何时产生shuffle,何时不产生shuffle?
  Spark中产生宽窄依赖的依据是shuffle,当发生shuffle时,会产生宽依赖,基本上shuffle算子都会产生宽依赖,但是join除外,在执行join算子之前如果先执行groupByKey,执行groupByKey之后,会把相同的key分到同一个分区,再执行join算子,join算子是把key相同的进行join,父RDD的分区只对应一个子RDD的分区,不一定会产生shuffle。

distinct

distinct算子将RDD中的元素进行去重操作,是宽依赖。

combineByKey

groupByKey/reduceByKey底层是由combineByKey实现的。

def combineByKey[C](
      createCombiner: V => C,
      mergeValue: (C, V) => C,
      mergeCombiners: (C, C) => C,
      partitioner: Partitioner,
      mapSideCombine: Boolean = true,
      serializer: Serializer = null)
  • createCombiner当第一次遇到key时,调用这个函数,将key对应的V转换成C(初始化操作)
  • mergeValue不是第一次遇到key时,调用这个函数,将key对应的元素V合并到之前的元素C(createCombiner)上 (对这个分区中相同的key的进一步操作)
  • -mergeCombiners该函数将相同Key的C合并成一个C(这个操作在不同分区间进行)

除此之外,还包含三个默认参数:
partitioner:分区函数,默认为HashPartitioner
mapSideCombine:是否需要在Map端进行combine操作,类似于MapReduce中的combine,默认为true
serializer:序列化,用于数据存储和传输

aggregateByKey

  函数对PairRDD中相同Key的值进行聚合操作。因为aggregateByKey是对相同Key中的值进行聚合操作,所以aggregateByKey函数最终返回的类型还是Pair RDD,对应的结果是Key和聚合好的值;而aggregate函数直接是返回非RDD的结果,这点需要注意。
  aggregateByKey()()存在两个参数列表,使用了函数柯里化:
zeroValue:表示分区内计算时的初始值,类型U要注意,最终返回的类型也必须是这个类型U

seqOp(U,Int):表示分区内计算规则,这个方法主要是做相同key在同一个partition的聚合操作,两个参数(U,int)第一个参数是开始初始值U,第二个类型是数据的value的类型,返回类型为定义的zeroValue的类型

combOp:表示分区间计算规则,根据key 对不同分区的数据进行一个聚合操作(也就是对seqOp的结果做合并操作),这个参数(U,U)这两个的类型都是seqOp返回类型

  性能调优中有个方案, 使用 aggregateBykey 代替 groupbykey。因为aggregateByKey,使用map-side预聚合的shuffle操作, 相当于在Map端进行了聚合的操作,相当于MapReduce 中进行combiner

repartition和coalesce

repartition(numPartitions:Int)和coalesce(numPartitions:Int,shuffle:Boolean=false)
作用:对RDD的分区进行重新划分,repartition内部调用了coalesce,参数shuffle为true。coalesce只能减少分区,而repartition可以减少和增加。
  对于coalesce:如果是生成一个窄依赖的结果,那么不会发生shuffle。比如:1000个分区被重新设置成10个分区,这样不会发生shuffle。即窄依赖:父RDD的多个分区对应一个子RDD分区。当把父RDD的分区数量增大时,比如RDD的分区是100,设置成1000,如果shuffle为false,并不会起作用。这时候就需要设置shuffle为true了,那么RDD将在shuffle之后返回一个1000个分区的RDD,数据分区方式默认是采用 hash partitioner。对于repartition:无论分区数是增加还是减少都会执行shuffle操作。

foreach与foreachPartition

  foreach是对RDD中的每一个元素执行指定的函数,foreachPartition是对RDD中的每一个Partition执行指定的函数。

/**
* Applies a function f to all elements of  this RDD.
*/
def foreach(f: T => Unit): Unit =  withScope {
  val cleanF = sc.clean(f)
  sc.runJob(this, (iter: Iterator[T]) =>  iter.foreach(cleanF))
}
/**
* Applies a function f to each partition of  this RDD.
*/
def foreachPartition(f: Iterator[T]  => Unit): Unit = withScope {
  val cleanF = sc.clean(f)
  sc.runJob(this, (iter: Iterator[T]) =>  cleanF(iter))
}

  可以看到方法通过clean操作(清理闭包,为序列化和网络传输做准备)进行了一层匿名函数的封装。针对foreach方法,是我们的方法被传入了迭代器的foreach(每个元素遍历执行一次函数),而对于foreachpartiton方法是迭代器被传入了我们的方法(每个分区执行一次函数。
  假如我们的Function中有数据库,网络TCP等IO链接,文件流等等的创建关闭操作,采用foreachPatition方法,针对每个分区集合进行计算,更能提高我们的性能。

union与union all

  union属于窄依赖,无shuffle。union all不去重,union会去重。union进行默认规则的排序,union all不进行排序。

collect

Spark的collect收集的数据在Driver JVM内存中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值