(二)Spark学习笔记之RDD

RDD(Resilient Distributed Dataset,弹性分布式数据集)

RDD 是 Resilient Distributed Dataset — 弹性分布式数据集的简称。它是 Spark 的基本数据结构。它是一个不可变的对象集合,在集群的不同节点行进行计算。

  • Resilient:即在 RDD lineage 的帮助下具有容错能力,能够重新计算由于节点故障而丢失或损坏的数据分区;
  • Distributed: 数据分布在多个节点上。
  • Dataset:数据集。数据可以通过外部加载收集,数据类型可以是JSON、CSV,文本或数据库等。

特点

  1. 内存计算:计算的中间结果存储在内存中而不是磁盘;
  2. 延迟计算:所有的 Transformation 都是惰性操作,它们不会立即计算结果,但是它们记住数据集的Transformation 操作,直到 action 的出现,才开始真正的计算;
  3. 容错性:在故障时自动重建丢失的数据;
  4. 不可变性:数据在计算过程中是不可变的,跨进程共享数据是安全的;
  5. 分区性:分区 partition 是 Spark RDD 并行性的基本单元,每个分区都是数据的逻辑分区。在进行 RDD 操作时,实际上是对每个分区的数据进行操作。
  6. 持久化:可以指定 RDD 的存储策略,来提高性能;
  7. 数据本地性:数据不动代码动。降低数据的流动(磁盘,网络的限制),提高性能。

操作类型

  • Transformation:lazy,不会立即计算;
  • action:触发计算。

Transformation 操作

Spark RDD transformation 操作是一个从现有的 RDD 生成新 RDD 的函数。如 map(),filter(),reduceByKey() 等。

  • Transformation 操作都是延迟计算的操作;
  • 有两种类型 Transformation:窄依赖、宽依赖。

窄依赖

诸如 map、filter 这样的操作,其数据来自于上一个单独的分区。即输出RDD分区中的数据,来自于父RDD中的单个分区。-- 输入、输出,分区数不变,两者的分区是一一对应的。
窄依赖有:map,flatmap,mapPartition,filter,sample、union 等。
在这里插入图片描述

宽依赖

在子 RDD 单个分区中计算结果所需的数据可能存在于父 RDD 的多个分区中,因此宽依赖也被成为 shuffle translation。
宽依赖有:intersection、distinct、reduceByKey、GroupByKey、join、cartesian、repartition、coalesce 等。
在这里插入图片描述

Tips:窄依赖是父 RDD 的一个分区最多仅能在子 RDD 一个分区;宽依赖是子 RDD 的一个分区依赖于多个父 RDD 的分区(或:父 RDD 的每个分区都可能被多个子 RDD 分区所使用),所以宽依赖会造成 shuffle。所以窄依赖是父、子 RDD 分区是一对一的关系,而宽依赖是父、子 RDD 的分区是多对多的关系。

借图: https://blog.csdn.net/tswisdom/article/details/79882308
在这里插入图片描述

action 操作

Spark 中的 action 操作,返回 RDD 计算的最终结果,其结果是一个值,而不是一个 RDD。action 触发血缘关系中 RDD 上的 transformation 操作的真正计算,计算结果返回 Driver 或被写入数据库。
这种设计使 Spark 运行更加高效。

常见的 action:first、take、reduce、collect、count 等。

创建 RDD

创建 RDD 有三种方式:

  • 使用集合创建 RDD;
  • 使用已有 RDD 创建 RDD;
  • 从外部数据源创建 RDD。

在新学习 Spark 时,先使用集合来创建 RDD。即在 Driver 程序中创建集合并将其传递给SparkContext 的 parallelize() 方法,这种方法很少使用在正式环境中。

  1. 实例化 SparkContext 对象
--scala
val conf = new SparkConf().setAppName(appName).setMaster(master)
val sc=new SparkContext(conf)

--java
SparkConf conf = new SparkConf().setAppName(appName).setMaster(master);
JavaSparkContext sc = new JavaSparkContext(conf);

