Scala实践Spark(二) pair(键值对操作)

转化

单个pair

  • reduceByKey(func) 合并相同建的value
  • groupByKey() 对相同键的value分组
  • combineByKey(createCombiner,mergeValue,mergeCombiners,partitioner) 合并相同key的value并返回不同的类型
  • aggregate()
  • mapValues(func)
  • flatMapValues(func)
  • keys 没有括号
  • values 没有括号
  • sortByKey()

两个pair

  • subtractByKey
  • join 内连接
  • rightOuterJoin 右外连接
  • leftOuterJoin 左外连接
  • cogroup

聚合

  • reduceByKey()
  • foldByKey()
    下面是几个聚合的例子
scala> val input = sc.parallelize(Array(("panda",0),("pink",3),("private",3),("panda",1),("pink",4)))
input: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at <console>:24

scala> input.mapValues(x=>(x,1)).reduceByKey((x,y)=>(x._1+y._1,x._2+y._2))
res0: org.apache.spark.rdd.RDD[(String, (Int, Int))] = ShuffledRDD[2] at reduceByKey at <console>:26

scala> val lines = sc.textFile("file:///home/hadoop/software/spark/spark-2.4.4-bin-hadoop2.7/README.md")
lines: org.apache.spark.rdd.RDD[String] = file:///home/hadoop/software/spark/spark-2.4.4-bin-hadoop2.7/README.md MapPartitionsRDD[4] at textFile at <console>:24

scala> val words = lines.flatMap(line=>line.split(" "))
words: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[5] at flatMap at <console>:25

scala> val result = words.map(x=>(x,1)).reduceByKey((x,y)=>x+y)
result: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[7] at reduceByKey at <console>:25

scala> result.take(5).foreach(println)
(package,1)                                                                     
(For,3)
(Programs,1)
(processing.,1)
(Because,1)

combineByKey()有多个参数对应聚合操作的各个阶段,可以用来解释聚合操作各个阶段的功能划分。

scala> val input = sc.parallelize(Array(("panda",0),("pink",3),("private",3),("panda",1),("pink",4)))
input: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[0] at parallelize at <console>:24

scala> val result = input.combineByKey(
	(v)=>(v,1), //创建 createCombiner V=>C
	(acc:(Int,Int),v)=>(acc._1+v,acc._2+1), //合并值 mergeValue
	(acc1:(Int,Int),acc2:(Int,Int))=>(acc1._1+acc2._1,acc1._1+acc2._2)) // 合并C
.map{case (key,value)=>(key,value._1/value._2.toFloat)}
result: org.apache.spark.rdd.RDD[(String, Float)] = MapPartitionsRDD[2] at map at <console>:25

scala> result.collectAsMap().map(println(_))
(private,3.0)                                                                   
(pink,3.5)
(panda,0.5)
res0: Iterable[Unit] = ArrayBuffer((), (), ())

Spark核心RDD:combineByKey函数详解

并行度调优:在执行聚合或分组操作时,可以要求Spark使用给定的分区数,Spark会尝试根据集群的大小推断出一个有意义的默认值。

scala> val data = Seq(("a",3),("b",4),("a",1))
data: Seq[(String, Int)] = List((a,3), (b,4), (a,1))

scala> sc.parallelize(data).reduceByKey((x,y)=>x+y)
res2: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[4] at reduceByKey at <console>:27

scala> sc.parallelize(data).reduceByKey((x,y)=>x+y,10)
res3: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[6] at reduceByKey at <console>:27

关于分区

  • rdd.partitions.size获取分区数
  • repartition()创建新的分区集合

分组

  • groupByKey()
  • cogroup() =>[(K,(Iterable[V],Iterable[W]))]

连接

  • join()
  • rightOuterJoin()
  • leftOuterJoin()

排序

  • sortByKey() 支持自定义比较函数
val input:RDD[(Int,Venue)] = ...
implicit val sortIntegerByString = new Ordering[Int]{
	override def compare(a:Int,b:Int) = a.toString.compare(b.toString)
}
rdd.sortByKey()

action

  • countByKey() 对key计数
  • collectAsMap() 将结果以映射表形式返回,现在是不是都为collect()?
  • lookup(key) 返回key对应的value

