9. Spark RDD

RDD的全称是Resilient Dirstributed DataSets,叫做弹性分布式数据集。

首先,需要明确的一点,它是一个 抽象 数据集,言下之意是,它并不是一个包含数据本体的变量或者集合,而是一个仅保存数据描述的抽象集合,这个集合中存储了该数据的一些信息(数据位置、分区等)。

其次,他是一个 分布式 的,也就是,这些数据可能存储在集群上的任何地方,并不是在本地。

再者,他是 弹性 的,言下之意是,它既可以保存在内存中,也可能会因为内存空间不足而转而存储到磁盘上的。

RDD解决的第一个问题就是数据共享的问题,MapReduce的数据共享是基于磁盘的,但是Spark的数据共享是基于内存的,RDD的就是基于内存的。其次,由于RDD是分布式的,所以我们在基于RDD进行计算时,执行的并行计算。RDD中会有 分区 的概念。存储在不同地区的数据,所在的不同区域称之为分区,不同的分区会共同进行计算,即,并行计算。

最后,RDD是 不可变 的。可以理解为RDD是一个常量,一旦RDD计算生成,其中的数据就不可改变的。我们只能基于该RDD再次计算,生成新的RDD,而不能直接修改原RDD。

一、Spark中的操作

Spark Core中的操作基本都是基于Spark RDD来进行的。大体上来看,这些操作可以分为四大类。

  • 创建操作

    他是用于RDD创建的操作,RDD的创建只有两种方式:一种是来自于内部集合和外部存储系统,另一种是通过转换操作生成的RDD。这里的 创建操作 指的是第一种。

    常用创建算子举例 注意,这里给出的所有API都是Scala API。 :

    parallelize[T](seq: Seq[T], numSlices: Int = defaultParallelism): RDD[T]
    // seq表示一个集合,numSlices表示分区的数目,你可以理解为它存储在多少机器上
    // 当Spark基于RDD进行计算的时候,分区数目其实就决定了他的并行度
    
    textFile(path: String, minPartitions: Int = defaultMinPartitions): RDD[String]
    // spark会从该路径path中读取数据,默认按照系统设置的最小分区defaultMinPartition进行分区
    // 得到的RDD的类型是String字符串类型
    
  • 转换操作

    他是指将RDD通过一定的操作转换为新的RDD的操作。

    常用RDD算子举例

    map[U](f:(T) => U): RDD[U]
    // 对RDD中数据集合应用函数f
    
    distinct(): RDD[(T)]
    // 去重
    
    flatMap[U](f:(T) => TraversableOnce[U]): RDD[U]
    // 对RDD中的数据集合应用函数f
    

    Notice!!! map和flatMap的区别

在这里插入图片描述

可以这么认为:

map保有了集合原有的结构,所以我们看到结果是一个Array内部含有多个Array

flatMap则会破坏原有的集合结构,相当于把集合先拍扁,再应用函数f

  • 控制操作

    进行RDD持久化的的操作,可以让RDD按照不同的存储策略保存在磁盘中或者内存中。

    常用算子举例

    cache(): RDD[T]
    // 将数据缓存
    
    persist(): RDD[T]
    // 数据持久化
    
  • 行动操作

    能够触发RDD运行的操作。

    常用算子举例

    fist()
    // 返回RDD中的第一个元素
    
    collect()
    // 将RDD转化为数组
    

二、Spark计算

Spark中引入了 惰性计算 的概念,它是指,程序在执行过程,并不是真正的进行了计算,而是使用某种方式记录了计算的线路,等到合适的时候再触发真正的计算。

Spark中,行动操作的算子是触发计算的标志 。每次行动操作触发的计算称为一个 Job

Spark采用 世系图(lineage) 的方式记录计算的线路。也就是说,在遇到行动操作前,Spark仅仅是记录当前所做的操作,并不会进行真正的计算,遇到行动操作后,才会触发计算,得到结果。

既然Spark的计算可以用世系图来记录,那我们当然可以将他们画出来。我们以上文中的WordCount为例。首先,我们先明确几个需要注意的点:

  • 我们前面提过,Spark中的RDD是不可变,只能有创建操作生成,或者从前几个RDD通过转化操作生成。这就意味着Spark的RDD具有依赖关系,若A RDD通过转换操作变成了B RDD,那么A称为B的父RDD,子RDD同理。
  • 我们前面也说过,RDD中有分区的概念。分区决定了计算时的并行度,也就是说,一个RDD中,有多个分区,每个分区中的数据在计算时是单独计算的,所以,其实 一个分区就是对应Spark中的一个 Task 。这样,我们就介绍了Spark中两个重要的概念 Job 和 Task了。但是我们介绍Task只是个引子,目的是为了引入Shuffle。还记得MapReduce中的Shuffle,数据从Map端到Reduce端,会经历Partitioner函数的划分,将一个Map节点上的数据划分到多个Reduce节点上。Spark中也有Shuffle,一个分区中的数据,自然也有可能会被发送到子RDD的多个分区中。 这里就引入了一个概念,如果父RDD到子RDD之间的依赖不是Shuflle,也就是说不存在父RDD的一个分区被子RDD的多个分区依赖,就称为窄依赖,否则就是宽依赖 。但是需要注意到是,并不是所有的转换算子都是Shuffle算子,只有类似于 join 这种会将一个分区中的数据发送到子RDD的多个分区中的算子才是Shuffle算子。

