版權宣告:本套技術專欄是作者(秦凱新)平時工作的總結和昇華,通過從真實商業環境抽取案例進行總結和分享,並給出商業應用的調優建議和叢集環境容量規劃等內容,請持續關注本套部落格。QQ郵箱地址:1120746959@qq.com,如有任何技術交流,可隨時聯絡。
1 Scala 操作符
2 Spark RDD 持久化
cache()和 persist()的區別在於, cache()是 persist()的一種簡化方式, cache()的底 層就是呼叫的 persist()的無參版本,同時就是呼叫 persist(MEMORY_ONLY),將輸 入持久化到記憶體中。如果需要從記憶體中清除快取,那麼可以使用 unpersist()方法。
3 Spark 廣播變數
廣播變數允許程式設計師在每個機器上保留快取的只讀變數,而不是給每個任務發 送一個副本。 例如,可以使用它們以有效的方式為每個節點提供一個大型輸入資料 集的副本。 Spark 還嘗試使用高效的廣播演算法分發廣播變數,以降低通訊成本。
Spark 提供的 Broadcast Variable 是隻讀的,並且在每個節點上只會有一個副本, 而不會為每個 task 都拷貝一份副本,因此, 它的最大作用,就是減少變數到各個節 點的網路傳輸消耗,以及在各個節點上的記憶體消耗。此外, Spark 內部也使用了高效 的廣播演算法來減少網路消耗。
4 Spark 累加器
累加器(accumulator): Accumulator 是僅僅被相關操作累加的變數,因此可以 在並行中被有效地支援。它們可用於實現計數器(如 MapReduce)或總和計數。 Accumulator 是存在於 Driver 端的,從節點不斷把值發到 Driver 端,在 Driver 端計數(Spark UI 在 SparkContext 建立時被建立,即在 Driver 端被建立,因此它可 以讀取 Accumulator 的數值), 累加器是存在於 Driver 端的一個值,從節點是讀取不到的。
Spark 提供的 Accumulator 主要用於多個節點對一個變數進行共享性的操作。 Accumulator 只提供了累加的功能,但是卻給我們提供了多個 task 對於同一個變數 並行操作的功能,但是 task 只能對 Accumulator 進行累加操作,不能讀取它的值, 只有 Driver 程式可以讀取 Accumulator 的值。
5 Spark將DataFrame插入到Hive表中
-
DataFrame儲存到Hive表中
// 1:ArrayBuffer[ProductInfo]生成 private def mockProductInfo(): Array[ProductInfo] = { val rows = ArrayBuffer[ProductInfo]() val random = new Random() val productStatus = Array(0, 1) for (i <- 0 to 100) { val productId = i val productName = "product" + i val extendInfo = "{\"product_status\": " + productStatus(random.nextInt(2)) + "}" rows += ProductInfo(productId, productName, extendInfo) } rows.toArray } // 2:模擬資料 val userVisitActionData = this.mockUserVisitActionData() val userInfoData = this.mockUserInfo() val productInfoData = this.mockProductInfo() // 3:將模擬資料裝換為RDD val userVisitActionRdd = spark.sparkContext.makeRDD(userVisitActionData) val userInfoRdd = spark.sparkContext.makeRDD(userInfoData) val productInfoRdd = spark.sparkContext.makeRDD(productInfoData) // 4:載入SparkSQL的隱式轉換支援 import spark.implicits._ // 5:將使用者訪問資料裝換為DF儲存到Hive表中 val userVisitActionDF = userVisitActionRdd.toDF() insertHive(spark, USER_VISIT_ACTION_TABLE, userVisitActionDF) // 6:將使用者資訊資料轉換為DF儲存到Hive表中 val userInfoDF = userInfoRdd.toDF() insertHive(spark, USER_INFO_TABLE, userInfoDF) // 7:將產品資訊資料轉換為DF儲存到Hive表中 val productInfoDF = productInfoRdd.toDF() insertHive(spark, PRODUCT_INFO_TABLE, productInfoDF) // 8:將DataFrame插入到Hive表中 private def insertHive(spark: SparkSession, tableName: String, dataDF: DataFrame): Unit = { spark.sql("DROP TABLE IF EXISTS " + tableName) dataDF.write.saveAsTable(tableName) } 複製程式碼
-
DataSet 與 RDD 互操作
1.通過程式設計獲取 Schema:通過 spark 內部的 StructType 方式,將普通的 RDD 轉換成 DataFrame。 object SparkRDDtoDF { def rddToDF(sparkSession:SparkSession):DataFrame = { //設定 schema 結構 val schema = StructType( Seq( StructField("name",StringType,true), StructField("age",IntegerType,true) ) ) val rowRDD = sparkSession.sparkContext .textFile("file:/E:/scala_workspace/z_spark_study/people.txt",2) .map( x => x.split(",")).map( x => Row(x(0),x(1).trim().toInt)) sparkSession.createDataFrame(rowRDD,schema) } 2.通過反射獲取 Schema:使用 case class 的方式,不過在 scala 2.10 中最大支援 22 個欄位的 case class,這點需要注意; case class Person(name:String,age:Int) def rddToDFCase(sparkSession : SparkSession):DataFrame = { //匯入隱飾操作,否則 RDD 無法呼叫 toDF 方法 import sparkSession.implicits._ val peopleRDD = sparkSession.sparkContext .textFile("file:/E:/scala_workspace/z_spark_study/people.txt",2) .map( x => x.split(",")).map( x => Person(x(0),x(1).trim().toInt)).toDF() peopleRDD } 3 Main函式 def main(agrs : Array[String]):Unit = { val conf = new SparkConf().setMaster("local[2]") conf.set("spark.sql.warehouse.dir","file:/E:/scala_workspace/z_spark_study/") conf.set("spark.sql.shuffle.partitions","20") val sparkSession = SparkSession.builder().appName("RDD to DataFrame") .config(conf).getOrCreate() // 通過程式碼的方式,設定 Spark log4j 的級別 sparkSession.sparkContext.setLogLevel("WARN") import sparkSession.implicits._ //使用 case class 的方式 //val peopleDF = rddToDFCase(sparkSession) // 通過程式設計的方式完成 RDD 向 val peopleDF = rddToDF(sparkSession) peopleDF.show() peopleDF.select($"name",$"age").filter($"age">20).show() } } 複製程式碼
-
4 DataFrame/DataSet 轉 RDD
val rdd1=testDF.rdd val rdd2=testDS.rdd 複製程式碼
-
5 RDD 轉 DataFrame
import spark.implicits._ val testDF = rdd.map {line=> (line._1,line._2) }.toDF("col1","col2") 複製程式碼
-
6 DataSet 轉 DataFrame
import spark.implicits._ val testDF = testDS.toDF 複製程式碼
-
7 DataFrame 轉 DataSet
import spark.implicits._ //定義欄位名和型別 case class Coltest(col1:String, col2:Int) extends Serializable val testDS = testDF.as[Coltest] 複製程式碼
6 使用者自定義聚合函式(UDAF)
-
-
弱型別 UDAF 函式
/** * 使用者自定義聚合函式 */ class GroupConcatDistinctUDAF extends UserDefinedAggregateFunction { /** * 聚合函式輸入引數的資料型別 */ override def inputSchema: StructType = StructType(StructField("cityInfo", StringType) :: Nil) /** * 聚合緩衝區中值的型別 * 中間進行聚合時所處理的資料型別 */ override def bufferSchema: StructType = StructType(StructField("bufferCityInfo", StringType) :: Nil) /** * 函式返回值的資料型別 */ override def dataType: DataType = StringType /** * 一致性檢驗,如果為 true,那麼輸入不變的情況下計算的結果也是不變的 */ override def deterministic: Boolean = true /** * 設定聚合中間 buffer 的初始值 * 需要保證這個語義:兩個初始 buffer 呼叫下面實現的 merge 方法後也應該為初始 buffer 即如果你初始值是 1,然後你 merge 是執行一個相加的動作,兩個初始 buffer 合併之後等於 2,不會等於初始 buffer 了。這樣的初始 值就是有問題的,所以初始值也叫"zero value" */ override def initialize(buffer: MutableAggregationBuffer): Unit = { buffer(0)= "" } /** * 用輸入資料 input 更新 buffer 值,類似於 combineByKey */ override def update(buffer: MutableAggregationBuffer, input: Row): Unit = { // 緩衝中的已經拼接過的城市資訊串 var bufferCityInfo = buffer.getString(0) // 剛剛傳遞進來的某個城市資訊 val cityInfo = input.getString(0) // 在這裡要實現去重的邏輯 // 判斷:之前沒有拼接過某個城市資訊,那麼這裡才可以接下去拼接新的城市資訊 if(!bufferCityInfo.contains(cityInfo)) { if("".equals(bufferCityInfo)) bufferCityInfo += cityInfo else { // 比如 1:北京 // 1:北京,2:上海 bufferCityInfo += "," + cityInfo } buffer.update(0, bufferCityInfo) } } /** * 合併兩個 buffer,將 buffer2 合併到 buffer1.在合併兩個分割槽聚合結果的時候會被用到,類似於 reduceByKey * 這裡要注意該方法沒有返回值,在實現的時候是把 buffer2 合併到 buffer1 中去,你需要實現這個合併細節 */ override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = { var bufferCityInfo1 = buffer1.getString(0); val bufferCityInfo2 = buffer2.getString(0); for(cityInfo <- bufferCityInfo2.split(",")) { if(!bufferCityInfo1.contains(cityInfo)) { if("".equals(bufferCityInfo1)) { bufferCityInfo1 += cityInfo; } else { bufferCityInfo1 += "," + cityInfo; } } } buffer1.update(0, bufferCityInfo1); } /** * 計算並返回最終的聚合結果 */ override def evaluate(buffer: Row): Any = { buffer.getString(0) } } 複製程式碼
-
-
-
強型別 UDAF 函式
// 定義 case 類 case class Employee(name: String, salary: Long) case class Average(var sum: Long, var count: Long) object MyAverage extends Aggregator[Employee, Average, Double] { /** * 計算並返回最終的聚合結果 */ def zero: Average = Average(0L, 0L) /** * 根據傳入的引數值更新 buffer 值 */ def reduce(buffer: Average, employee: Employee): Average = { buffer.sum += employee.salary buffer.count += 1 buffer } /** * 合併兩個 buffer 值,將 buffer2 的值合併到 buffer1 */ def merge(b1: Average, b2: Average): Average = { b1.sum += b2.sum b1.count += b2.count b1 } /** * 計算輸出 */ def finish(reduction: Average): Double = reduction.sum.toDouble / reduction.count /** * 設定中間值型別的編碼器,要轉換成 case 類 * Encoders.product 是進行 scala 元組和 case 類轉換的編碼器 */ def bufferEncoder: Encoder[Average] = Encoders.product /** * 設定最終輸出值的編碼器 */ def outputEncoder: Encoder[Double] = Encoders.scalaDouble } 複製程式碼
-
7 開窗函式
-
開窗用於為行定義一個視窗(這裡的視窗是指運算將要操作的行的集合), 它 對一組值進行操作,不需要使用 GROUP BY 子句對資料進行分組,能夠在同一行中 同時返回基礎行的列和聚合列。
-
開窗函式的呼叫格式為: 函式名(列) OVER(選項)
第一大類: 聚合開窗函式 -> 聚合函式(列) OVER (選項),這裡的選項可以是 PARTITION BY 子句,但不可是 ORDER BY 子句。 def main(args: Array[String]): Unit = { val sparkConf = new SparkConf().setAppName("score").setMaster("local[*]") val sparkSession = SparkSession.builder().config(sparkConf).getOrCreate() import sparkSession.implicits._ val scoreDF = sparkSession.sparkContext.makeRDD(Array(Score("a1", 1, 80), Score("a2", 1, 78), Score("a3", 1, 95), Score("a4", 2, 74), Score("a5", 2, 92), Score("a6", 3, 99), Score("a7", 3, 99), Score("a8", 3, 45), Score("a9", 3, 55), Score("a10", 3, 78))).toDF("name", "class ", "score") scoreDF.createOrReplaceTempView("score") scoreDF.show() } OVER 關鍵字表示把聚合函式當成聚合開窗函式而不是聚合函式 sparkSession.sql("select name, class, score, count(name) over() name_count from score") PARTITION BY 子句建立的分割槽是獨立於結果集的,建立的分割槽只是供進行聚合計算的,而且不同的開窗函式所建立的分割槽也不互相影響。 sparkSession.sql("select name, class, score, count(name) over(partition by class) name_count from score").show() |name|class|score|name_count| +----+-----+-----+----------+ | a1| 1| 80| 3| | a2| 1| 78| 3| | a3| 1| 95| 3| | a6| 3| 99| 5| | a7| 3| 99| 5| | a8| 3| 45| 5| | a9| 3| 55| 5| | a10| 3| 78| 5| | a4| 2| 74| 2| | a5| 2| 92| 2| +----+-----+-----+----------+ 第二大類: 排序開窗函式 -> 排序函式(列) OVER(選項),這裡的選項可以是 ORDER BY 子句,也可以是 OVER(PARTITION BY 子句 ORDER BY 子句), 但不可以是 PARTITION BY 子句。 對於排序開窗函式來講,它支援的開窗函式分別為: ROW_NUMBER(行號)、 RANK(排名)、 DENSE_RANK(密集排名)和 NTILE(分組排名)。 sparkSession.sql("select name, class, score, row_number() over(order by score) rank from score").show() +----+-----+-----+----+ |name|class|score|rank| +----+-----+-----+----+ | a8| 3| 45| 1| | a9| 3| 55| 2| | a4| 2| 74| 3| | a2| 1| 78| 4| | a10| 3| 78| 5| | a1| 1| 80| 6| | a5| 2| 92| 7| | a3| 1| 95| 8| | a6| 3| 99| 9| | a7| 3| 99| 10| +----+-----+-----+----+ sparkSession.sql("select name, class, score, rank() over(order by score) rank from score").show() +----+-----+-----+----+ |name|class|score|rank| +----+-----+-----+----+ | a8| 3| 45| 1| | a9| 3| 55| 2| | a4| 2| 74| 3| | a2| 1| 78| 4| | a10| 3| 78| 4| | a1| 1| 80| 6| | a5| 2| 92| 7| | a3| 1| 95| 8| | a6| 3| 99| 9| | a7| 3| 99| 9| +----+-----+-----+----+ sparkSession.sql("select name, class, score, dense_rank() over(order by score) rank from score").show() ----+-----+-----+----+ |name|class|score|rank| +----+-----+-----+----+ | a8| 3| 45| 1| | a9| 3| 55| 2| | a4| 2| 74| 3| | a2| 1| 78| 4| | a10| 3| 78| 4| | a1| 1| 80| 5| | a5| 2| 92| 6| | a3| 1| 95| 7| | a6| 3| 99| 8| | a7| 3| 99| 8| +----+-----+-----+----+ sparkSession.sql("select name, class, score, ntile(6) over(order by score) rank from score").show() +----+-----+-----+----+ |name|class|score|rank| +----+-----+-----+----+ | a8| 3| 45| 1| | a9| 3| 55| 1| | a4| 2| 74| 2| | a2| 1| 78| 2| | a10| 3| 78| 3| | a1| 1| 80| 3| | a5| 2| 92| 4| | a3| 1| 95| 4| | a6| 3| 99| 5| | a7| 3| 99| 6| +----+-----+-----+----+ 複製程式碼
8 Dstream updataStateByKey 運算元(要求必須開啟 Checkpoint 機制)
object updateStateByKeyWordCount {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[2]").setAppName("Wordcount")
val ssc = new StreamingContext(conf, Seconds(1))
ssc.checkpoint("hdfs://s100:8020/wordcount_checkpoint")
val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
val wordCount = pairs.updateStateByKey((values:Seq[Int], state:Option[Int]) =>{
var newValue = state.getOrElse(0)
for(value <- values){
newValue += value
}
Option(newValue)
})
wordCount.print()
ssc.start()
ssc.awaitTermination()
}
}
複製程式碼
9 電商綜合應用案例
9.1 原資料模型
- 使用者行為表模型(每一次Action點選都會生成多條記錄,1個Session對應多個頁面Id)
- 使用者表
- 物品表
1. 點選Session
2018-02-11,81,af18373e1dbc47a397e87f186ffd9555,3,2018-02-11 17:04:42,null,37,17,null,null,null,null,7
2. 搜尋Session
2018-02-11,81,af18373e1dbc47a397e87f186ffd9555,3,2018-02-11 17:29:50,重慶小面,-1,-1,null,null,null,null,1
3. 下單Session
2018-02-11,81,af18373e1dbc47a397e87f186ffd9555,6,2018-02-11 17:50:10,null,-1,-1,61,71,null,null,2
4. 付款Session
2018-02-11,81,af18373e1dbc47a397e87f186ffd9555,4,2018-02-11 17:18:24,null,-1,-1,null,null,83,17,1
複製程式碼
9.2 資料處理模型
-
使用者訪問行為模型(每一個 Session_Id對應一個使用者,從而可以聚合一個使用者的所有操作行為)
-
一個 Session_Id 對應多個action_time,從而可以得出每一個Session的訪問週期Visit_Length。
-
一個 Session_Id 對應多個page_id,可以進一步統計出Step_Length 以及轉化率等指標。
Session_Id | Search_Keywords | Click_Category_Id | Visit_Length | Step_Length | Start_Time 複製程式碼
-
初步統計出每一個 Session_Id對應的Visit_Length和Step_Length
- 聯合使用者資訊進行定製過濾後,通過累加器,統計出visit_length_ratio及step_length_ratio
9.3 累加器功能實現
-
累加器在Driver端維護了一個Map,用於集中儲存所有Sesson中(如:1s_3s或1_3_ratio等)的訪問步長和訪問時長佔比累積數。
-
每一個Sesson 包含了一種(如:1s_3s或1_3_ratio)特徵。
import org.apache.spark.util.AccumulatorV2 import scala.collection.mutable /** * 自定義累加器 */ class SessionAggrStatAccumulator extends AccumulatorV2[String, mutable.HashMap[String, Int]] { // 儲存所有聚合資料 private val aggrStatMap = mutable.HashMap[String, Int]() override def isZero: Boolean = { aggrStatMap.isEmpty } override def copy(): AccumulatorV2[String, mutable.HashMap[String, Int]] = { val newAcc = new SessionAggrStatAccumulator aggrStatMap.synchronized{ newAcc.aggrStatMap ++= this.aggrStatMap } newAcc } override def reset(): Unit = { aggrStatMap.clear() } mutable.HashMap[String, Int]()的更新操作 override def add(v: String): Unit = { if (!aggrStatMap.contains(v)) aggrStatMap += (v -> 0) aggrStatMap.update(v, aggrStatMap(v) + 1) } override def merge(other: AccumulatorV2[String, mutable.HashMap[String, Int]]): Unit = { other match { case acc:SessionAggrStatAccumulator => { (this.aggrStatMap /: acc.value){ case (map, (k,v)) => map += ( k -> (v + map.getOrElse(k, 0)) )} } } } override def value: mutable.HashMap[String, Int] = { this.aggrStatMap } } 複製程式碼
9.4 Session分析模組
-
獲取統計任務引數【為了方便,直接從配置檔案中獲取,企業中會從一個排程平臺獲取】
task.params.json={startDate:"2018-02-01", \ endDate:"2018-02-28", \ startAge: 20, \ endAge: 50, \ professionals: "", \ cities: "", \ sex:"", \ keywords:"", \ categoryIds:"", \ targetPageFlow:"1,2,3,4,5,6,7"} val taskParam = JSONObject.fromObject(jsonStr) 複製程式碼
-
建立Spark客戶端
// 構建Spark上下文 val sparkConf = new SparkConf().setAppName("SessionAnalyzer").setMaster("local[*]") // 建立Spark客戶端 val spark = SparkSession.builder().config(sparkConf).enableHiveSupport().getOrCreate() val sc = spark.sparkContext 複製程式碼
-
設定自定義累加器,實現所有資料的統計功能,注意累加器也是懶執行的
val sessionAggrStatAccumulator = new SessionAggrStatAccumulator 複製程式碼
-
註冊自定義累加器
sc.register(sessionAggrStatAccumulator, "sessionAggrStatAccumulator") 複製程式碼
-
首先要從user_visit_action的Hive表中,查詢出來指定日期範圍內的行為資料
def getParam(jsonObject:JSONObject, field:String):String = { jsonObject.getString(field) } def getActionRDDByDateRange(spark: SparkSession, taskParam: JSONObject): RDD[UserVisitAction] = { val startDate = ParamUtils.getParam(taskParam, Constants.PARAM_START_DATE) val endDate = ParamUtils.getParam(taskParam, Constants.PARAM_END_DATE) import spark.implicits._ spark.sql("select * from user_visit_action where date>='" + startDate + "' and date<='" + endDate + "'") .as[UserVisitAction].rdd } rdd仍然具有表頭資訊 val actionRDD = this.getActionRDDByDateRange(spark, taskParam) 將使用者行為資訊轉換為 K-V 結構 val sessionid2actionRDD = actionRDD.map(item => (item.session_id, item)) 複製程式碼
-
將資料進行記憶體快取
sessionid2actionRDD.persist(StorageLevel.MEMORY_ONLY) 複製程式碼
-
將資料轉換為Session粒度(對資料聚合變換,得到過濾,搜尋列表陣列,點選類別陣列,訪問起始時間及訪問步長,訪問時長等)
格式為<sessionid,(sessionid,searchKeywords,clickCategoryIds,age,professional,city,sex)> def aggregateBySession(spark: SparkSession, sessinoid2actionRDD: RDD[(String, UserVisitAction)]): RDD[(String, String)] = { // 對行為資料按session粒度進行分組 val sessionid2ActionsRDD = sessinoid2actionRDD.groupByKey() // 對每一個session分組進行聚合,將session中所有的搜尋詞和點選品類都聚合起來,<userid,partAggrInfo(sessionid,searchKeywords,clickCategoryIds)> val userid2PartAggrInfoRDD = sessionid2ActionsRDD.map { case (sessionid, userVisitActions) => val searchKeywordsBuffer = new StringBuffer("") val clickCategoryIdsBuffer = new StringBuffer("") var userid = -1L // session的起始和結束時間 var startTime: Date = null var endTime: Date = null // session的訪問步長 var stepLength = 0 // 遍歷session所有的訪問行為 userVisitActions.foreach { userVisitAction => if (userid == -1L) { userid = userVisitAction.user_id } val searchKeyword = userVisitAction.search_keyword val clickCategoryId = userVisitAction.click_category_id // 實際上這裡要對資料說明一下 // 並不是每一行訪問行為都有searchKeyword何clickCategoryId兩個欄位的 // 其實,只有搜尋行為,是有searchKeyword欄位的 // 只有點選品類的行為,是有clickCategoryId欄位的 // 所以,任何一行行為資料,都不可能兩個欄位都有,所以資料是可能出現null值的 // 我們決定是否將搜尋詞或點選品類id拼接到字串中去 // 首先要滿足:不能是null值 // 其次,之前的字串中還沒有搜尋詞或者點選品類id if (StringUtils.isNotEmpty(searchKeyword)) { if (!searchKeywordsBuffer.toString.contains(searchKeyword)) { searchKeywordsBuffer.append(searchKeyword + ",") } } if (clickCategoryId != null && clickCategoryId != -1L) { if (!clickCategoryIdsBuffer.toString.contains(clickCategoryId.toString)) { clickCategoryIdsBuffer.append(clickCategoryId + ",") } } // 計算session開始和結束時間 val actionTime = DateUtils.parseTime(userVisitAction.action_time) if (startTime == null) { startTime = actionTime } if (endTime == null) { endTime = actionTime } if (actionTime.before(startTime)) { startTime = actionTime } if (actionTime.after(endTime)) { endTime = actionTime } // 計算session訪問步長 stepLength += 1 } val searchKeywords = StringUtils.trimComma(searchKeywordsBuffer.toString) val clickCategoryIds = StringUtils.trimComma(clickCategoryIdsBuffer.toString) // 計算session訪問時長(秒) val visitLength = (endTime.getTime() - startTime.getTime()) / 1000 // 聚合資料,使用key=value|key=value val partAggrInfo = Constants.FIELD_SESSION_ID + "=" + sessionid + "|" + Constants.FIELD_SEARCH_KEYWORDS + "=" + searchKeywords + "|" + Constants.FIELD_CLICK_CATEGORY_IDS + "=" + clickCategoryIds + "|" + Constants.FIELD_VISIT_LENGTH + "=" + visitLength + "|" + Constants.FIELD_STEP_LENGTH + "=" + stepLength + "|" + Constants.FIELD_START_TIME + "=" + DateUtils.formatTime(startTime) (userid, partAggrInfo); } // 查詢所有使用者資料,並對映成<userid,Row>的格式 import spark.implicits._ val userid2InfoRDD = spark.sql("select * from user_info").as[UserInfo].rdd.map(item => (item.user_id, item)) // 將session粒度聚合資料,與使用者資訊進行join val userid2FullInfoRDD = userid2PartAggrInfoRDD.join(userid2InfoRDD); // 對join起來的資料進行拼接,並且返回<sessionid,fullAggrInfo>格式的資料 val sessionid2FullAggrInfoRDD = userid2FullInfoRDD.map { case (uid, (partAggrInfo, userInfo)) => val sessionid = StringUtils.getFieldFromConcatString(partAggrInfo, "\\|", Constants.FIELD_SESSION_ID) val fullAggrInfo = partAggrInfo + "|" + Constants.FIELD_AGE + "=" + userInfo.age + "|" + Constants.FIELD_PROFESSIONAL + "=" + userInfo.professional + "|" + Constants.FIELD_CITY + "=" + userInfo.city + "|" + Constants.FIELD_SEX + "=" + userInfo.sex (sessionid, fullAggrInfo) } sessionid2FullAggrInfoRDD } 複製程式碼
-
根據查詢任務的配置,過濾使用者的行為資料,同時在過濾的過程中,對累加器中的資料進行統計
按照年齡、職業、城市範圍、性別、搜尋詞、點選品類這些條件過濾後的最終結果 def filterSessionAndAggrStat(sessionid2AggrInfoRDD: RDD[(String, String)], taskParam: JSONObject, sessionAggrStatAccumulator: AccumulatorV2[String, mutable.HashMap[String, Int]]): RDD[(String, String)] = { // 獲取查詢任務中的配置 val startAge = ParamUtils.getParam(taskParam, Constants.PARAM_START_AGE) val endAge = ParamUtils.getParam(taskParam, Constants.PARAM_END_AGE) val professionals = ParamUtils.getParam(taskParam, Constants.PARAM_PROFESSIONALS) val cities = ParamUtils.getParam(taskParam, Constants.PARAM_CITIES) val sex = ParamUtils.getParam(taskParam, Constants.PARAM_SEX) val keywords = ParamUtils.getParam(taskParam, Constants.PARAM_KEYWORDS) val categoryIds = ParamUtils.getParam(taskParam, Constants.PARAM_CATEGORY_IDS) var _parameter = (if (startAge != null) Constants.PARAM_START_AGE + "=" + startAge + "|" else "") + (if (endAge != null) Constants.PARAM_END_AGE + "=" + endAge + "|" else "") + (if (professionals != null) Constants.PARAM_PROFESSIONALS + "=" + professionals + "|" else "") + (if (cities != null) Constants.PARAM_CITIES + "=" + cities + "|" else "") + (if (sex != null) Constants.PARAM_SEX + "=" + sex + "|" else "") + (if (keywords != null) Constants.PARAM_KEYWORDS + "=" + keywords + "|" else "") + (if (categoryIds != null) Constants.PARAM_CATEGORY_IDS + "=" + categoryIds else "") if (_parameter.endsWith("\\|")) { _parameter = _parameter.substring(0, _parameter.length() - 1) } val parameter = _parameter // 根據篩選引數進行過濾 val filteredSessionid2AggrInfoRDD = sessionid2AggrInfoRDD.filter { case (sessionid, aggrInfo) => // 接著,依次按照篩選條件進行過濾 // 按照年齡範圍進行過濾(startAge、endAge) var success = true if (!ValidUtils.between(aggrInfo, Constants.FIELD_AGE, parameter, Constants.PARAM_START_AGE, Constants.PARAM_END_AGE)) success = false // 按照職業範圍進行過濾(professionals) // 網際網路,IT,軟體 // 網際網路 if (!ValidUtils.in(aggrInfo, Constants.FIELD_PROFESSIONAL, parameter, Constants.PARAM_PROFESSIONALS)) success = false // 按照城市範圍進行過濾(cities) // 北京,上海,廣州,深圳 // 成都 if (!ValidUtils.in(aggrInfo, Constants.FIELD_CITY, parameter, Constants.PARAM_CITIES)) success = false // 按照性別進行過濾 // 男/女 // 男,女 if (!ValidUtils.equal(aggrInfo, Constants.FIELD_SEX, parameter, Constants.PARAM_SEX)) success = false // 按照搜尋詞進行過濾 // 我們的session可能搜尋了 火鍋,蛋糕,燒烤 // 我們的篩選條件可能是 火鍋,串串香,iphone手機 // 那麼,in這個校驗方法,主要判定session搜尋的詞中,有任何一個,與篩選條件中 // 任何一個搜尋詞相當,即通過 if (!ValidUtils.in(aggrInfo, Constants.FIELD_SEARCH_KEYWORDS, parameter, Constants.PARAM_KEYWORDS)) success = false // 按照點選品類id進行過濾 if (!ValidUtils.in(aggrInfo, Constants.FIELD_CLICK_CATEGORY_IDS, parameter, Constants.PARAM_CATEGORY_IDS)) success = false // 如果符合任務搜尋需求 if (success) { sessionAggrStatAccumulator.add(Constants.SESSION_COUNT); // 計算訪問時長範圍 def calculateVisitLength(visitLength: Long) { if (visitLength >= 1 && visitLength <= 3) { sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_1s_3s); } else if (visitLength >= 4 && visitLength <= 6) { sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_4s_6s); } else if (visitLength >= 7 && visitLength <= 9) { sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_7s_9s); } else if (visitLength >= 10 && visitLength <= 30) { sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_10s_30s); } else if (visitLength > 30 && visitLength <= 60) { sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_30s_60s); } else if (visitLength > 60 && visitLength <= 180) { sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_1m_3m); } else if (visitLength > 180 && visitLength <= 600) { sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_3m_10m); } else if (visitLength > 600 && visitLength <= 1800) { sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_10m_30m); } else if (visitLength > 1800) { sessionAggrStatAccumulator.add(Constants.TIME_PERIOD_30m); } } // 計算訪問步長範圍 def calculateStepLength(stepLength: Long) { if (stepLength >= 1 && stepLength <= 3) { sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_1_3); } else if (stepLength >= 4 && stepLength <= 6) { sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_4_6); } else if (stepLength >= 7 && stepLength <= 9) { sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_7_9); } else if (stepLength >= 10 && stepLength <= 30) { sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_10_30); } else if (stepLength > 30 && stepLength <= 60) { sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_30_60); } else if (stepLength > 60) { sessionAggrStatAccumulator.add(Constants.STEP_PERIOD_60); } } // 計算出session的訪問時長和訪問步長的範圍,並進行相應的累加 val visitLength = StringUtils.getFieldFromConcatString(aggrInfo, "\\|", Constants.FIELD_VISIT_LENGTH).toLong val stepLength = StringUtils.getFieldFromConcatString(aggrInfo, "\\|", Constants.FIELD_STEP_LENGTH).toLong calculateVisitLength(visitLength) calculateStepLength(stepLength) } success } filteredSessionid2AggrInfoRDD } 複製程式碼
-
持久化辛苦聚合過濾統計值,對資料進行記憶體快取
filteredSessionid2AggrInfoRDD.persist(StorageLevel.MEMORY_ONLY) 複製程式碼
-
得到篩選的session對應的訪問明細資料(得到過濾後的原始資料)
def getSessionid2detailRDD(sessionid2aggrInfoRDD: RDD[(String, String)], sessionid2actionRDD: RDD[(String, UserVisitAction)]): RDD[(String, UserVisitAction)] = { sessionid2aggrInfoRDD.join(sessionid2actionRDD).map(item => (item._1, item._2._2)) } sessionid2detailRDD.persist(StorageLevel.MEMORY_ONLY) 複製程式碼
-
利用累積器開發業務功能一:統計各個範圍的session佔比,並寫入MySQL
calculateAndPersistAggrStat(spark, sessionAggrStatAccumulator.value, taskUUID) def calculateAndPersistAggrStat(spark: SparkSession, value: mutable.HashMap[String, Int], taskUUID: String) { // 從Accumulator統計串中獲取值 val session_count = value(Constants.SESSION_COUNT).toDouble val visit_length_1s_3s = value.getOrElse(Constants.TIME_PERIOD_1s_3s, 0) val visit_length_4s_6s = value.getOrElse(Constants.TIME_PERIOD_4s_6s, 0) val visit_length_7s_9s = value.getOrElse(Constants.TIME_PERIOD_7s_9s, 0) val visit_length_10s_30s = value.getOrElse(Constants.TIME_PERIOD_10s_30s, 0) val visit_length_30s_60s = value.getOrElse(Constants.TIME_PERIOD_30s_60s, 0) val visit_length_1m_3m = value.getOrElse(Constants.TIME_PERIOD_1m_3m, 0) val visit_length_3m_10m = value.getOrElse(Constants.TIME_PERIOD_3m_10m, 0) val visit_length_10m_30m = value.getOrElse(Constants.TIME_PERIOD_10m_30m, 0) val visit_length_30m = value.getOrElse(Constants.TIME_PERIOD_30m, 0) val step_length_1_3 = value.getOrElse(Constants.STEP_PERIOD_1_3, 0) val step_length_4_6 = value.getOrElse(Constants.STEP_PERIOD_4_6, 0) val step_length_7_9 = value.getOrElse(Constants.STEP_PERIOD_7_9, 0) val step_length_10_30 = value.getOrElse(Constants.STEP_PERIOD_10_30, 0) val step_length_30_60 = value.getOrElse(Constants.STEP_PERIOD_30_60, 0) val step_length_60 = value.getOrElse(Constants.STEP_PERIOD_60, 0) // 計算各個訪問時長和訪問步長的範圍 val visit_length_1s_3s_ratio = NumberUtils.formatDouble(visit_length_1s_3s / session_count, 2) val visit_length_4s_6s_ratio = NumberUtils.formatDouble(visit_length_4s_6s / session_count, 2) val visit_length_7s_9s_ratio = NumberUtils.formatDouble(visit_length_7s_9s / session_count, 2) val visit_length_10s_30s_ratio = NumberUtils.formatDouble(visit_length_10s_30s / session_count, 2) val visit_length_30s_60s_ratio = NumberUtils.formatDouble(visit_length_30s_60s / session_count, 2) val visit_length_1m_3m_ratio = NumberUtils.formatDouble(visit_length_1m_3m / session_count, 2) val visit_length_3m_10m_ratio = NumberUtils.formatDouble(visit_length_3m_10m / session_count, 2) val visit_length_10m_30m_ratio = NumberUtils.formatDouble(visit_length_10m_30m / session_count, 2) val visit_length_30m_ratio = NumberUtils.formatDouble(visit_length_30m / session_count, 2) val step_length_1_3_ratio = NumberUtils.formatDouble(step_length_1_3 / session_count, 2) val step_length_4_6_ratio = NumberUtils.formatDouble(step_length_4_6 / session_count, 2) val step_length_7_9_ratio = NumberUtils.formatDouble(step_length_7_9 / session_count, 2) val step_length_10_30_ratio = NumberUtils.formatDouble(step_length_10_30 / session_count, 2) val step_length_30_60_ratio = NumberUtils.formatDouble(step_length_30_60 / session_count, 2) val step_length_60_ratio = NumberUtils.formatDouble(step_length_60 / session_count, 2) // 將統計結果封裝為Domain物件 val sessionAggrStat = SessionAggrStat(taskUUID, session_count.toInt, visit_length_1s_3s_ratio, visit_length_4s_6s_ratio, visit_length_7s_9s_ratio, visit_length_10s_30s_ratio, visit_length_30s_60s_ratio, visit_length_1m_3m_ratio, visit_length_3m_10m_ratio, visit_length_10m_30m_ratio, visit_length_30m_ratio, step_length_1_3_ratio, step_length_4_6_ratio, step_length_7_9_ratio, step_length_10_30_ratio, step_length_30_60_ratio, step_length_60_ratio) import spark.implicits._ val sessionAggrStatRDD = spark.sparkContext.makeRDD(Array(sessionAggrStat)) sessionAggrStatRDD.toDF().write .format("jdbc") .option("url", ConfigurationManager.config.getString(Constants.JDBC_URL)) .option("dbtable", "session_aggr_stat") .option("user", ConfigurationManager.config.getString(Constants.JDBC_USER)) .option("password", ConfigurationManager.config.getString(Constants.JDBC_PASSWORD)) .mode(SaveMode.Append) .save() } 複製程式碼
-
按照Session粒度(注意每一個session可能有多條action記錄。)隨機均勻獲取Session。
randomExtractSession(spark, taskUUID, filteredSessionid2AggrInfoRDD, sessionid2detailRDD) def randomExtractSession(spark: SparkSession, taskUUID: String, sessionid2AggrInfoRDD: RDD[(String, String)], sessionid2actionRDD: RDD[(String, UserVisitAction)]) { // 第一步,計算出每天每小時的session數量,獲取<yyyy-MM-dd_HH,aggrInfo>格式的RDD val time2sessionidRDD = sessionid2AggrInfoRDD.map { case (sessionid, aggrInfo) => val startTime = StringUtils.getFieldFromConcatString(aggrInfo, "\\|", Constants.FIELD_START_TIME) // 將key改為yyyy-MM-dd_HH的形式(小時粒度) val dateHour = DateUtils.getDateHour(startTime) (dateHour, aggrInfo) } // 得到每天每小時的session數量 // countByKey()計算每個不同的key有多少個資料 // countMap<yyyy-MM-dd_HH, count> val countMap = time2sessionidRDD.countByKey() // 第二步,使用按時間比例隨機抽取演算法,計算出每天每小時要抽取session的索引,將<yyyy-MM-dd_HH,count>格式的map,轉換成<yyyy-MM-dd,<HH,count>>的格式 // dateHourCountMap <yyyy-MM-dd,<HH,count>> val dateHourCountMap = mutable.HashMap[String, mutable.HashMap[String, Long]]() for ((dateHour, count) <- countMap) { val date = dateHour.split("_")(0) val hour = dateHour.split("_")(1) // 通過模式匹配實現了if的功能 dateHourCountMap.get(date) match { // 對應日期的資料不存在,則新增 case None => dateHourCountMap(date) = new mutable.HashMap[String, Long](); dateHourCountMap(date) += (hour -> count) // 對應日期的資料存在,則更新 // 如果有值,Some(hourCountMap)將值取到了hourCountMap中 case Some(hourCountMap) => hourCountMap += (hour -> count) } } // 按時間比例隨機抽取演算法,總共要抽取100個session,先按照天數,進行平分 // 獲取每一天要抽取的數量 val extractNumberPerDay = 100 / dateHourCountMap.size // dateHourExtractMap[天,[小時,index列表]] val dateHourExtractMap = mutable.HashMap[String, mutable.HashMap[String, mutable.ListBuffer[Int]]]() val random = new Random() /** * 根據每個小時應該抽取的數量,來產生隨機值 * 遍歷每個小時,填充Map<date,<hour,(3,5,20,102)>> * @param hourExtractMap 主要用來存放生成的隨機值 * @param hourCountMap 每個小時的session總數 * @param sessionCount 當天所有的seesion總數 */ def hourExtractMapFunc(hourExtractMap: mutable.HashMap[String, mutable.ListBuffer[Int]], hourCountMap: mutable.HashMap[String, Long], sessionCount: Long) { for ((hour, count) <- hourCountMap) { // 計算每個小時的session數量,佔據當天總session數量的比例,直接乘以每天要抽取的數量 // 就可以計算出,當前小時需要抽取的session數量 var hourExtractNumber = ((count / sessionCount.toDouble) * extractNumberPerDay).toInt if (hourExtractNumber > count) { hourExtractNumber = count.toInt } // 仍然通過模式匹配實現有則追加,無則新建 hourExtractMap.get(hour) match { case None => hourExtractMap(hour) = new mutable.ListBuffer[Int](); // 根據數量隨機生成下標 for (i <- 0 to hourExtractNumber) { var extractIndex = random.nextInt(count.toInt); // 一旦隨機生成的index已經存在,重新獲取,直到獲取到之前沒有的index while (hourExtractMap(hour).contains(extractIndex)) { extractIndex = random.nextInt(count.toInt); } hourExtractMap(hour) += (extractIndex) } case Some(extractIndexList) => for (i <- 0 to hourExtractNumber) { var extractIndex = random.nextInt(count.toInt); // 一旦隨機生成的index已經存在,重新獲取,直到獲取到之前沒有的index while (hourExtractMap(hour).contains(extractIndex)) { extractIndex = random.nextInt(count.toInt); } hourExtractMap(hour) += (extractIndex) } } } } // session隨機抽取功能 for ((date, hourCountMap) <- dateHourCountMap) { // 計算出這一天的session總數 val sessionCount = hourCountMap.values.sum // dateHourExtractMap[天,[小時,小時列表]] dateHourExtractMap.get(date) match { case None => dateHourExtractMap(date) = new mutable.HashMap[String, mutable.ListBuffer[Int]](); // 更新index hourExtractMapFunc(dateHourExtractMap(date), hourCountMap, sessionCount) case Some(hourExtractMap) => hourExtractMapFunc(hourExtractMap, hourCountMap, sessionCount) } } /* 至此,index獲取完畢 */ //將Map進行廣播 val dateHourExtractMapBroadcast = spark.sparkContext.broadcast(dateHourExtractMap) // time2sessionidRDD <yyyy-MM-dd_HH,aggrInfo> // 執行groupByKey運算元,得到<yyyy-MM-dd_HH,(session aggrInfo)> val time2sessionsRDD = time2sessionidRDD.groupByKey() // 第三步:遍歷每天每小時的session,然後根據隨機索引進行抽取,我們用flatMap運算元,遍歷所有的<dateHour,(session aggrInfo)>格式的資料 val sessionRandomExtract = time2sessionsRDD.flatMap { case (dateHour, items) => val date = dateHour.split("_")(0) val hour = dateHour.split("_")(1) // 從廣播變數中提取出資料 val dateHourExtractMap = dateHourExtractMapBroadcast.value // 獲取指定天對應的指定小時的indexList // 當前小時需要的index集合 val extractIndexList = dateHourExtractMap.get(date).get(hour) // index是在外部進行維護 var index = 0 val sessionRandomExtractArray = new ArrayBuffer[SessionRandomExtract]() // 開始遍歷所有的aggrInfo for (sessionAggrInfo <- items) { // 如果篩選List中包含當前的index,則提取此sessionAggrInfo中的資料 if (extractIndexList.contains(index)) { val sessionid = StringUtils.getFieldFromConcatString(sessionAggrInfo, "\\|", Constants.FIELD_SESSION_ID) val starttime = StringUtils.getFieldFromConcatString(sessionAggrInfo, "\\|", Constants.FIELD_START_TIME) val searchKeywords = StringUtils.getFieldFromConcatString(sessionAggrInfo, "\\|", Constants.FIELD_SEARCH_KEYWORDS) val clickCategoryIds = StringUtils.getFieldFromConcatString(sessionAggrInfo, "\\|", Constants.FIELD_CLICK_CATEGORY_IDS) sessionRandomExtractArray += SessionRandomExtract(taskUUID, sessionid, starttime, searchKeywords, clickCategoryIds) } // index自增 index += 1 } sessionRandomExtractArray } /* 將抽取後的資料儲存到MySQL */ // 引入隱式轉換,準備進行RDD向Dataframe的轉換 import spark.implicits._ // 為了方便地將資料儲存到MySQL資料庫,將RDD資料轉換為Dataframe sessionRandomExtract.toDF().write .format("jdbc") .option("url", ConfigurationManager.config.getString(Constants.JDBC_URL)) .option("dbtable", "session_random_extract") .option("user", ConfigurationManager.config.getString(Constants.JDBC_USER)) .option("password", ConfigurationManager.config.getString(Constants.JDBC_PASSWORD)) .mode(SaveMode.Append) .save() // 提取抽取出來的資料中的sessionId val extractSessionidsRDD = sessionRandomExtract.map(item => (item.sessionid, item.sessionid)) // 第四步:獲取抽取出來的session的明細資料 // 根據sessionId與詳細資料進行聚合 val extractSessionDetailRDD = extractSessionidsRDD.join(sessionid2actionRDD) // 對extractSessionDetailRDD中的資料進行聚合,提煉有價值的明細資料 val sessionDetailRDD = extractSessionDetailRDD.map { case (sid, (sessionid, userVisitAction)) => SessionDetail(taskUUID, userVisitAction.user_id, userVisitAction.session_id, userVisitAction.page_id, userVisitAction.action_time, userVisitAction.search_keyword, userVisitAction.click_category_id, userVisitAction.click_product_id, userVisitAction.order_category_ids, userVisitAction.order_product_ids, userVisitAction.pay_category_ids, userVisitAction.pay_product_ids) } // 將明細資料儲存到MySQL中 sessionDetailRDD.toDF().write .format("jdbc") .option("url", ConfigurationManager.config.getString(Constants.JDBC_URL)) .option("dbtable", "session_detail") .option("user", ConfigurationManager.config.getString(Constants.JDBC_USER)) .option("password", ConfigurationManager.config.getString(Constants.JDBC_PASSWORD)) .mode(SaveMode.Append) .save() } 複製程式碼
-
獲取top10熱門品類
排序 case class CategorySortKey(val clickCount: Long, val orderCount: Long, val payCount: Long) extends Ordered[CategorySortKey] { override def compare(that: CategorySortKey): Int = { if (this.clickCount - that.clickCount != 0) { return (this.clickCount - that.clickCount).toInt } else if (this.orderCount - that.orderCount != 0) { return (this.orderCount - that.orderCount).toInt } else if (this.payCount - that.payCount != 0) { return (this.payCount - that.payCount).toInt } 0 } } 獲取各個品類的點選次數RDD def getClickCategoryId2CountRDD(sessionid2detailRDD: RDD[(String, UserVisitAction)]): RDD[(Long, Long)] = { // 只將點選行為過濾出來 val clickActionRDD = sessionid2detailRDD.filter { case (sessionid, userVisitAction) => userVisitAction.click_category_id != null } // 獲取每種類別的點選次數 // map階段:(品類ID,1L) val clickCategoryIdRDD = clickActionRDD.map { case (sessionid, userVisitAction) => (userVisitAction.click_category_id, 1L) } // 計算各個品類的點選次數 // reduce階段:對map階段的資料進行彙總 // (品類ID1,次數) (品類ID2,次數) (品類ID3,次數) ... ... (品類ID4,次數) clickCategoryIdRDD.reduceByKey(_ + _) } 連線品類RDD與資料RDD def joinCategoryAndData(categoryidRDD: RDD[(Long, Long)], clickCategoryId2CountRDD: RDD[(Long, Long)], orderCategoryId2CountRDD: RDD[(Long, Long)], payCategoryId2CountRDD: RDD[(Long, Long)]): RDD[(Long, String)] = { // 將所有品類資訊與點選次數資訊結合【左連線】 val clickJoinRDD = categoryidRDD.leftOuterJoin(clickCategoryId2CountRDD).map { case (categoryid, (cid, optionValue)) => val clickCount = if (optionValue.isDefined) optionValue.get else 0L val value = Constants.FIELD_CATEGORY_ID + "=" + categoryid + "|" + Constants.FIELD_CLICK_COUNT + "=" + clickCount (categoryid, value) } // 將所有品類資訊與訂單次數資訊結合【左連線】 val orderJoinRDD = clickJoinRDD.leftOuterJoin(orderCategoryId2CountRDD).map { case (categoryid, (ovalue, optionValue)) => val orderCount = if (optionValue.isDefined) optionValue.get else 0L val value = ovalue + "|" + Constants.FIELD_ORDER_COUNT + "=" + orderCount (categoryid, value) } // 將所有品類資訊與付款次數資訊結合【左連線】 val payJoinRDD = orderJoinRDD.leftOuterJoin(payCategoryId2CountRDD).map { case (categoryid, (ovalue, optionValue)) => val payCount = if (optionValue.isDefined) optionValue.get else 0L val value = ovalue + "|" + Constants.FIELD_PAY_COUNT + "=" + payCount (categoryid, value) } payJoinRDD } def getTop10Category(spark: SparkSession, taskid: String, sessionid2detailRDD: RDD[(String, UserVisitAction)]): Array[(CategorySortKey, String)] = { // 第一步:獲取每一個Sessionid 點選過、下單過、支付過的數量 // 獲取所有產生過點選、下單、支付中任意行為的商品類別 val categoryidRDD = sessionid2detailRDD.flatMap { case (sessionid, userVisitAction) => val list = ArrayBuffer[(Long, Long)]() // 一個session中點選的商品ID if (userVisitAction.click_category_id != null) { list += ((userVisitAction.click_category_id, userVisitAction.click_category_id)) } // 一個session中下單的商品ID集合 if (userVisitAction.order_category_ids != null) { for (orderCategoryId <- userVisitAction.order_category_ids.split(",")) list += ((orderCategoryId.toLong, orderCategoryId.toLong)) } // 一個session中支付的商品ID集合 if (userVisitAction.pay_category_ids != null) { for (payCategoryId <- userVisitAction.pay_category_ids.split(",")) list += ((payCategoryId.toLong, payCategoryId.toLong)) } list } // 對重複的categoryid進行去重 // 得到了所有被點選、下單、支付的商品的品類 val distinctCategoryIdRDD = categoryidRDD.distinct // 第二步:計算各品類的點選、下單和支付的次數 // 計算各個品類的點選次數 val clickCategoryId2CountRDD = getClickCategoryId2CountRDD(sessionid2detailRDD) // 計算各個品類的下單次數 val orderCategoryId2CountRDD = getOrderCategoryId2CountRDD(sessionid2detailRDD) // 計算各個品類的支付次數 val payCategoryId2CountRDD = getPayCategoryId2CountRDD(sessionid2detailRDD) // 第三步:join各品類與它的點選、下單和支付的次數 // distinctCategoryIdRDD中是所有產生過點選、下單、支付行為的商品類別 // 通過distinctCategoryIdRDD與各個統計資料的LeftJoin保證資料的完整性 val categoryid2countRDD = joinCategoryAndData(distinctCategoryIdRDD, clickCategoryId2CountRDD, orderCategoryId2CountRDD, payCategoryId2CountRDD); // 第四步:自定義二次排序key // 第五步:將資料對映成<CategorySortKey,info>格式的RDD,然後進行二次排序(降序) // 建立用於二次排序的聯合key —— (CategorySortKey(clickCount, orderCount, payCount), line) // 按照:點選次數 -> 下單次數 -> 支付次數 這一順序進行二次排序 val sortKey2countRDD = categoryid2countRDD.map { case (categoryid, line) => val clickCount = StringUtils.getFieldFromConcatString(line, "\\|", Constants.FIELD_CLICK_COUNT).toLong val orderCount = StringUtils.getFieldFromConcatString(line, "\\|", Constants.FIELD_ORDER_COUNT).toLong val payCount = StringUtils.getFieldFromConcatString(line, "\\|", Constants.FIELD_PAY_COUNT).toLong (CategorySortKey(clickCount, orderCount, payCount), line) } // 降序排序 val sortedCategoryCountRDD = sortKey2countRDD.sortByKey(false) // 第六步:用take(10)取出top10熱門品類,並寫入MySQL val top10CategoryList = sortedCategoryCountRDD.take(10) val top10Category = top10CategoryList.map { case (categorySortKey, line) => val categoryid = StringUtils.getFieldFromConcatString(line, "\\|", Constants.FIELD_CATEGORY_ID).toLong val clickCount = StringUtils.getFieldFromConcatString(line, "\\|", Constants.FIELD_CLICK_COUNT).toLong val orderCount = StringUtils.getFieldFromConcatString(line, "\\|", Constants.FIELD_ORDER_COUNT).toLong val payCount = StringUtils.getFieldFromConcatString(line, "\\|", Constants.FIELD_PAY_COUNT).toLong Top10Category(taskid, categoryid, clickCount, orderCount, payCount) } // 將Map結構轉化為RDD val top10CategoryRDD = spark.sparkContext.makeRDD(top10Category) // 寫入MySQL之前,將RDD轉化為Dataframe import spark.implicits._ top10CategoryRDD.toDF().write .format("jdbc") .option("url", ConfigurationManager.config.getString(Constants.JDBC_URL)) .option("dbtable", "top10_category") .option("user", ConfigurationManager.config.getString(Constants.JDBC_USER)) .option("password", ConfigurationManager.config.getString(Constants.JDBC_PASSWORD)) .mode(SaveMode.Append) .save() top10CategoryList } 複製程式碼
-
獲取top10熱門品類的活躍session(先join熱門品類得到熱門的session,再迭代計算每一種品類對應的session中點選次數排名,取前10)
1 sessionid2detailRDD 資料結構重組和計算所有品類出現的次數累加值count (一個SessionId對應的多條action記錄:sessionid-iter(userVisitAction)) val sessionid2ActionsRDD = sessionid2ActionRDD.groupByKey() 資料結構重組後輸出 (categoryid, sessionid + "," + count) 2 獲取到top10熱門品類,被各個session點選的次數【將資料集縮小】,包含大量的重複key val top10CategorySessionCountRDD = top10CategoryIdRDD.join(categoryid2sessionCountRDD).map { case (cid, (ccid, value)) => (cid, value) } 3 整合大量重複的key,按照品類分組,獲取品類下的所有(sessionid + "," + count)迭代器。 val top10CategorySessionCountsRDD = top10CategorySessionCountRDD.groupByKey() 4 每一種品類對應的session中點選次數進行排序,取前10 val top10Sessions = clicks.toList.sortWith(_.split(",")(1) > _.split(",")(1)).take(10) 複製程式碼
-
版權宣告:本套技術專欄是作者(秦凱新)平時工作的總結和昇華,通過從真實商業環境抽取案例進行總結和分享,並給出商業應用的調優建議和叢集環境容量規劃等內容,請持續關注本套部落格。QQ郵箱地址:1120746959@qq.com,如有任何技術交流,可隨時聯絡。
def getTop10Session(spark: SparkSession, taskid: String, top10CategoryList: Array[(CategorySortKey, String)], sessionid2ActionRDD: RDD[(String, UserVisitAction)]) { // 第一步:將top10熱門品類的id,生成一份RDD // 獲得所有需要求的category集合 val top10CategoryIdRDD = spark.sparkContext.makeRDD(top10CategoryList.map { case (categorySortKey, line) => val categoryid = StringUtils.getFieldFromConcatString(line, "\\|", Constants.FIELD_CATEGORY_ID).toLong; (categoryid, categoryid) }) // 第二步:計算top10品類被各session點選的次數 // sessionid2ActionRDD是符合過濾(職業、年齡等)條件的完整資料 // sessionid2detailRDD ( sessionId, userAction ) val sessionid2ActionsRDD = sessionid2ActionRDD.groupByKey() // 獲取每個品類被每一個Session點選的次數 val categoryid2sessionCountRDD = sessionid2ActionsRDD.flatMap { case (sessionid, userVisitActions) => val categoryCountMap = new mutable.HashMap[Long, Long]() // userVisitActions中聚合了一個session的所有使用者行為資料 // 遍歷userVisitActions是提取session中的每一個使用者行為,並對每一個使用者行為中的點選事件進行計數 for (userVisitAction <- userVisitActions) { // 如果categoryCountMap中尚不存在此點選品類,則新增品類 if (!categoryCountMap.contains(userVisitAction.click_category_id)) categoryCountMap.put(userVisitAction.click_category_id, 0) // 如果categoryCountMap中已經存在此點選品類,則進行累加 if (userVisitAction.click_category_id != null && userVisitAction.click_category_id != -1L) { categoryCountMap.update(userVisitAction.click_category_id, categoryCountMap(userVisitAction.click_category_id) + 1) } } // 對categoryCountMap中的資料進行格式轉化 for ((categoryid, count) <- categoryCountMap) yield (categoryid, sessionid + "," + count) } // 通過top10熱門品類top10CategoryIdRDD與完整品類點選統計categoryid2sessionCountRDD進行join,僅獲取熱門品類的資料資訊 // 獲取到to10熱門品類,被各個session點選的次數【將資料集縮小】 val top10CategorySessionCountRDD = top10CategoryIdRDD.join(categoryid2sessionCountRDD).map { case (cid, (ccid, value)) => (cid, value) } // 第三步:分組取TopN演算法實現,獲取每個品類的top10活躍使用者 // 先按照品類分組 val top10CategorySessionCountsRDD = top10CategorySessionCountRDD.groupByKey() // 將每一個品類的所有點選排序,取前十個,並轉換為物件 //(categoryid, sessionId=1213,sessionId=908) val top10SessionObjectRDD = top10CategorySessionCountsRDD.flatMap { case (categoryid, clicks) => // 先排序,然後取前10 val top10Sessions = clicks.toList.sortWith(_.split(",")(1) > _.split(",")(1)).take(10) // 重新整理資料 top10Sessions.map { case line => val sessionid = line.split(",")(0) val count = line.split(",")(1).toLong Top10Session(taskid, categoryid, sessionid, count) } } // 將結果以追加方式寫入到MySQL中 import spark.implicits._ top10SessionObjectRDD.toDF().write .format("jdbc") .option("url", ConfigurationManager.config.getString(Constants.JDBC_URL)) .option("dbtable", "top10_session") .option("user", ConfigurationManager.config.getString(Constants.JDBC_USER)) .option("password", ConfigurationManager.config.getString(Constants.JDBC_PASSWORD)) .mode(SaveMode.Append) .save() val top10SessionRDD = top10SessionObjectRDD.map(item => (item.sessionid, item.sessionid)) // 第四步:獲取top10活躍session的明細資料 val sessionDetailRDD = top10SessionRDD.join(sessionid2ActionRDD).map { case (sid, (sessionid, userVisitAction)) => SessionDetail(taskid, userVisitAction.user_id, userVisitAction.session_id, userVisitAction.page_id, userVisitAction.action_time, userVisitAction.search_keyword, userVisitAction.click_category_id, userVisitAction.click_product_id, userVisitAction.order_category_ids, userVisitAction.order_product_ids, userVisitAction.pay_category_ids, userVisitAction.pay_product_ids) } // 將活躍Session的明細資料,寫入到MySQL sessionDetailRDD.toDF().write .format("jdbc") .option("url", ConfigurationManager.config.getString(Constants.JDBC_URL)) .option("dbtable", "session_detail") .option("user", ConfigurationManager.config.getString(Constants.JDBC_USER)) .option("password", ConfigurationManager.config.getString(Constants.JDBC_PASSWORD)) .mode(SaveMode.Append) .save() } 複製程式碼
10 總結
溫故而知新,本文為了綜合複習,進行程式碼總結,內容粗鄙,勿怪
版權宣告:本套技術專欄是作者(秦凱新)平時工作的總結和昇華,通過從真實商業環境抽取案例進行總結和分享,並給出商業應用的調優建議和叢集環境容量規劃等內容,請持續關注本套部落格。QQ郵箱地址:1120746959@qq.com,如有任何技術交流,可隨時聯絡。
秦凱新 於深圳