数据分区

合理的分组
以下是一个简单的应用,假设过去存在一个[UserID,UserInfo]用户订阅表,周期性的会产生一个过去五分钟的事件,是[UserID,LinkInfo]的表。

val sc = new Sparkcontext(...)
val userData = sc.sequenceFile[UserID,UserInfo]("hdfs://...").persist()

def processNewLogs(logFileName:String){
	val events = sc.sequenceFile[UserID,LinkInfo](logFileName)
	val joined = userData.join(events)
	val offTopicVisits = joined.filter{
		case (userId,(userInfo,linkInfo)) => !userInfo.topics.contains(linkInfo.topic)
	}.count()
	println("Number of visits to non-subscribed topics:" + offTopicVisits)
}

上述代码不够高效,在于join()执行,每次都会计算所有键的哈希值传输到同一台机器然后对所有键相同的记录连接操作。(哈希值计算和跨节点数据混洗)
通过partitionBy()将表转化为哈希分区。

val sc = new SparkContext(...)
val userData = sc.sequenceFile[UserID,UserInfo]("hdfs://...").partitionBy(new HashPartitioner(100)).persist()

  • 由于构建userData时调用了partitionBy(),Spark就知道RDD根据键的哈希值来分区的,这样在调用join()时,Spark只会对events混洗操作,将events中特定UserID的记录发送到userdata的对应分区所在的机器上。
  • 事实上,除了join()的很多操作也会利用已有的分区信息,比如sortByKey()和groupByKey()会分别生成范围分区的RDD和哈希分区的RDD;另一方面,map()这样的操作会导致新的RDD失去父RDD的分区信息
  • 如果没有将partitionBy()转化操作的结果持久化,RDD都会重复地对数据进行分区操作。

获取RDD的分区方式

通过partitoner属性获取分区信息

scala> import org.apache.spark
import org.apache.spark

scala> val pairs = sc.parallelize(List((1,1),(2,2),(3,3)))
pairs: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[7] at parallelize at <console>:25

scala> pairs.partitioner
res4: Option[org.apache.spark.Partitioner] = None

scala> val partitioned = pairs.partitionBy(new spark.HashPartitioner(2))
partitioned: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[8] at partitionBy at <console>:26

scala> partitioned.partitioner
res5: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@2)
  • Spark内部知道各操作如何影响分区方式,并将对数据进行分区的操作的结果RDD自动设置为对应的分区器。如join()连接两个RDD,由于键相同的元素会被hash到同一台机器上,Spark直到输出结果也是hash分区的;不过转化操作不一定按已知方式分区,如map会改变键值,此时就不会有固定的分区方式,不过我们可以采用另外两个操作mapValues()和flatMapValues()作为替代方法,可以保证每个二元组的键保持不变。以下是为生成的RDD设定好分区方式的操作:cogroup()、groupWith()、join()、leftOuterJoin()、rightOuterJoin()、groupByKey()、reduceByKey()、combineByKey()、partitionBy()、sort()、mapValues()、flatMapValues()、filter()

示例:PageRank

算法维护两个数据集一个(pageID,linkList)包含每个页面的相邻页面表,一个(pageID,rank)。计算每个页面的PageRank值

  • 1.排序值初始化为1.0
  • 2.每次迭代向其相邻页面发送一个值为rank(p)/numNeighbors(p)
  • 3.将每个页面的排序值设为0.15+0.85*contributionReceived
  • 4.重复循环,算法会逐渐收敛于每个页面的实际PageRank值
val links = sc.objectFile[(String,Seq[String])]("links").partitionBy(new HashPartitioner(100)).persist()
val ranks = links.mapValues(v=>1.0)
for(i <- 0 until 10){
	val contributions = links.join(ranks).flatMap{
		// dest相邻的link  rank/links.size传递的值
		case (pageId,(links,rank))=>links.map(dest=>(dest,rank/links.size))
	}
	ranks = contributions.reduceByKey((x,y)=>x+y).mapValues(v=>0.15+0.85*v)
}
result.saveAsTextFile("ranks")

