声明: 使用Spark Streaming + Spark SQL实现在线动态计算出特定时间窗口下的不同种类商品中的热门商品排名 整理而成,未经允许不得转载。

实现技术:Spark Streaming+Spark SQL,之所以Spark Streaming能够使用ML、SQL、GraphX等功能是因为有foreachRDD和Transform等接口,这些接口中其实是基于RDD进行操作,所以以RDD为基石,就可以直接使用Spark其它所有的功能,就像直接调用API一样简单。

实现思路:假设这里的数据格式为:user item category(这里假设空格分隔),其中user为用户姓名,item为商品名称,category为商品的分类。例如Rocky Samsung Android。

实现步骤:
1.获取数据源。在实际的生产环境中我们的数据来源可能有很多种,例如说Kafka,日志系统等。无论数据来源如何复杂,我们实现的方式大致都是通过Receiver去获取的,这里我们为了方便测试,仍然采用监听端口的方式(socketTextStream),然后用Linux下的nc -lk来模拟数据的产生。

2.将获取的数据源采用DStream的map操作将每一行的数据用空格分隔,由于需要统计不同种类商品的热门商品,我们需要把category和item联合拼接成String类型作为Tuple的key,把value计数为1。即user item category ==> (category_item,1) 这里构建的Tuple类型为(String,Int)。

3.基于第2步我们得到的类型是DStream[(String,Int)]。然后采用reduceByKey对key相同的value进行累加。需要注意是基于的DStream没有提供像RDD的reduceByKey的操作,同时因为我们针对流进来的数据在特定的时间窗口下进行统计,例如说统计最近半小时的数据,所有我们采用reduceByKeyAndWindow操作。

4.上一步中我们返回的仍然是DStream[(String,Int)]类型,接下需要对数据采用Spark SQL进行TopN的操作,这里我们采用foreachRDD操作,为何要采用这个操作的原因是:
(a)foreachRDD内部可以针对RDD进行进一步的操作。
(b)采用Spark SQL是需要见RDD转换为DataFrame。
下面我们可以针对DStream内部的每个RDD进行操作了。
4.1 这里我们获取的类型就变成了RDD[(String,Int)]类型,我们采用RDD的map操作需要将类型转变为RDD[Row]类型,方便后续创建DataFrame,从上述第二步中我们知道RDD中的Tuple类型的第一个元素是以category_item为格式的String类型,在map操作中用”_”进行分割来构建我们的Row对象,每一条记录为Row(category, item, click_count),这里的click_count就是Tuple的第二个元素。
4.2 创建DataFrame。通过rdd的context方法获取到HiveContext操作句柄,针对每一个列的信息(这里分别将三列取名为category, item, click_count)构建structType,通过structType这个元信息和4.1的RDD[Row]调用HiveContext的createDataFrame创建DataFrame,并注册临时表名称为categoryItemTable。
4.3 编写HQL获取DataFrame最为结果。我们对商品名称进行分区(category),对点击量(click_count)降序排列,这里采用开窗函数,HQL如下:

SELECT
        category,item,click_count 
    FROM 
    (SELECT category,item,click_count,
            row_number() OVER (PARTITION BY category ORDER BY click_count DESC)             rank 
    FROM categoryItemTable
    ) subquery  
    WHERE rank <= 3

这里完成了案例中最重要的一步Top3,返回的是DataFrame类型。
4.4 步骤4.2中我们已经获取的了结果,但是在实际的生产环境中我们需要将结果保存到外部存储介质中,比如数据库。这里我们通过DataFrame的rdd方法将DataFrame转换为RDD,这里是RDD[Row]类型,采用RDD的foreachPartition方法数据以partition(分片的类型为Iterator[Row])批量更新到数据库中,这是一种较为高效的方式。这里我们首先需要获取数据库连接,然后对partition内部的每条记录进行数据库操作,最后关闭数据库连接。
以上我们是采用Scala的方式对这个案例进行了思考,代码实现就非常容易了。

案例代码

package com.dt.spark.sparkstreaming

import org.apache.spark.SparkConf
import org.apache.spark.sql.Row
import org.apache.spark.sql.hive.HiveContext
import org.apache.spark.sql.types.{IntegerType, StringType, StructField, StructType}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import com.dt.spark.streaming.ConnectionPool

object OnlineTheTop3ItemForEachCategory2DB {
  def main(args: Array[String]){
    //创建SparkConf对象
    val conf = new SparkConf()
    conf.setMaster("spark://Master:7077")
    conf.setAppName("OnlineTheTop3ItemForEachCategory2DB")

    val ssc = new StreamingContext(conf, Seconds(5))
    ssc.checkpoint("/root/Documents/SparkApps/checkpoint")

    val userClickLogsDStream = ssc.socketTextStream("Master", 9999)

    val formattedUserClickLogsDStream = userClickLogsDStream.map(clickLog =>
        (clickLog.split(" ")(2) + "_" + clickLog.split(" ")(1), 1))

    val categoryUserClickLogsDStream = formattedUserClickLogsDStream.reduceByKeyAndWindow(_+_,
      _-_, Seconds(60), Seconds(20))

      categoryUserClickLogsDStream.foreachRDD { rdd => {
        if (rdd.isEmpty()) {
          println("No data inputted!!!")
        } else {
          val categoryItemRow = rdd.map(reducedItem => {
            val category = reducedItem._1.split("_")(0)
            val item = reducedItem._1.split("_")(1)
            val click_count = reducedItem._2
            Row(category, item, click_count)
          })

          val structType = StructType(Array(
            StructField("category", StringType, true),
            StructField("item", StringType, true),
            StructField("click_count", IntegerType, true)
          ))

          val hiveContext = new HiveContext(rdd.context)
          val categoryItemDF = hiveContext.createDataFrame(categoryItemRow, structType)

          categoryItemDF.registerTempTable("categoryItemTable")

          val reseltDataFram = hiveContext.sql("SELECT category,item,click_count FROM (SELECT category,item,click_count,row_number()" + " OVER (PARTITION BY category ORDER BY click_count DESC) rank FROM categoryItemTable) subquery " +  " WHERE rank <= 3")

          val resultRowRDD = reseltDataFram.rdd
          resultRowRDD.foreachPartition { partitionOfRecords => {
              if (partitionOfRecords.isEmpty){
                println("This RDD is not null but partition is null")
              } else {
                // ConnectionPool is a static, lazily initialized pool of connections
                val connection = ConnectionPool.getConnection()
                partitionOfRecords.foreach(record => {
                  val sql = "insert into categorytop3(category,item,client_count) values('" + record.getAs("category") + "','" + record.getAs("item") + "'," + record.getAs("click_count") + ")"
                  val stmt = connection.createStatement();
                  stmt.executeUpdate(sql);
                })
                // return to the pool for future reuse
                ConnectionPool.returnConnection(connection) 
              }
            }
          }
        }
      }
    }
    ssc.start()
    ssc.awaitTermination()
  }
}