这样,我们可以画一个世系图了。我们先看WordCount的代码:

import org.apache.spark.{SparkConf, SparkContext}

object WordCount {

  def main(args: Array[String]): Unit = {
	//创建Spark的配置信息,setMaster是设置CPU个数,local[*]意味采用本地运行模式,调用本地尽可能多的CPU
    //setAppName为设置Application的名字
    val conf = new SparkConf().setMaster("local[*]").setAppName("WordCount") 
    //创建Spark的执行环境
    val sc = new SparkContext(conf)
	//读取对应路径的数据,生成RDD,是创建操作
    val wordpair = sc.textFile("D:\\代码\\java\\Apache-Spark\\data.txt") 
    val results = wordpair.flatMap(_.split(" ")) //转换操作,对数据做拆分
      .map((_,1)) //转换操作,将单个字符串映射为键值对
      .reduceByKey(_+_) //转换操作,按照Key对键值对进行加和

    results.foreach(println) //打印
  }
}

有了注释,我们应该可以看懂代码了。然后我们启动spark-shell,在shell中运行代码。在shell运行代码就不必创建Spark的执行环境了。直接贴操作部分的代码就可以。

在这里插入图片描述

这个时候,我们可以在4040端口看到世系图。

在这里插入图片描述

世系图可以形象地看到算子的变换过程。但是,其实世系图中还有一个概念,stage ,也叫task set ,他通过宽依赖进行划分,若我们按照宽依赖将世系图切分开,剩下的一个个就是Stage。

由图中可以看出,wordcount的执行分为两个 stagestage 0 内部的算子都是窄依赖的,stage 1 内只有一个算子,reduceByKey是一个Shuffle算子,因为他会统计一个RDD中所有分区的内容,假设一个该RDD有3个分区,每个分区中都有一个key为A的数据,那么reduceByKey在计算时,就会统计这三个分区中的key为A的数据,并存储到一个新的RDD的一个分区中。这个过程其实就是Shuffle过程。

综上,我们就聊完了Spark中最基础的三个概念,Job, task, stage 。简单来说,Job和Stage都是针对RDD的执行流程而言的,Task则是具体到RDD的每个分区。

那么,我们可以提一个问题?Spark中的分区和MapReduce中的分区(partition)有什么异同吗

  • MapReduce中分区是指Partitioner组件依据哈希函数,将键值对发送到相应的reduce节点上。
  • Spark的分区涉及的是任务的并行度,对应的是task这个概念。
如果你也对大数据感兴趣,欢迎扫码关注公众号 “大数据面试学习指北”

在这里插入图片描述

