从装饰者的角度来观察RDD

从装饰者的角度来观察RDD

大家好,我是一拳就能打还是爆A柱的硬核男人

好久没有更新博客了,不是我没有干活,而是我暂时迷茫找不到目标了。这段时间真的恶心,迷茫到爆,每天看点博客,有时看点书,每天好像开开心心的,可是我没动力了。突然就没动力了,不知道为什么。但是还好,经过调整我还是决定尽量每天都写点博客,针对一个问题深挖下去,记录下来分享给大家。也不知道我这个辣鸡博主有没有人看。最近也是有点其他的收获的,比如我发现我之前对RDD的理解还是有偏差,所以今天我决定去翻RDD的源码希望能给大家带来点新东西。

1、设计模式 - 装饰者

在了解RDD之前,我们需要去看装饰者的定义,因为RDD用到了这个设计模式,根据百度百科的解释:

装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

所以说装饰者模式可以通过继承的方式扩展对象的功能,就像在外面包了一层一样。

在博客《装饰者模式》中,记录了装饰者模式的示例,我摘取一部分:

装饰者模式图1

在这个继承树中,右方的AttachedPropertiesDecorator作为抽象装饰者,但是其实现类可以在这个基础上拓展出addCar、addHouse、addDeposit、addQuality方法。通过这一系列的装饰者可以对Man拓展出许多功能,而Man类作为成员对象出现在装饰者家族中,完全是被动的、不知情的,也就可以被动的赋予许多功能。大家感兴趣可以去看一看这篇博客。

在《尚硅谷2021迎新版大数据Spark从入门到精通P25》中关于RDD的描述也通过装饰者的角度解读了RDD,其中使用的例子是Java的IO流操作,不同的对象负责一部分的功能,这些功能的叠加最终实现了字符的IO。

按字节输入的读取案例:

FileInputStream in = new FileInputstream("1.png");
BufferedInputStream buffIn = new BufferedIntputStream(in);
buffIn.read

FileInputStream读取字节,读取完字节后将数据放入BufferedIntputStream的Buffer中。这样做可以减少交互次数,从而提高读取效率。

按字符输入的读取案例:

Reader in = new BufferedReader(
    new InputStreamReader(new FileInputStream("path"), "UTF-8")
);

如下图:

在这里插入图片描述

FileInputStream负责读取字节,InputStreamReader将字节在缓冲区中转义,最后送到BufferedReader缓冲区。

2、 从wordCount入手

目前了解了装饰者模式,对它有个大概的印象,接下来我们从wordCount案例来看RDD:

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


object wc {

    def main(args: Array[String]): Unit = {

        val conf = new SparkConf().setMaster("local[*]").setAppName("wc")
        val sc = new SparkContext(conf)

        val file: RDD[String] = sc.textFile("1.txt")
        val flat: RDD[String] = file.flatMap(_.split(" "))
        val value: RDD[(String, Int)] = flat.map(word => (word, 1))
        val value1: RDD[(String, Int)] = value.reduceByKey(_ + _)
        value1.collect().foreach(println)
    }
}

第一行

根据第一行去读取文件得到了一个RDD:

val file: RDD[String] = sc.textFile("1.txt")

第二行

所以接下来看下一行flatMap:

val flat: RDD[String] = file.flatMap(_.split(" "))

点击进入flatMap方法:

def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF))
}

在方法内部可以知道,f是具体的计算方法,方法第一行是做环境清理,第二行直接生成MapPartitionsRDD,该构造方法传入的对象有当前的RDD(this),也就是说flatMap生成的MapPartitionsRDD将源RDD作为了它的成员变量(这里不讨论f计算方法如何操作,重点观察RDD的封装)。

flatMap最后返回的还是RDD类型,显然MapPartitionsRDD也是RDD抽象类的子实现类。

第三行

val value: RDD[(String, Int)] = flat.map(word => (word, 1))

map方法传入计算规则,最后生成新的RDD返回,点击看map:

def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}

