基於Spark實時計算商品關注度

基於Spark實時計算商品關注度

一、實驗介紹

1.1 內容簡介

處於網絡時代的我們,隨着 O2O 的營銷模式的流行,越來越多的人開始做起了電商。

與此同時也產生了許多網絡數據,然而這些數據有什麼用呢。比如說一個電商公司可以根據一個商品被用戶點擊了多少次,用戶停留時間是多久,用戶是否收藏了該商品。這些都是可以被記錄下來的。通過這些數據我們就能分析出這段時間內哪些商品最受普遍人們的關注。同時也可以針對這些數據進行用戶商品推薦。

後面我們將使用Scoket來模擬用戶瀏覽商品產生實時數據,數據包括用戶當前瀏覽的商品以及瀏覽商品的次數和停留時間和是否收藏該商品。使用Spark Streaming構建實時數據處理系統,來計算當前電商平臺最受人們關注的商品是哪些。

1.2 實驗知識點

  • Java Scoket編程的基本原理

  • Spark Streaming應用的基本實現

  • DStream的基本操作

  • updateStateByKey的使用

1.3 實驗環境

  • Spark1.6.1

  • Xfce終端

  • eclipse

1.4 適合人羣

該項目難度一般,屬於中等難度,該項目適合有一定的Java編程基礎以及一定得Spark知識,瞭解Streaming的工作機制。

二、實驗原理

通過蒐集用戶瀏覽信息,通過網絡傳輸,實時處理用戶瀏覽數據。進行必要的處理,經過計算得到商品的關注度。現在網絡上很多人都是使用得Scala語言進行Spark得實現。因爲它書寫簡潔,快速。但是好多學校並沒有開設這一個課程。好多都是學習得Java知識,但是網上有關Java方式得實現少之又少。這次的項目我就是使用Java得方式進行Spark Streaming的實現。

2.1 實驗流程圖

此處輸入圖片的描述

2.2 實驗拓展

當然這只是個簡單的項目,想要拓展的話,可以再加上Hadoop集羣,將數據保存在HDFS上,還可以通過用戶數據進行建模處理,建模型存儲在HDFS上,進行用戶商品推薦這個項目可以橫向擴展。加上更多的模塊,比如說Mlib。ALS算法進行用戶商品推薦。當然這裏也可以使用kafka來進行消息的傳輸。但是由於一些原因,這裏就沒有使用kafka了,使用了Socket來模擬數據傳輸,感興趣的同學可以嘗試使用kafka來作爲數據傳輸。又不清楚的地方可以留言進行諮詢。

2.3 實驗結果展示

項目結構圖

此處輸入圖片的描述

用戶數據模擬效果:數據格式(商品ID::瀏覽次數::停留時間::是否收藏::購買件數)

此處輸入圖片的描述

商品關關注度計算結果展示(這個值會隨着數據的產生會不斷的變化)

此處輸入圖片的描述

三、實驗步驟

下述介紹爲實驗樓默認環境,如果您使用的是定製環境,請修改成您自己的環境介紹。

3.1 工程準備

由於在現實場景下,我們並沒有真正的實時的電商平臺數據,這些數據肯定也是人家公司保密的數據。那麼也只能通過我們自己創建一個模擬器,實現用戶瀏覽商品的數據。下面我們就來創建一Java Project,來實現相關功能。

3.2 Java工程創建

File -> New -> Java Project

此處輸入圖片的描述

此處輸入圖片的描述

到此我們就創建好了一個成功的名爲StreamingProject得Java工程。

目錄結構爲:

此處輸入圖片的描述

3.3 代碼實現

3.3.1 Spark-assembly工具包的引入

在工程中創建一個lib的文件夾,用於存放jar

右鍵項目 ->New -> Folder

此處輸入圖片的描述

此處輸入圖片的描述

現在就會看到當前工程下多了一個lib文件夾:

此處輸入圖片的描述

向lib中添加jar:

現在我們回到桌面上,到文件系統中的spark的安裝目錄下的lib下拷貝需要的jar

主文件夾 -> 文件系統 -> opt -> spark-1.6.1-bin-hadoop2.6 -> lib

此處輸入圖片的描述

找到該jar將其複製到我們在工程裏創建lib文件夾下

此處輸入圖片的描述

然後在當前工程中引用該jar

右鍵該jar -> build path -> add to build path

此處輸入圖片的描述

3.3.2 模擬器實現 SimulatorSocket.java

實現比較簡單,使用了簡單的Java Socket知識,以及線程來控制它發送消息的頻率。詳細請看註釋