appName:是用来显示在 Spark 集群的 WebUI 上的 Tag 标签,用来区分项目。
master:是一个 spark、yarn、mesos 集群的 url,在实际项目中,集群运行时不会对 master 进行硬编码,而是用 spark-submit 启动应用程序,并传递 master 给应用程序。但是在进行本地测试或单元测试时,可以使用 local 字符串来运行 spark。

Spark 的提交方式:

./bin/spark-submit \
--class <main-class>
--master <master-url>
--deploy-mode <deploy-mode>
--conf <key>=<value>
...   # other options
<application-jar> \
[application-arguments]

eg:./bin/spark-sumit  --class packageName+className  --deploy-mode cluster/client

使用集合创建RDD

--scala
val data = Array(1, 2, 3, 4, 5)
val distData = sc.parallelize(data)

--java
List<Integer> data = Arrays.asList(1, 2, 3, 4, 5);
JavaRDD<Integer> distData = sc.parallelize(data);

从外部数据源创建 RDD

Spark 可以从 Hadoop 支持的任意存储源创建分布式数据集,包括本地文件系统,HDFS,Cassandra,HBase,Amazon S3 等,Spark 支持文本文件,SequenceFiles 和任何其它 Hadoop InputFormat。

读取本地文件

对于文本文件,RDD 可以使用 SparkContext 的 textFile 方法来创建数据集,此方法可接受文件的 URL。如本地路径或 hdfs://,s3a:// 等 URL。创建数据集,并将其作为行集合读取。

--scals
val distFile = sc.textFile("data.txt")
Val textFile = sc.textFile("filePath")

创建之后,就可以对 RDD 进行相应的操作了。。

distFile.map(s => s.length).reduce((a, b) => a + b);

读取文件的注意事项

  1. 如果使用本地文件系统上的路径,则必须在 worker 节点上的同一路径上,此文件可以被访问。那么就需要保证该文件在所有 worker 节点上,或被挂载在一个所有 worker 可以访问的共享文件系统上。
  2. spark 所有基于文件的输入方式(textFile),支持在目录,压缩文件和通配符等。
textFile("/my/directory") 
textFile("/my/directory/*.txt")
textFile("/my/directory/*.gz").
  1. textFile 方法还接收一个可选的第二个参数,用于控制文件的分区数量。默认情况下,spark 为文件的每个块创建一个分区,但是也可以通过自己设置该值来传递一个更大的值来要求更高的分区数量。在 HDFS 中默认块的大小为 128MB,在设置分区数时,必须保证分区数值大于块数值。

除了文本文件外,spark 的 scala API 还支持其他几种数据格式:

  1. SparkContext.wholeTextFiles 允许读取包括多个小文本文件的目录,并将它们作为(filename,content)的键值对返回。这与 textFile 不同,textFile 将在每个文件中每行返回一条记录。分区有数据本地性决定。在某些情况下,数据本地性可能导致分区太少。对于这些情况,wholeTextFile 提供了最小分区书的第二个可选参数。
  2. 对于 SequenceFiles,使用 SparkContext 的 sequenceFile<K,V> 方法,其中 K 和 V 是文件中的键和值的类型。这些应该是 Hadoop Writable 接口的子类,比如:IntWritable 和 Text。
  3. 对于其它 Hadoop InputFormat,可以使用 SparkContext.HadoopRDD 方法,它可以接受任意的jobConf 和输入格式类、键类和值类。将这些设置为与使用输入源 Hadoop 作业相同的方式,还可以通过使用 SparkContext。基于 new MapReduce API (org.apache.hadoop.mapreduce) 的 inputformat 的 newAPIHadoopRDD。

读取 HDFS 上的数据

启动 HDFS -> 读取 hdfs 上的数据
这里和读取本地数据一样,一定要保证所有 worker 节点都能访问到 HDFS 上的文件。

val textFileRDD=sc.textFile("hdfs://bigdata01:9000/testdata/order.txt")
val count=textFileRDD.count()
println("count:"+count)

提交应用程序到 Spark 集群

  1. 打包应用程序, maven --> install --> run spark

  2. 上传 jar 包到服务器

 ./spark-submit --class sparkcore.learnTextFile --deploy-mode client /opt/sparkapp/learnTextFile.jar