同样的,做了clean,然后生成了新的MapPartitionsRDD,也就是说现在的关系是:[MapPartitionsRDD[MapPartitionsRDD[RDD]]],MapPartitionsRDD里包了MapPartitionsRDD里包了RDD。

第四行

val value1: RDD[(String, Int)] = value.reduceByKey(_ + _)

点击进入reduceByKey:

def reduceByKey(func: (V, V) => V): RDD[(K, V)] = self.withScope {
    reduceByKey(defaultPartitioner(self), func)
}

进入下一层:

def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
    combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
}

继续下一层:

def combineByKeyWithClassTag[C](
    createCombiner: V => C,
    mergeValue: (C, V) => C,
    mergeCombiners: (C, C) => C,
    partitioner: Partitioner,
    mapSideCombine: Boolean = true,
    serializer: Serializer = null)(implicit ct: ClassTag[C]): RDD[(K, C)] = self.withScope {
   ......
    if (self.partitioner == Some(partitioner)) {
        self.mapPartitions(iter => {
            val context = TaskContext.get()
            new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
        }, preservesPartitioning = true)
    } else {
        new ShuffledRDD[K, V, C](self, partitioner)
        .setSerializer(serializer)
        .setAggregator(aggregator)
        .setMapSideCombine(mapSideCombine)
    }
}

省略其他无关的计算,最后的if-else可以看到要么调用mapPartitions,要么new一个ShuffledRDD,显然mapPartitions也是生成一个MapPartitionsRDD。所以最后reduceByKey又是在原有的基础上包了一层外衣。

现在回头看Java-IO的图是不是很像,我们重新思考一下:

  • Java-IO中FileInputStream负责读取字节,InputStreamReader负责将多字节转成字符,BufferedReader负责将多个字符缓存以减少连接次数提高效率。
  • Spark-wordCount中textFile产生的RDD负责从数据源读取数据,flatMap负责按规则1计算数据,map负责按规则2计算数据,reduceByKey负责按规则3计算数据。

注意:上面包了那么多层还是一个RDD,注意看上面的继承树。

各位想想,RDD的操作是不是都是在规定一些规则,然后一层层的包下去。在最底层的操作做好了之后上一层继续,所以应该是可以将这些操作定义成lazy操作,因为只要数据到位后面对数据的操作都可以顺理成章的进行下去。实时也正是如此的,Spark将RDD的操作分为两大类,一类transformation,一类action。只有当action执行的时候才知道数据需要输出,这也就意味着一个计算阶段的结束,所以前面这一段transformation操作可以连续执行了。

transformation操作属于懒加载操作,只有当action触发的时候才会将前面的一系列操作执行下来。这就好比我们定义一个函数:

def compute(x : Int) : Int = {
    y = x+1
    y2 = y-2
    y3 = y2*3
    y4 = y3*y3
    return y4
}

这一系列的对x的操作,只要主调函数传入x值(action),那么之前定义好的操作(transformation)都会像流水线一样执行下来。

关于如何划分阶段(stage)还需要单独一篇博客才能讲清楚,所以这里还是先不提了。

总结

RDD遵循装饰者模式,而且经过wordcount案例可以知道RDD中的一系列算子就是对数据操作的定义,这只是定义而不是具体执行。只有待action触发后,之前的transformation才会执行,这就好像上面的compute函数定义,action未触发则不会执行。

这篇博客写的太差了,我自己都看不下去,好久没动笔去写东西了,所以质量很差,我接下来每天会坚持写一篇,每天针对一个问题去尽可能深的挖下去,把我的见解都记录下来。

