現(xiàn)象
在運行[spark][1]程序期間,在針對 [dataFrame][2] 的map操作中,產(chǎn)生了類似[HashMap][3] key重復(fù)的現(xiàn)象,如圖所示
這個問題導(dǎo)致了后續(xù)統(tǒng)計上的一系列問題
分析
起初我們是實際跟蹤代碼進行分析的,但是發(fā)現(xiàn)scala代碼中沒有任何問題,各種處理也非常合理
代碼羅列如下,想看就看看,不看也能理解
// 目標是針對businessid進行聚合,然后輸出各個業(yè)務(wù)id下,銷售天數(shù)
// 獲取數(shù)據(jù)的代碼
val dataframe = spark.sql("select businessid,date,money from table1")
case class Stat() {
// 針對每一個businessid的統(tǒng)計
var moneyByDay: HashMap[String, Double] = HashMap[String, Double]()
// 針對每一條記錄的id進行相加
def moneyByDayOp(data: (String, Double)) = {
if (this.moneyByDay.contains(data._1)) {
val tmpMoney = this.moneyByDay.get(data._1)
val finalMoney = tmpMoney.getOrElse[Double](0) + data._2
this.moneyByDay.remove(data._1)
this.moneyByDay.put(data._1, finalMoney)
} else {
if (data._2 > 0)
this.moneyByDay.put(data._1, data._2)
}
}
}
/**
* 歸并多條記錄的結(jié)果
**/
def reduceStatModel(one: Stat, another: Stat): Stat = {
one.moneyByDay ++= another.moneyByDay
one
}
/**
* 針對每條記錄生成一個統(tǒng)計值
**/
import org.apache.spark.sql.Row
def parseProductDay(data: Row): (Long, Stat) = {
val result: Stat = new Stat
val a: String = data.getAs[String]("date")
result.moneyByDayOp(a, data.getAs[Long]("money"))
(data.getAs[Long]("product_id"), result)
}
// 核心流程
val finalRs = dataframe.rdd
.map(line => parseProductDay(line))
.reduceByKey(reduceStatModel(_, _))
val hashMp = finalRs.collect()(0)._2.moneyByDay
hashMp.put("20170727",1)
針對的存儲,其實就是Stat類中的 moneyByDay對象,本質(zhì)上是一個HashMap,并且通過泛型控制,key類型為String,value的類型為Double
放到集群上執(zhí)行,是可以通過的,并且得到類似上圖的結(jié)論
hashMap中包含重復(fù)的key,只能是兩個可能
- key類型相同,但是可能原始字符串中有空格,或者不可見字符
- key類型不同,一個是字符串,另一個是其他數(shù)據(jù)類型
經(jīng)過檢查,1的可能性排除
問題原因是2,居然是2,排除了所有不可能之后,最后的真相即使再不可能,也是真的
原因
其實這種結(jié)論是意料之外,情理之中
眾所周知,java的泛型檢查是編譯時的檢查,實際運行時,容器類存儲和運算都是將對象看做object進行處理的
對于基于jvm的scala,泛型本質(zhì)上也是一個靜態(tài)編譯檢查
上述代碼,如果table1表中的字段date,其類型是string,那么萬事ok,運算和結(jié)論都會正常
但是如果date是其他類型,比如int,那么就會產(chǎn)生問題
問題出現(xiàn)在上述代碼的37行
val a: String = data.getAs[String]("date")
// 這行代碼是org.apache.spark.sql.Row對象的一個調(diào)用,目的是獲取指定類型的字段,并且轉(zhuǎn)化為指定類型
// 源碼如下:
/**
* Returns the value at position i.
* For primitive types if value is null it returns 'zero value' specific for primitive
* ie. 0 for Int - use isNullAt to ensure that value is not null
*
* @throws ClassCastException when data type does not match.
*/
def getAs[T](i: Int): T = get(i).asInstanceOf[T] // 此處是進行強制轉(zhuǎn)化
/**
* Returns the value of a given fieldName.
* For primitive types if value is null it returns 'zero value' specific for primitive
* ie. 0 for Int - use isNullAt to ensure that value is not null
*
* @throws UnsupportedOperationException when schema is not defined.
* @throws IllegalArgumentException when fieldName do not exist.
* @throws ClassCastException when data type does not match.
*/
def getAs[T](fieldName: String): T = getAs[T](fieldIndex(fieldName))
解決方案
將table1表的字段設(shè)計成string,代碼可以運行通過
-
37行代碼改為如下,轉(zhuǎn)化string即可
val a: String = String.valueOf(data.getAs[String]("date"))
?
疑問
在spark-shell中調(diào)用這段代碼,其實是會報錯的
val a = data.getAs[String]("date")
但是為什么集群執(zhí)行會通過?
并且返回了一個HashMap[String,Double],其中的key都是Int。。。每次調(diào)用foreach方法都會報錯
-
https://spark.apache.org/ "apache基于內(nèi)存的分布式計算框架" ?
-
https://spark.apache.org/docs/latest/sql-programming-guide.html "spark-dataframe, 與pandas的Dataframe相似,是針對分布式計算的抽象和實現(xiàn)" ?
-
http://www.scala-lang.org/api/current/scala/collection/mutable/HashMap.html " "scala.mutable.hashmap scala中的map分為可變和不可變的" ?