spark 算子

spark API 严重依赖有将 Driver 程序中的函数传递到集群上运行。

scala

在 scala 语言环境下,可以采用两种方式实现函数的传递:

  1. 匿名函数语法,可用于短代码段(lambda表达式)
  2. 全局单例对象中的静态方法。
object MyFunctions {
  def func1(s: String): String = { ... }
}

myRdd.map(MyFunctions.func1)

java

在 java 中,函数由实现 org.apache.spark.api.java.function 包中的接口的类表示。有两种方式可以创建函数:

  1. 匿名内部类
JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(new Function<String, Integer>() {
  public Integer call(String s) { return s.length(); }
});
int totalLength = lineLengths.reduce(new Function2<Integer, Integer, Integer>() {
  public Integer call(Integer a, Integer b) { return a + b; }
});
  1. 创建类实现相应接口,并将其实例传递给 Spark。
class GetLength implements Function<String, Integer> {
  public Integer call(String s) { return s.length(); }
}
class Sum implements Function2<Integer, Integer, Integer> {
  public Integer call(Integer a, Integer b) { return a + b; }
}
JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(new GetLength());
int totalLength = lineLengths.reduce(new Sum());

在Java8,可以使用 Lambda 表达式来处理。

Transformation

map和flatmap算子

map 算子: 将传入的函数应用于 RDD 中的每一条记录,返回有函数结果组成的新 RDD。函数的结果值是一个对象而不是一个集合。

----map操作 scala版本
val textFileRDD=sc.textFile("in/README.md")
val uppercaseRDD=textFileRDD.map(line => line.toUpperCase)
for (elem <- uppercaseRDD.take(3)) {
    println(elem)
}

flatmap 操作:与 map 操作类似,但是传入 flatmap 的函数可以返回0个,1个或多个结果值。即函数结果值是一个集合而不是一个对象。

----flatmap操作 scala版本
val flatMapRDD=textFileRDD.flatMap(line =>line.split(" "))
flatMapRDD.take(3).foreach(word => println(word))

注:map:针对每一条输入进行操作,并为每一条输入返回一个对象。(一行输入就返回一个对象);flatmap:针对每一条输入进行操作,并最终针对所有输入返回一个对象(所有输入返回一个对象)。它们都是属于窄依赖。

从 map 和 flatmap 的比较可以看出,map 函数时一对一的转换,它将集合的每个数据元素转换成结果集合的一个数据元素;而 flatmap 则是一对多的转换,将每个元素转换为0或更多的元素。

filter算子

filter 函数返回一个新的 RDD,只包含满足过滤条件的元素。这个也是一个窄依赖的操作,不会将数据从一个分区转移到其他分区。即不会发生 shuffle 操作。

    //过滤出包含单词"spark"的行,然后打印出来
    val filterRDD=textFileRDD.filter(line =>line.contains("spark"))
    println("count:"+filterRDD.count())

distinct 算子

返回 RDD 中非重复记录。它会发生数据从一个分区转移到另外其他分区的情况。即会发生 shuffle 操作。

在这里插入代码片

mapPartition

在 mapPartition 函数中,map 函数同时应用于每个 partition 分区。即 mapPartition 会针对分区进行操作。同时对比 foreachPartition 操作,该操作是一个 action 算子,其也是针对每个分区进行 foreach 操作,能提高性能。

    //map每一个分区,然后再map分区中的每一个元素
    val mapPartitionRDD=textFileRDD.mapPartitions(partition => {
        partition.map(line =>line.toUpperCase)
    })

mapPartitionWithIndex

这个操作是在 mapPartition 的基础上,还为传入的函数提供一个整数值,表示分区的索引,map 在分区上依次应用。

val mapPartitionsWithIndexRDD=textFileRDD.mapPartitionsWithIndex((index,partition) => {
      partition.map(line =>index +" : "+line.toUpperCase)
    })

union 并集

使用 union 函数,可以在新的 RDD 中获得两个 RDD 元素。这个函数的关键规则是两个 RDDs 应该属于同一类型。