### 回答1: 使用csv的方式读取数据来创建rdd,可以使用Spark的CSV库来实现。具体步骤如下: 1. 导入CSV库 ```python from pyspark.sql import SparkSession from pyspark.sql.functions import * ``` 2. 创建SparkSession ```python spark = SparkSession.builder.appName("CSV Reader").getOrCreate() ``` 3. 读取CSV文件 ```python df = spark.read.format("csv").option("header", "true").load("path/to/csv/file") ``` 其中,`option("header", "true")`表示第一行为表头。 4. 将DataFrame转换为RDD ```python rdd = df.rdd ``` 这样就可以使用CSV文件中的数据创建RDD了。 ### 回答2: 在Spark中,使用CSV的方式读取数据来创建RDD是非常常见的操作。CSV是一种常用的数据格式,它可以被大多数的数据处理和分析工具所支持。本文主要介绍如何使用CSV的方式读取数据来创建RDD。 首先,我们需要导入需要使用的依赖。在这里我们需要导入spark-csv扩展包,以及spark-core和spark-sql。这里的版本可以根据具体的环境需求来确定。 ```scala import org.apache.spark.sql.SQLContext import org.apache.spark.sql.Row import org.apache.spark.sql.types.{StructType, StructField, StringType, IntegerType} import org.apache.spark.SparkContext import org.apache.spark.SparkConf ``` 接着,我们需要创建一个SparkContext和SQLContext对象。SparkContext对象是Spark应用程序中通用的入口点,SQLContext对象用于执行SQL查询。 ```scala val conf = new SparkConf().setAppName("CSV Read").setMaster("local") val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc) ``` 接下来,我们需要定义CSV文件的结构。这里的结构决定了CSV文件中每一列的名称和类型。在这里,假设我们有一个包含姓名和年龄的CSV文件。 ```scala val schema = StructType(Array( StructField("name", StringType, true), StructField("age", IntegerType, true))) ``` 然后,我们就可以使用CSV的方式读取数据创建RDD了。具体的代码如下: ```scala val csvFile = "data.csv" val data = sqlContext.read.format("com.databricks.spark.csv") .option("header", "true") .option("delimiter", ",") .schema(schema) .load(csvFile) val rdd = data.rdd ``` 这里,我们通过read方法从CSV文件中读取数据。在这个过程中,我们可以指定CSV文件的分隔符、是否包含首行标题等信息。同时,我们也需要为CSV文件中的每一列指定结构。 最后,我们将得到的DataFrame对象转换为RDD即可。在这一步中,我们可以执行各种RDD操作,例如map、filter、reduce等等。 使用CSV的方式读取数据来创建RDD是非常方便的。通过合理的定义结构和使用SQLContext,我们可以快速地进行CSV数据的处理和分析。 ### 回答3: 在Spark中,我们可以使用CSV格式来读取数据并创建RDD。CSV文件是一种以逗号分隔的文本文件,其中每一行都包含相同数量的字段,并且每个字段都包含一个值。在读取这种文件时,我们通过指定分隔符、行分隔符和数据类型等信息来定义数据格式。 使用csv的方式读取数据来创建rdd,需要从spark-csv库中引入相应的相关类。目前,这个库已经被Spark集成,并且在Spark 2.0之后,可以直接使用spark.read.csv方法来读取CSV格式的文件,而不需要额外引入该库。 使用spark.read.csv方法,我们需要提供数据文件的路径,以及一些可选的参数。其中,最常用的参数是sep,用于指定分隔符, header参数用于指定CSV文件是否有头部包含列名等。 读取CSV数据的示例代码如下: ``` from pyspark.sql import SparkSession spark=SparkSession.builder.appName('csvreader').getOrCreate() csv_data = spark.read.csv('file:///data.csv',header=True, inferSchema=True) csv_data.show(10) ``` 上述代码使用SparkSession创建Spark应用程序并指定了应用程序的名称。接着,我们使用read.csv方法读取data.csv文件,并通过将header参数设置为True,指定CSV文件包含列名,同时将inferSchema参数设置为True,使Spark自动推断每个字段的数据类型。最后,我们使用show方法打印出数据集的前10行和相关列名。 总之,使用CSV格式来读取数据并创建RDD,是Spark中常见的数据读取方式。使用Spark中的csv读取功能,可帮助我们轻松定义和读取不同格式的CSV文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值