<think>好的,我现在要解决用户在Spark中遇到的RDD类型不匹配错误,尤其是在执行join操作时出现的“found: RDD[String] required: RDD[(String, ?)]”问题。首先,我需要理解这个错误的根本原因。根据用户提供的错误信息,错误发生在join操作时,Spark期望一个键值对RDD,也就是RDD[(String, ?)],但实际传入的却是一个只包含String的RDD,即RDD[String]。这说明用户在准备进行join的两个RDD中,至少有一个没有被正确转换为键值对形式。 接下来,我需要回顾Spark中join操作的要求。在Spark中,join操作只能应用于两个键值对RDD(即PairRDD),也就是说,每个RDD的元素都应该是(key, value)的元组。如果其中一个RDD的元素不是元组形式,而是单个值(比如String),那么尝试进行join时就会抛出类型不匹配的错误。 用户提供的引用[1]中提到了类似的类型不匹配问题,其中RDD的类型是(Array[Object], Array[Object]),但需要的是(Array[Int], Array[Int])。这说明类型转换的重要性。同样地,用户的问题涉及到RDD[String]需要转换为RDD[(String, ?)],这里的“?”代表可以是任何类型的值部分,但必须有键的存在。 接下来,我需要考虑如何将RDD[String]转换为键值对形式。根据引用[2],用户尝试将DataFrame转换为RDD[(String, String)],使用了map函数将行转换为元组。这说明使用map操作来转换元素类型是常见的方法。因此,对于当前的RDD[String],用户需要确定如何将每个String元素映射成一个键值对。可能的方案包括:将整个字符串作为键,值设为某个默认值(如null或1),或者从字符串中提取出键和值。 例如,假设用户的RDD中的每个字符串是以特定分隔符分隔的键值对,如“key,value”,那么可以通过split操作分割字符串,生成(key, value)的元组。如果字符串本身就是键,而值不需要,那么可以将其转换为(key, null)的形式。这样,RDD[String]就被转换成了RDD[(String, Null)],满足join操作的要求。 此外,用户可能在生成RDD时没有正确进行转换。例如,如果数据来源是文本文件,读取后的RDD默认是RDD[String],每个元素是一行文本。如果需要进行join操作,必须确保后续的处理步骤中将每一行转换为键值对。 另一个需要注意的点是类型的一致性。即使在转换后,键的类型必须与另一个RDD中的键类型一致,否则join仍然会失败。例如,如果一个RDD的键是String类型,另一个是Int类型,那么类型不匹配的问题依然存在。因此,用户需要确保两个RDD的键类型相同。 此外,用户提到的引用[3]中处理了类型不匹配的问题,通过将Column类型转换为Double,这提醒我们在Spark中类型转换的重要性。类似地,在RDD操作中,需要确保每一步的类型转换正确无误。 总结解决步骤应该是:首先检查两个参与join的RDD是否都是键值对形式;如果不是,找到需要转换的那个RDD,使用map函数将其元素转换为元组;确保转换后的键类型与另一个RDD的键类型一致;最后重新运行join操作。 可能的错误原因包括:错误地读取数据导致RDD元素不是元组,或者在处理过程中意外地将键值对RDD转换为了单值RDD。例如,使用map操作时可能误删了值部分,导致RDD退化成了单值类型。 为了验证这个思路,可以编写示例代码。假设有一个RDDRDD[String],每个字符串是键,比如rdd1 = sc.parallelize(["a", "b", "c"])。此时若直接尝试rdd1.join(rdd2),就会报错,因为rdd1不是键值对RDD。正确的做法是将rdd1转换为键值对,例如:rdd1_kv = rdd1.map(lambda x: (x, None)),这样rdd1_kv的类型就是RDD[(String, None)],就可以参与join了。 此外,用户可能需要检查是否在某个操作中错误地使用了函数,例如在map操作中返回了单个值而不是元组。例如,如果原本应该生成(key, value),但错误地只返回了key,就会导致RDD类型不正确。 最后,建议用户在转换后使用print或take方法检查RDD的元素是否符合预期,确保每个元素都是元组形式,从而避免类型不匹配的问题。</think>要解决SparkRDD类型不匹配的错误(`found: RDD[String] required: RDD[(String, ?)]`),需确保参与`join`操作的RDD均为键值对格式。以下是分步解决方案: --- ### **1. 错误原因分析** `join`操作要求两个RDD均为键值对类型(即`RDD[(K, V)]`)。若其中一个RDD为`RDD[String]`(单值类型),Spark会因无法识别键值关系而报错[^1]。 --- ### **2. 解决方案** #### **步骤一:将单值RDD转换为键值对** 使用`map`操作将`RDD[String]`转换为`RDD[(String, ?)]`,其中`?`可为任意类型(如`null`、数值或嵌套结构)。 **示例代码:** ```scala // 原始单值RDD(类型为RDD[String]) val rddString: RDD[String] = sc.textFile("data.txt") // 转换为键值对RDD(假设字符串本身为键,值为占位符) val rddKeyValue: RDD[(String, Int)] = rddString.map(str => (str, 1)) // 值为1 // 或使用占位符null val rddKeyValueAlt: RDD[(String, Null)] = rddString.map(str => (str, null)) ``` #### **步骤二:验证RDD类型** 使用`printSchema`或`take`检查转换后的RDD元素是否为元组: ```scala rddKeyValue.take(3).foreach(println) // 输出示例:("a", 1), ("b", 1), ("c", 1) ``` #### **步骤三:执行join操作** 确保两个RDD均为键值对后执行`join`: ```scala val rdd1: RDD[(String, Int)] = ... // 第一个键值对RDD val rdd2: RDD[(String, String)] = ... // 第二个键值对RDD val joinedRDD = rdd1.join(rdd2) // 正常执行 ``` --- ### **3. 常见场景与优化** - **场景1:从文本生成键值对** 若文本行包含键值分隔符(如逗号),使用`split`提取键值: ```scala val rddFromText = sc.textFile("data.csv").map(line => { val parts = line.split(",") (parts(0), parts(1)) // 假设第一列为键,第二列为值 }) ``` - **场景2:修复DataFrame转换错误** 若从DataFrame转换RDD时丢失键值结构,需显式映射列到元组(参考引用[2]): ```scala val df: DataFrame = ... // 原始DataFrame val rddKV = df.map(r => (r.getString(0), r.getString(1))).rdd // 转为RDD[(String, String)] ``` --- ### **4. 注意事项** - **类型一致性**:确保两个RDD的键类型相同(例如均为`String`或`Int`)。 - **避免值丢失**:检查`map`操作是否意外过滤了值(如错误使用`map(_._1)`)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值