val rdd1 = sc.parallelize(Seq((1,"jan",2016),(3,"nov",2014),(16,"feb",2014)))
val rdd2 = sc.parallelize(Seq((5,"dec",2014),(17,"sep",2015)))
val rdd3 = sc.parallelize(Seq((6,"dec",2011),(16,"may",2015)))

val rddUnion = rdd1.union(rdd2).union(rdd3)

intersection交集

使用 intersection 函数,在新的 RDD 中只能得到两个 RDD 的公共元素。这个函数的关键规则是这两个 RDDs 是同一类型的。

val rdd1 = sc.parallelize(Seq((1,"jan",2016),(3,"nov",2014, (16,"feb",2014)))
val rdd2 = sc.parallelize(Seq((5,"dec",2014),(1,"jan",2016)))
val comman = rdd1.intersection(rdd2)

PairRDD

这种数据集的典型模式是每一行都是一个 key 映射到一个或多个 value。因此,spark 提供了一个名为 PairRDD 的数据结构,而不是常规的 RDD。这使得处理此数据更加简单和高效。
PairRDDs 是一个特殊类型的 RDD,可以存储键 - 值对。

创建 PairRDD 的步骤:

  1. 通过键值数据结构列表构建 PairRDD。键值数据结构成为 tuple2 元组。在 Java 语言中,由于没有Tuple类型,所以使用 scala.Tuple2 类创建元组。
--- scala
val tuple = List(("zhangsan","spark"), ("lisi","kafka"), ("Mary","hadoop"), ("James","hive"))
val pairRDD = sc.parallelize(tuple)
pairRDD.foreach(t =>println(t._1+","+t._2))

--- java
List<Tuple2<String, Integer>> tuple = Arrays.asList(new Tuple2<>("Lily", 23),
                                                        new Tuple2<>("Jack", 29),
                                                        new Tuple2<>("Mary", 29),
                                                        new Tuple2<>("James",8));
JavaPairRDD<String, Integer> pairRDD = sc.parallelizePairs(tuple);
  1. 将一个常规的RDD转换成PairRDD。
--- scala
val inputStrings = List("Lily 23", "Jack 29", "Mary 29", "James 8")
val regularRDDs = sc.parallelize(inputStrings)
val pairRDD = regularRDDs.map(s => (s.split(" ")(0), s.split(" ")(1)))

--- java
List<String> inputStrings = Arrays.asList("Lily 23", "Jack 29", "Mary 29", "James 8");
JavaRDD<String> regularRDDs = sc.parallelize(inputStrings);
JavaPairRDD<String, Integer> pairRDD = regularRDDs.mapToPair(getPairFunction());
       
private static PairFunction<String, String, Integer> getPairFunction() {
    return s -> new Tuple2<>(s.split(" ")[0], Integer.valueOf(s.split(" ")[1]));
}
PairRDD 上的 Transformation 操作

PairRDDs 允许使用常规 RDDs 可用的所有 transformation,支持与常规 RDDs 相同的功能。
由于 PairRDDs 包含元组,所以我们需要传递操作元组而不是单个元素的函数给 Spark。

filter
案例: 过滤value

val tuple = List(("zhangsan","spark"), ("lisi","kafka"), ("Mary","hadoop"), ("James","hive"))
val pairRDD = sc.parallelize(tuple)
val filterRDD=pairRDD.filter(t =>t._2.equals("spark"))
filterRDD.foreach(t =>println(t._1+","+t._2))

reduceByKey
案例:wordcount

val lines = sc.textFile("in/word_count.text")
val wordRdd = lines.flatMap(line => line.split(" "))
val wordPairRdd = wordRdd.map(word => (word, 1))
val wordCounts = wordPairRdd.reduceByKey((x, y) => x + y)
for ((word, count) <- wordCounts.collect()) println(word + " : " + count)

combineByKey
案例:计算平均分数

    val scores=List(
      ScoreDetail("A","Math",98),
      ScoreDetail("A","English",88),
      ScoreDetail("B","Math",75),
      ScoreDetail("B","English",78),
      ScoreDetail("C","Math",90),
      ScoreDetail("C","English",80),
      ScoreDetail("D","Math",91),
      ScoreDetail("D","English",80)
    )

    val scoresWithKey=for(i <- scores)yield (i.studentName,i)
    val scoreWithKeyRDD=sc.parallelize(scoresWithKey).partitionBy(new HashPartitioner(3)).cache()
    scoreWithKeyRDD.foreachPartition(partition =>println(partition.length))
    scoreWithKeyRDD.foreachPartition(partition =>{
      partition.foreach(item => println(item._1+":"+item._2))
      println("**********************")
    })
    
    val avgScoresRDD=scoreWithKeyRDD.combineByKey(
      (x:ScoreDetail) => (x.score,1),
      (acc:(Float,Int),x:ScoreDetail) => (acc._1+x.score,acc._2+1),
      (acc1:(Float,Int),acc2:(Float,Int)) => (acc1._1+acc2._1,acc1._2+acc2._2)
    ).map({
      case (key,value) =>(key,value._1/value._2)
    })
    avgScoresRDD.collect().foreach(println)
  }