这里解释一下,根据pageid合并上述两个表=>(pageId,(links,rank)),将links中的每一个项更新为一个pair(dest,rank/links/size),即每一个项的计算贡献值,最后合并贡献值更新rank值。
上述代码看起来简单,还是做了不少事情来保持高效的方式:

  • links是一个静态数据集,所以我们程序一开始进行了分区操作,这样就不需要把它通过网络进行数据混洗,相比于普通MapReduce节省了相当可观的网络通信开销。
  • 我们对links调用了持久化,将它保存在内存中以供每次迭代使用
  • 第一次创建ranks,我们通过mapValues()而不是map()来保留父RDD的分区方式,这样对于它进行的第一次连接操作开销很小
  • 循环体中,我们用reduceByKey()后使用mapValues():因为reduceByKey()的结果已经是哈希分区,下一次循环将映射操作的结果再次与links进行连接操作更加高效。
  • 短短的几行代码简直great!!

注意:为了最大化分区优化的潜在作用,在无需改变键的情况下金亮使用mapValues()或flatMapValues()

自定义分区方式

还是以上面的例子,如果pageid是url,不同的url域名可能相同,相同域名下的网页可能相互链接,因此把相同域名分在同一个分区会更好。这里我们自定义分区实现仅根据域名而不是整个url分区。
定义自定义的分类器,需要继承org.apache.spark.Partitioner并实现下面三个方法:

  • numPartitions:Int:返回创建出来的分区数
  • getPartition(key:Any):Int:返回给定键的分区编号(0到numPartition-1)
  • equals():Java判断相等性的方法,Spark需要用这个方法检查你的分区器对象是否和其他分区器实例相同,这样Spark才能判断RDD的分区方式是否相同。
    下面编写一个基于域名的分区器,这个分区器只对url中的域名部分hash
class DomainNamePartitioner(numParts:Int) extends Partitioner{
	override def numpartitions: Int = numParts
	override def getPartition(key:Any): Int = {
	val domain = new Java.net.URL(key.toString).getHost()
	val code = (domain.hashCode % numPartitions)
	if(code<0){
		code + numPartitions
		}
	else{
		code
		}
	}
	override def equals(other:Any):Boolean = other match{
		case dnp:DomainNamePartitioner=>dnp.numPartitions == numPartitions
		case _ => false
		}
}