package com.shiyanlou.simulator;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;
public class SimulatorSocket {
    public static void main(String[] args) throws Exception {
        //創建一個線程來啓動模擬器
        new Thread(new SimulatorSocketLog()).start();
    }
}
class SimulatorSocketLog implements Runnable{
    //假設一共有200個商品
    private int GOODSID = 200;
    //隨機發送消息的條數
    private int MSG_NUM = 30;
    //假設用戶瀏覽該商品的次數
    private int BROWSE_NUM = 5;
    //假設用戶瀏覽商品停留的時間
    private int STAY_TIME = 10;
    //用來體現用戶是否收藏,收藏爲1,不收藏爲0,差評爲-1
    int[] COLLECTION = new int[]{-1,0,1};
    //用來模擬用戶購買商品的件數,0比較多是爲了增加沒有買的概率,畢竟不買的還是很多的,很多用戶都只是看看
    private int[] BUY_NUM = new int[]{0,1,0,2,0,0,0,1,0};
    public void run() {
        // TODO Auto-generated method stub
        Random r = new Random();

        try {
            /**
            *創建一個服務器端,監聽9999端口,客戶端就是Streaming,通過看源碼才知道,Streaming *socketTextStream 其實就是相當於一個客戶端
            */
            ServerSocket sScoket = new ServerSocket(9999);
            System.out.println("成功開啓數據模擬模塊,去運行Streaming程序把!");
            while(true){
                //隨機消息數
                int msgNum = r.nextInt(MSG_NUM)+1;
                    //開始監聽
                    Socket socket = sScoket.accept();
                    //創建輸出流
                    OutputStream os = socket.getOutputStream();    
                    //包裝輸出流
                    PrintWriter pw = new PrintWriter(os);
                    for (int i = 0; i < msgNum; i++) {
                    //消息格式:商品ID::瀏覽次數::停留時間::是否收藏::購買件數
                    StringBuffer sb = new StringBuffer();
                    sb.append("goodsID-"+(r.nextInt(GOODSID)+1));
                    sb.append("::");
                    sb.append(r.nextInt(BROWSE_NUM)+1);
                    sb.append("::");
                    sb.append(r.nextInt(STAY_TIME)+r.nextFloat());
                    sb.append("::");
                    sb.append(COLLECTION[r.nextInt(2)]);
                    sb.append("::");
                    sb.append(BUY_NUM[r.nextInt(9)]);
                    System.out.println(sb.toString());
                    //發送消息
                    pw.write(sb.toString()+"\n");
                }
                    pw.flush();
                    pw.close();
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    System.out.println("thread sleep failed");
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            System.out.println("port used");
        }

    }

}

3.3.3 流式計算,Streaming 程序:StreamingGoods.java

簡單的實現了Streaming程序。通過接收socket數據,對數據進行拆分,計算。

我們先來了解一些關於Streaming的基礎知識。

1.什麼是Streaming

Spark Streaming它實現了對實時流數據的高吞吐量, 低容錯率的流處理。數據可以有許多來源,如 Kafka, Flume, TCP 套接字,可以使用一些邏輯算法來處理分析這些數據,並將這些數據持久化到數據庫。比如說Hbase、Spark SQL、HDFS等等。如下圖所示。圖來自Spark官網。

此處輸入圖片的描述

其內部工作原理如下圖所示,圖片來自Spark官網。

此處輸入圖片的描述

2.關於DStream的操作

關於DStream的瞭解均來自於Spark官方網站。有什麼不足,還請多多諒解。

DStream 是 Spark Streaming 提供的基本抽象。它代表了一個連續的數據 流,或者從源端接收到的,或通過將輸入流中產生處理後的數據流。在內部,它是由 RDDS 的連續序列表示。在 DStream 中每 個 RDD 包含數據從某一個時間間隔,如下圖。圖片來自Spark官網。

此處輸入圖片的描述

說白了他其實還是RDD。和Spark RDD的操作是差不多的,在DStream上面的任何操作都會轉化爲底層的RDDS操作。如下圖所示。圖片來自官網。

此處輸入圖片的描述

有關DStream的算子,在這裏就不一一羅列了。大家可以參考官網上的說明。鏈接地址:http://spark.apache.org/docs/1.6.1/streaming-programming-guide.html

有關Transformations on Dstreams的說明。

現在我們先分開講解StreamingGoods.java類的具體實現,完整代碼在最後貼出

初始化StreamingContext

首先使用Java初始化Streaming程序,需要先定義一個JavaStreamingContext,它是所有的Java Streaming程序的切入點。

實現一個JavaStreamingContext需要先定義一個SparkConf。實現代碼如下

SparkConf sparkConf = new SparkConf().setAppName("StreamingGoods").setMaster("local[2]");
//AppName 自然就是當前Job的名字,Master就是Spark的主機地址,這裏採用的是本地模式
JavaStreamingContext jsc = new JavaStreamingContext(sparkConf,new Duration(5000));
// new Duration(5000) 窗口時間,單位毫秒

獲取模擬數據

JavaReceiverInputDStream<String> jds = jsc.socketTextStream("127.0.0.1", 9999);

消息處理

商品關注度怎麼計算呢,這個可能需要一個約定,就是說瀏覽次數和瀏覽時間以及購買力度和是否收藏該商品都有一個權重,可能不同的公司覺得不同的選項權重不一樣,可能你覺得瀏覽時間更重要,另一人覺得瀏覽次數更重要,所以我們事先約定好這個計算公式。我們約定瀏覽次數的權重爲0.8,瀏覽時間權重爲0.6,是否收藏和購買力度都爲1。

//mapToPair就是將rdd轉換爲鍵值對rdd,與map不同的是添加了一個key
JavaPairDStream<String, Double> splitMess = jds.mapToPair(new PairFunction<String,String,Double>(){
            private static final long serialVersionUID = 1L;
            public Tuple2<String, Double> call(String line) throws Exception {
                // TODO Auto-generated method stub.
                String[] lineSplit = line.toString().split("::");
                Double followValue = Double.parseDouble(lineSplit[1])*0.8+Double.parseDouble(lineSplit[2])*0.6+Double.parseDouble(lineSplit[3])*1+Double.parseDouble(lineSplit[4])*1;
                return new Tuple2<String, Double>(lineSplit[0], followValue);
            }});

更新關注度值

由於是流式數據,數據每分每秒都在產生,那麼計算的關注值也在變化,那麼就需要更新這個狀態值。使用updateStateByKey來進行操作。這也是這裏相對比較難的知識點。

對初始化的DStream進行Transformation級別的處理,例如map、filter等高階函數等的編程,來進行具體的數據計算,

在這裏是通過updateStateByKey來以Batch Interval爲單位來對歷史狀態進行更新,在這裏需要使用checkPoint,用於保存父RDD的值。在Spark1.6.X之後也可以嘗試使用mapWithState來進行更新值。

JavaPairDStream<String, Double> UpdateFollowValue = splitMess.updateStateByKey(new Function2<List<Double>,Optional<Double>,Optional<Double>>(){

            public Optional<Double> call(List<Double> newValues,
                    Optional<Double> statValue) throws Exception {
                // 對相同的key進行value統計,實現累加
                Double updateValue = statValue.or(0.0);
                for (Double values : newValues) {
                    updateValue += values;
                }
                return Optional.of(updateValue);
            }},new HashPartitioner(jsc.sparkContext().defaultParallelism()));

輸出關注度值

結果輸出,並將裏面的商品進行關注度排序,降序排序,只顯示關注度最高的十個商品。實現思想,由於原RDD UpdateFollowValue的值

可以知道是的形式,我們使用sortByKey是不能這樣進行排序的,因爲它並不是按照關注度排序。我們需要將其轉化爲的形式,然後再按照sortByKey來進行排序,然後進行輸出。

UpdateFollowValue.foreachRDD(new VoidFunction<JavaPairRDD<String,Double>>(){
            private static final long serialVersionUID = 1L;
            public void call(JavaPairRDD<String, Double> followValue) throws Exception {
                // TODO Auto-generated method stub
                JavaPairRDD<Double,String> followValueSort = followValue.mapToPair(new PairFunction<Tuple2<String,Double>,Double,String>(){

                    public Tuple2<Double, String> call(
                            Tuple2<String, Double> valueToKey) throws Exception {
                        // TODO Auto-generated method stub
                        return new Tuple2<Double,String>(valueToKey._2,valueToKey._1);
                    }
                }).sortByKey(false);
                List<Tuple2<String,Double>> list = followValueSort.mapToPair(new PairFunction<Tuple2<Double,String>,String, Double>() {

                    public Tuple2<String, Double> call(
                            Tuple2<Double, String> arg0) throws Exception {
                        // TODO Auto-generated method stub
                        return new Tuple2<String,Double>(arg0._2,arg0._1);
                    }
                }).take(10);
                for (Tuple2<String,Double> tu : list) {
                    System.out.println("商品ID: "+tu._1+"  關注度: "+tu._2);
                }
            }});

附上最終StreamingGoods.java的代碼

package com.shiyanlou.simulator;

import java.io.Serializable;
import java.util.List;

import org.apache.spark.HashPartitioner;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.Function2;
import org.apache.spark.api.java.function.PairFunction;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.streaming.Duration;
import org.apache.spark.streaming.api.java.JavaDStream;
import org.apache.spark.streaming.api.java.JavaPairDStream;
import org.apache.spark.streaming.api.java.JavaReceiverInputDStream;
import org.apache.spark.streaming.api.java.JavaStreamingContext;

import scala.Tuple2;

import com.google.common.base.Optional;

public class StreamingGoods implements Serializable{
    private static final long serialVersionUID = 1L;
    //定義一個文件夾,用於保存上一個RDD的數據。該文件夾會自動創建,不需要提前創建
    private static String checkpointDir = "checkDir";
    public static void main(String[] args) {
        SparkConf sparkConf = new SparkConf().setAppName("StreamingGoods").setMaster("local[2]");
        JavaStreamingContext jsc = new JavaStreamingContext(sparkConf,new Duration(5000));
        jsc.checkpoint(checkpointDir);
        JavaReceiverInputDStream<String> jds = jsc.socketTextStream("127.0.0.1", 9999);
        JavaDStream<String> mess = jds.map(new Function<String,String>(){
            private static final long serialVersionUID = 1L;
            public String call(String arg0) throws Exception {
                // TODO Auto-generated method stub
                return arg0;
            }});
        mess.print();
        JavaPairDStream<String, Double> splitMess = jds.mapToPair(new PairFunction<String,String,Double>(){
            private static final long serialVersionUID = 1L;
            public Tuple2<String, Double> call(String line) throws Exception {
                // TODO Auto-generated method stub.
                String[] lineSplit = line.toString().split("::");
                Double followValue = Double.parseDouble(lineSplit[1])*0.8+Double.parseDouble(lineSplit[2])*0.6+Double.parseDouble(lineSplit[3])*1+Double.parseDouble(lineSplit[4])*1;
                return new Tuple2<String, Double>(lineSplit[0], followValue);
            }});
        JavaPairDStream<String, Double> UpdateFollowValue = splitMess.updateStateByKey(new Function2<List<Double>,Optional<Double>,Optional<Double>>(){

            public Optional<Double> call(List<Double> newValues,
                    Optional<Double> statValue) throws Exception {
                // TODO Auto-generated method stub
                Double updateValue = statValue.or(0.0);
                for (Double values : newValues) {
                    updateValue += values;
                }
                return Optional.of(updateValue);
            }},new HashPartitioner(jsc.sparkContext().defaultParallelism()));
        UpdateFollowValue.foreachRDD(new VoidFunction<JavaPairRDD<String,Double>>(){
            private static final long serialVersionUID = 1L;
            public void call(JavaPairRDD<String, Double> followValue) throws Exception {
                // TODO Auto-generated method stub
                JavaPairRDD<Double,String> followValueSort = followValue.mapToPair(new PairFunction<Tuple2<String,Double>,Double,String>(){

                    public Tuple2<Double, String> call(
                            Tuple2<String, Double> valueToKey) throws Exception {
                        // TODO Auto-generated method stub
                        return new Tuple2<Double,String>(valueToKey._2,valueToKey._1);
                    }
                }).sortByKey(false);
                List<Tuple2<String,Double>> list = followValueSort.mapToPair(new PairFunction<Tuple2<Double,String>,String, Double>() {

                    public Tuple2<String, Double> call(
                            Tuple2<Double, String> arg0) throws Exception {
                        // TODO Auto-generated method stub
                        return new Tuple2<String,Double>(arg0._2,arg0._1);
                    }
                }).take(10);
                for (Tuple2<String,Double> tu : list) {
                    System.out.println("商品ID: "+tu._1+"  關注度: "+tu._2);
                }
            }});

        jsc.start();
        jsc.awaitTermination();
    }
}

程序運行說明

1.先啓動模擬器,模擬數據。相當於服務端。

此處輸入圖片的描述

成功運行結果:

此處輸入圖片的描述

2.現在去運行Streaming程序,模擬器檢測到Streaming程序啓動(時間片開始),開始發送模擬數據。然後Stram ing端接收數據並計算。

此處輸入圖片的描述

模擬器開始發送數據:

此處輸入圖片的描述

Streaming接收到的數據:

此處輸入圖片的描述

Streaming計算結果:

此處輸入圖片的描述

此處輸入圖片的描述

我們刷新工程,看是否創建好checkDir

此處輸入圖片的描述

從下圖我們可以看到,已經自動創建了checkDir這個文件夾

此處輸入圖片的描述

0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章