sortByKey
案例:针对键值排序

val data =sc.parallelize(Seq(("maths",52), ("english",75), ("science",82), ("computer",65), ("maths",85)))
val sorted = data.sortByKey()
sorted.foreach(println)

join
根据 key 值来自组合两个不同的 RDD。

val data = sc.parallelize(Array(('A',1),('b',2)))
val data2 =sc.parallelize(Array(('A',4),('A',6),('b',7),('c',3),('c',8)))
val result = data.join(data2)
//输出:(A,(1,4)),(A,(1,6)),(b,(2,7))
println(result.collect().mkString(","))
/**
  * 输出:
  * A:(1,4)
  * A:(1,6)
  * b:(2,7)
  */

action算子

count

count 返回 RDD 中的数据数量。

在这里插入代码片

take

从 RDD 返回 n 个元素。它试图减少访问的分区数量,但不能使用此方法来控制访问元素的顺序。

在这里插入代码片

top

如果 RDD 中的元素有序,那么可以使用 top 从 RDD 中提取前几个元素

//map()操作将用它的长度映射每一行。top(3)将从mapFile返回3条默认排序的记录
val data =sc.textFile("spark_test.txt").rdd
val mapFile = data.map(line => (line,line.length))
val res = mapFile.top(3)
res.foreach(println)

countByValue

countByValue 返回,每个元素出现在 RDD 中的次数。

    val sc =new SparkContext(conf)
    val textFileRDD=sc.textFile("in/README.md")
    val flatMapRDD=textFileRDD.flatMap(line =>line.split(" "))
    val countValue=flatMapRDD.countByValue()
    countValue.foreach(t => println(t._1+" : "+t._2))
  }

reduce

reduce 函数将 RDD 中的两个元素作为输入,然后生成与输入元素相同类型的输出。

val rdd1 = sc.parallelize(List(20,32,45,62,8,5))
val sum = rdd1.reduce(_+_)
println(sum)

collect

collect 函数时将整个 RDDs 内容返回给 Driver 程序的最常见最简单的操作。collect 主要应用在单元测试中,在使用 collect 方法时,期望 Driver 是满足数据大小的,否则会出现内存溢出。

val data = sc.parallelize(Array(('A',1),('b',2),('c',3)))
val data2 =sc.parallelize(Array(('A',4),('A',6),('b',7),('c',3),('c',8)))
val result = data.join(data2)
println(result.collect().mkString(","))

foreach

当希望对 RDD 的每个元素操作时,但它不应该返回给 Driver 程序时。这种情况下使用 foreach 函数是非常适合的。

在这里插入代码片

foreachPartition

类似于 mapPartition,针对每个分区进行 foreach 操作。但是 foreachPartition 是 action 操作,同时 foreachPartition 没有返回值。

在这里插入代码片

保存数据

  1. 保存数据到 HDFS
在这里插入代码片
  1. 保存数据到 mysql 数据库
在这里插入代码片
  1. 保存数据到 kafka
在这里插入代码片

猜你喜欢

转载自blog.csdn.net/dec_sun/article/details/89389771
今日推荐