以上是Spark键值对操作的简单学习,下一篇是数据读取与保存

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ScalaSpark都可以操作MySQL数据库,具体步骤如下: 1. 导入MySQL JDBC驱动 在ScalaSpark程序中,需要先导入MySQL JDBC驱动,可以通过以下代码实现: ```scala Class.forName("com.mysql.jdbc.Driver") ``` 2. 创建MySQL连接 在ScalaSpark程序中,需要创建MySQL连接,可以通过以下代码实现: ```scala val url = "jdbc:mysql://localhost:3306/test" val username = "root" val password = "123456" val conn = DriverManager.getConnection(url, username, password) ``` 其中,url为MySQL数据库的连接地址,username和password为MySQL数据库的用户名和密码。 3. 执行MySQL查询 在ScalaSpark程序中,可以通过以下代码执行MySQL查询: ```scala val stmt = conn.createStatement() val rs = stmt.executeQuery("SELECT * FROM users") while (rs.next()) { val id = rs.getInt("id") val name = rs.getString("name") val age = rs.getInt("age") println(s"id=$id, name=$name, age=$age") } ``` 其中,stmt为MySQL的Statement对象,rs为查询结果集。 4. 关闭MySQL连接 在ScalaSpark程序中,需要关闭MySQL连接,可以通过以下代码实现: ```scala rs.close() stmt.close() conn.close() ``` 以上就是ScalaSpark操作MySQL的基本步骤。 ### 回答2: Scala是一种集成了面向对象编程和函数式编程的编程语言,由于其简洁和易读性,越来越受到开发者的喜爱。Spark则是一种大数据处理框架,具有高度的可扩展性和灵活性,适用于各种大数据场景。而对于操作MySQL,ScalaSpark都提供了一定的支持,下面将分别介绍它们的相关操作Scala操作MySQL Scala中使用MySQL可以使用Java JDBC API来实现,需要导入MySQL JDBC驱动,示例如下: ``` import java.sql.{Connection, DriverManager, ResultSet} object ScalaMySQL extends App { // 加载MySQL JDBC驱动 Class.forName("com.mysql.cj.jdbc.Driver") // 建立连接 val conn: Connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test?useSSL=false", "root", "password") // 执行查询 val stmt = conn.createStatement() val rs: ResultSet = stmt.executeQuery("SELECT * FROM users") while (rs.next()) { val id = rs.getInt("id") val name = rs.getString("name") println(s"ID: $id, Name: $name") } // 关闭连接 rs.close() stmt.close() conn.close() } ``` 在该示例代码中,我们先加载MySQL JDBC驱动,然后建立连接并执行查询操作,最后关闭连接。 Spark操作MySQL 对于Spark,我们可以使用Spark SQL的相关API来操作MySQL数据库,需要先导入MySQL JDBC驱动并配置连接信息,示例如下: ``` import org.apache.spark.sql.{DataFrame, SparkSession} object SparkMySQL extends App { // 配置MySQL连接信息 val jdbcHostname = "localhost" val jdbcPort = 3306 val jdbcDatabase = "test" val jdbcUsername = "root" val jdbcPassword = "password" val jdbcUrl = s"jdbc:mysql://${jdbcHostname}:${jdbcPort}/${jdbcDatabase}?user=${jdbcUsername}&password=${jdbcPassword}" // 创建SparkSession val spark = SparkSession.builder() .appName("SparkMySQL") .master("local[*]") .getOrCreate() // 读取MySQL中的表数据 val tableDF: DataFrame = spark.read.jdbc(jdbcUrl, "users", new Properties()) // 显示表数据 tableDF.show() // 关闭SparkSession spark.stop() } ``` 在该示例代码中,我们先配置MySQL连接信息,然后创建一个SparkSession,接着使用`read.jdbc()`方法读取MySQL数据库中的表数据,最后通过`show()`方法展示数据。得注意的是,如果表数据较大,可能需要使用分区读取和并行处理等优化方式。 总结 两种语言的方法都比较简单明了,Scala直接使用JDBC API进行操作,而Spark则使用Spark SQL的相关API,但都需要先导入MySQL JDBC驱动和配置连接信息。无论采用哪种方法,都需要注意安全性、性能和扩展性等问题,以使得操作MySQL更加高效、稳定和可靠。 ### 回答3: Scala是一门功能强大且可以与Java互操作的静态类型编程语言,它可以为大规模数据处理和分布式系统编程提供解决方案。而Spark则是一种快速的大数据处理框架,可以同时支持处理大型数据和实时数据。接下来,我们将讨论ScalaSpark如何与MySQL进行交互。 首先,ScalaSpark都支持使用JDBC API来连接MySQL数据库。在Scala中,我们可以使用Java的JDBC API来连接MySQL数据库并执行SQL语句,以执行数据的读取和写入。可以使用MySQL的Java驱动程序(mysql-connector-java),并在Scala中使用它来建立连接并执行SQL查询。 在Spark中,我们可以使用Spark SQL模块来处理MySQL数据。Spark SQL将JDBC连接封装在Spark DataFrame API中,这使得我们可以轻松地将数据从MySQL中读取到Spark中,然后对其进行处理。Spark SQL提供了许多连接器,包括JDBC连接器,可以轻松地将数据从外部数据存储(如MySQL)加载到Spark中。 除了使用JDBC连接器外,Spark也提供了用于连接各种数据源(包括MySQL)的扩展API。例如,我们可以使用Spark的Connector API来编写自己的自定义连接器(Custom connector),这些连接器可以与任何框架进行交互并支持自定义数据源。此外,Spark还提供了一种名为Spark Streaming的实时流处理框架,可以与MySQL等数据源进行交互,并在实时流处理环境中进行数据处理。 总结而言,ScalaSpark都能够灵活地与MySQL数据库进行交互,以实现大规模数据处理和分析。无论是在Scala中使用Java JDBC API,还是在Spark中使用Spark SQL模块和连接器,都可以方便地将MySQL数据集成到ScalaSpark的大数据生态系统中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值