電商推薦系統(中):實時推薦服務、實時框架、實時推薦算法、獲取用戶的K次最近評分、商品推薦優先級、實時系統聯調、更新實時推薦結果

接上篇文章第4章的4.3.3:電商推薦系統(上):推薦系統架構、數據模型、離線統計與機器學習推薦、歷史熱門商品、最近熱門商品、商品平均得分統計推薦、基於隱語義模型的協同過濾推薦、用戶商品推薦列表、商品相似度矩陣、模型評估和參數選取

第5章 實時推薦服務建設

5.1 實時推薦服務

5.2 實時推薦模型和代碼框架

5.2.1 實時推薦模型算法設計

5.2.2 實時推薦模塊框架

5.3 實時推薦算法的實現

5.3.1 獲取用戶的K次最近評分

5.3.2 獲取當前商品最相似的K個商品

5.3.3 商品推薦優先級計算

5.3.4 將結果保存到mongoDB

5.3.5 更新實時推薦結果

5.4 實時系統聯調

5.4.1 啓動實時系統的基本組件

5.4.2 啓動zookeeper

5.4.3 啓動kafka

5.4.4 構建Kafka Streaming程序

5.4.5 配置並啓動flume

5.4.6 啓動業務系統後臺

第5章 實時推薦服務建設

5.1 實時推薦服務

實時計算與離線計算應用於推薦系統上最大的不同在於實時計算推薦結果應該反映最近一段時間用戶近期的偏好,而離線計算推薦結果則是根據用戶從第一次評分起的所有評分記錄來計算用戶總體的偏好。

用戶對物品的偏好隨着時間的推移總是會改變的。比如一個用戶u 在某時刻對商品p 給予了極高的評分,那麼在近期一段時候,u 極有可能很喜歡與商品p 類似的其他商品;而如果用戶u 在某時刻對商品q 給予了極低的評分,那麼在近期一段時候,u 極有可能不喜歡與商品q 類似的其他商品。所以對於實時推薦,當用戶對一個商品進行了評價後,用戶會希望推薦結果基於最近這幾次評分進行一定的更新,使得推薦結果匹配用戶近期的偏好,滿足用戶近期的口味。

如果實時推薦繼續採用離線推薦中的ALS 算法,由於算法運行時間巨大,不具有實時得到新的推薦結果的能力;並且由於算法本身的使用的是評分表,用戶本次評分後只更新了總評分表中的一項,使得算法運行後的推薦結果與用戶本次評分之前的推薦結果基本沒有多少差別,從而給用戶一種推薦結果一直沒變化的感覺,很影響用戶體驗。

另外,在實時推薦中由於時間性能上要滿足實時或者準實時的要求,所以算法的計算量不能太大,避免複雜、過多的計算造成用戶體驗的下降。鑑於此,推薦精度往往不會很高。實時推薦系統更關心推薦結果的動態變化能力,只要更新推薦結果的理由合理即可,至於推薦的精度要求則可以適當放寬。

所以對於實時推薦算法,主要有兩點需求:

(1)用戶本次評分後、或最近幾個評分後系統可以明顯的更新推薦結果;

(2)計算量不大,滿足響應時間上的實時或者準實時要求;

5.2 實時推薦模型和代碼框架

5.2.1 實時推薦模型算法設計

當用戶u 對商品p 進行了評分,將觸發一次對u 的推薦結果的更新。由於用戶u 對商品p 評分,對於用戶u 來說,他與p 最相似的商品們之間的推薦強度將發生變化,所以選取與商品p 最相似的K 個商品作爲候選商品。

每個候選商品按照“推薦優先級”這一權重作爲衡量這個商品被推薦給用戶u 的優先級。

這些商品將根據用戶u 最近的若干評分計算出各自對用戶u 的推薦優先級,然後與上次對用戶u 的實時推薦結果的進行基於推薦優先級的合併、替換得到更新後的推薦結果。

具體來說:

首先,獲取用戶u 按時間順序最近的K 個評分,記爲RK;獲取商品p 的最相似的K 個商品集合,記爲S;

然後,對於每個商品q S ,計算其推薦優先級  ,計算公式如下:

    其中:

 表示用戶u 對商品r 的評分;

sim(q,r)表示商品q 與商品r 的相似度,設定最小相似度爲0.6,當商品q和商品r 相似度低於0.6 的閾值,則視爲兩者不相關並忽略;

sim_sum 表示q 與RK 中商品相似度大於最小閾值的個數;

incount 表示RK 中與商品q 相似的、且本身評分較高(>=3)的商品個數;

recount 表示RK 中與商品q 相似的、且本身評分較低(<3)的商品個數;

公式的意義如下:

首先對於每個候選商品q,從u 最近的K 個評分中,找出與q 相似度較高(>=0.6)的u 已評分商品們,對於這些商品們中的每個商品r,將r 與q 的相似度乘以用戶u 對r 的評分,將這些乘積計算平均數,作爲用戶u 對商品q 的評分預測即

然後,將u 最近的K 個評分中與商品q 相似的、且本身評分較高(>=3)的商品個數記爲 incount,計算lgmax{incount,1}作爲商品 q 的“增強因子”,意義在於商品q 與u 的最近K 個評分中的n 個高評分(>=3)商品相似,則商品q 的優先級被增加lgmax{incount,1}。如果商品 q 與 u 的最近 K 個評分中相似的高評分商品越多,也就是說n 越大,則商品q 更應該被推薦,所以推薦優先級被增強的幅度較大;如果商品q 與u 的最近K 個評分中相似的高評分商品越少,也就是n 越小,則推薦優先級被增強的幅度較小;

而後,將u 最近的K 個評分中與商品q 相似的、且本身評分較低(<3)的商品個數記爲 recount,計算lgmax{recount,1}作爲商品 q 的“削弱因子”,意義在於商品q 與u 的最近K 個評分中的n 個低評分(<3)商品相似,則商品q 的優先級被削減lgmax{incount,1}。如果商品 q 與 u 的最近 K 個評分中相似的低評分商品越多,也就是說n 越大,則商品q 更不應該被推薦,所以推薦優先級被減弱的幅度較大;如果商品q 與u 的最近K 個評分中相似的低評分商品越少,也就是n 越小,則推薦優先級被減弱的幅度較小;

最後,將增強因子增加到上述的預測評分中,並減去削弱因子,得到最終的q 商品對於u 的推薦優先級。在計算完每個候選商品q 的 後,將生成一組<商品q 的ID, q 的推薦優先級>的列表updatedList:

而在本次爲用戶u 實時推薦之前的上一次實時推薦結果Rec 也是一組<商品m,m 的推薦優先級>的列表,其大小也爲K:

接下來,將updated_S 與本次爲u 實時推薦之前的上一次實時推薦結果Rec進行基於合併、替換形成新的推薦結果NewRec:

其中,i表示updated_S 與Rec 的商品集合中的每個商品,topK 是一個函數,表示從 Rec updated _ S中選擇出最大的 K 個商品,cmp =   表示topK 函數將推薦優先級 值最大的K 個商品選出來。最終,NewRec 即爲經過用戶u 對商品p 評分後觸發的實時推薦得到的最新推薦結果。

總之,實時推薦算法流程流程基本如下:

(1)用戶u 對商品p 進行了評分,觸發了實時推薦的一次計算;

(2)選出商品p 最相似的K 個商品作爲集合S;

(3)獲取用戶u 最近時間內的K 條評分,包含本次評分,作爲集合RK;

(4)計算商品的推薦優先級,產生<qID,>集合updated_S;

將updated_S 與上次對用戶u 的推薦結果Rec 利用公式(4-4)進行合併,產生新的推薦結果NewRec;作爲最終輸出。

5.2.2 實時推薦模塊框架

我們在recommender下新建子項目StreamingRecommender,引入spark、scala、mongo、redis和kafka的依賴:

<dependencies>
    <!-- Spark的依賴引入 -->
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-core_2.11</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-sql_2.11</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.spark</groupId>
       <artifactId>spark-streaming_2.11</artifactId>
    </dependency>
    <!-- 引入Scala -->
    <dependency>
        <groupId>org.scala-lang</groupId>
        <artifactId>scala-library</artifactId>
    </dependency>
    <!-- 加入MongoDB的驅動 -->
    <!-- 用於代碼方式連接MongoDB -->
    <dependency>
        <groupId>org.mongodb</groupId>
        <artifactId>casbah-core_2.11</artifactId>
        <version>${casbah.version}</version>
    </dependency>
    <!-- 用於Spark和MongoDB的對接 -->
    <dependency>
        <groupId>org.mongodb.spark</groupId>
        <artifactId>mongo-spark-connector_2.11</artifactId>
        <version>${mongodb-spark.version}</version>
    </dependency>
    <!-- redis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
    <!-- kafka -->
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-clients</artifactId>
        <version>0.10.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
        <version>${spark.version}</version>
    </dependency>
</dependencies>

代碼中首先定義樣例類和一個連接助手對象(用於建立redis和mongo連接),並在StreamingRecommender中定義一些常量:

src/main/scala/com.atguigu.streaming/StreamingRecommender.scala

// 連接助手對象
object ConnHelper extends Serializable{
  lazy val jedis = new Jedis("localhost")
  lazy val mongoClient = MongoClient(MongoClientURI("mongodb://localhost:27017/recommender"))
}
case class MongConfig(uri:String,db:String)
// 標準推薦
case class Recommendation(productId:Int, score:Double)
// 用戶的推薦
case class UserRecs(userId:Int, recs:Seq[Recommendation])
//商品的相似度
case class ProductRecs(productId:Int, recs:Seq[Recommendation])
object StreamingRecommender {
  val MAX_USER_RATINGS_NUM = 20
  val MAX_SIM_PRODUCTS_NUM = 20
  val MONGODB_STREAM_RECS_COLLECTION = "StreamRecs"
  val MONGODB_RATING_COLLECTION = "Rating"
  val MONGODB_PRODUCT_RECS_COLLECTION = "ProductRecs"
//入口方法
def main(args: Array[String]): Unit = {
}
}

實時推薦主體代碼如下:

def main(args: Array[String]): Unit = {
  val config = Map(
    "spark.cores" -> "local[*]",
    "mongo.uri" -> "mongodb://localhost:27017/recommender",
    "mongo.db" -> "recommender",
    "kafka.topic" -> "recommender"
  )
  //創建一個SparkConf配置
  val sparkConf = new SparkConf().setAppName("StreamingRecommender").setMaster(config("spark.cores"))
  val spark = SparkSession.builder().config(sparkConf).getOrCreate()
  val sc = spark.sparkContext
  val ssc = new StreamingContext(sc,Seconds(2))
  implicit val mongConfig = MongConfig(config("mongo.uri"),config("mongo.db"))
  import spark.implicits._
  // 廣播商品相似度矩陣
  //裝換成爲 Map[Int, Map[Int,Double]]
  val simProductsMatrix = spark
    .read
    .option("uri",config("mongo.uri"))
    .option("collection",MONGODB_PRODUCT_RECS_COLLECTION)
    .format("com.mongodb.spark.sql")
    .load()
    .as[ProductRecs]   
    .rdd
    .map{recs =>
      (recs.productId,recs.recs.map(x=> (x.productId,x.score)).toMap)
    }.collectAsMap()  
  val simProductsMatrixBroadCast = sc.broadcast(simProductsMatrix)
  //創建到Kafka的連接
  val kafkaPara = Map(
    "bootstrap.servers" -> "localhost:9092",
    "key.deserializer" -> classOf[StringDeserializer],
    "value.deserializer" -> classOf[StringDeserializer],
    "group.id" -> "recommender",
    "auto.offset.reset" -> "latest"
  )
  val kafkaStream = KafkaUtils.createDirectStream[String,String](ssc,LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String,String](Array(config("kafka.topic")),kafkaPara))
  // UID|MID|SCORE|TIMESTAMP
  // 產生評分
  val ratingStream = kafkaStream.map{case msg=>
    var attr = msg.value().split("\\|")
    (attr(0).toInt,attr(1).toInt,attr(2).toDouble,attr(3).toInt)
  }
// 核心實時推薦算法
  ratingStream.foreachRDD{rdd =>
    rdd.map{case (userId,productId,score,timestamp) =>
      println(">>>>>>>>>>>>>>>>")
      //獲取當前最近的M次商品評分
      val userRecentlyRatings = getUserRecentlyRating(MAX_USER_RATINGS_NUM,userId,ConnHelper.jedis)
      //獲取商品P最相似的K個商品
      val simProducts = getTopSimProducts(MAX_SIM_PRODUCTS_NUM,productId,userId,simProductsMatrixBroadCast.value)
      //計算待選商品的推薦優先級
      val streamRecs = computeProductScores(simProductsMatrixBroadCast.value,userRecentlyRatings,simProducts)
      //將數據保存到MongoDB
      saveRecsToMongoDB(userId,streamRecs)
    }.count()
  }
  //啓動Streaming程序
  ssc.start()
  ssc.awaitTermination()
}

5.3 實時推薦算法的實現

實時推薦算法的前提:

  1. 在Redis集羣中存儲了每一個用戶最近對商品的K次評分。實時算法可以快速獲取。
  2. 離線推薦算法已經將商品相似度矩陣提前計算到了MongoDB中。
  3. Kafka已經獲取到了用戶實時的評分數據。

算法過程如下:

實時推薦算法輸入爲一個評分<userId, productId, rate, timestamp>,而執行的核心內容包括:獲取userId 最近K 次評分、獲取productId 最相似K 個商品、計算候選商品的推薦優先級、更新對userId 的實時推薦結果。

5.3.1 獲取用戶的K次最近評分

業務服務器在接收用戶評分的時候,默認會將該評分情況以userId, productId, rate, timestamp的格式插入到Redis中該用戶對應的隊列當中,在實時算法中,只需要通過Redis客戶端獲取相對應的隊列內容即可。

import scala.collection.JavaConversions._
/**
  * 獲取當前最近的M次商品評分
  * @param num  評分的個數
  * @param userId  誰的評分
  * @return
  */
def getUserRecentlyRating(num:Int, userId:Int,jedis:Jedis): Array[(Int,Double)] ={
  //從用戶的隊列中取出num個評分
  jedis.lrange("userId:"+userId.toString, 0, num).map{item =>
    val attr = item.split("\\:")
    (attr(0).trim.toInt, attr(1).trim.toDouble)
  }.toArray
}

5.3.2 獲取當前商品最相似的K個商品

在離線算法中,已經預先將商品的相似度矩陣進行了計算,所以每個商品productId 的最相似的K 個商品很容易獲取:從MongoDB中讀取ProductRecs數據,從productId 在simHash 對應的子哈希表中獲取相似度前K 大的那些商品。輸出是數據類型爲Array[Int]的數組,表示與productId 最相似的商品集合,並命名爲candidateProducts 以作爲候選商品集合。

/**
  * 獲取當前商品K個相似的商品
  * @param num          相似商品的數量
  * @param productId          當前商品的ID
  * @param userId          當前的評分用戶
  * @param simProducts    商品相似度矩陣的廣播變量值
  * @param mongConfig   MongoDB的配置
  * @return
  */

def getTopSimProducts(num:Int, productId:Int, userId:Int, simProducts:scala.collection.Map[Int,scala.collection.immutable.Map[Int,Double]])(implicit mongConfig: MongConfig): Array[Int] ={
  //從廣播變量的商品相似度矩陣中獲取當前商品所有的相似商品
  val allSimProducts = simProducts.get(productId).get.toArray
  //獲取用戶已經觀看過得商品
  val ratingExist = ConnHelper.mongoClient(mongConfig.db)(MONGODB_RATING_COLLECTION).find(MongoDBObject("userId" -> userId)).toArray.map{item =>
    item.get("productId").toString.toInt
  }
  //過濾掉已經評分過得商品,並排序輸出
  allSimProducts.filter(x => !ratingExist.contains(x._1)).sortWith(_._2 > _._2).take(num).map(x => x._1)

}

5.3.3 商品推薦優先級計算

對於候選商品集合simiHash和userId 的最近K 個評分recentRatings,算法代碼內容如下:

/**
  * 計算待選商品的推薦分數
  * @param simProducts            商品相似度矩陣
  * @param userRecentlyRatings  用戶最近的k次評分
  * @param topSimProducts         當前商品最相似的K個商品
  * @return
  */
def computeProductScores(          simProducts:scala.collection.Map[Int,scala.collection.immutable.Map[Int,Doub          le]],userRecentlyRatings:Array[(Int,Double)],topSimProducts: Array[Int]):           Array[(Int,Double)] ={
  //用於保存每一個待選商品和最近評分的每一個商品的權重得分
  val score = scala.collection.mutable.ArrayBuffer[(Int,Double)]()
  //用於保存每一個商品的增強因子數
  val increMap = scala.collection.mutable.HashMap[Int,Int]()
  //用於保存每一個商品的減弱因子數
  val decreMap = scala.collection.mutable.HashMap[Int,Int]()
  for (topSimProduct <- topSimProducts; userRecentlyRating <- userRecentlyRatings){
    val simScore = getProductsSimScore(simProducts,userRecentlyRating._1,topSimProduct)
    if(simScore > 0.6){
      score += ((topSimProduct, simScore * userRecentlyRating._2 ))
      if(userRecentlyRating._2 > 3){
        increMap(topSimProduct) = increMap.getOrDefault(topSimProduct,0) + 1
      }else{
        decreMap(topSimProduct) = decreMap.getOrDefault(topSimProduct,0) + 1
      }
    }
  }
  score.groupBy(_._1).map{case (productId,sims) =>
    (productId,sims.map(_._2).sum / sims.length + log(increMap.getOrDefault(productId, 1)) - log(decreMap.getOrDefault(productId, 1)))
  }.toArray.sortWith(_._2>_._2)
}

其中,getProductSimScore是取候選商品和已評分商品的相似度,代碼如下:

/**
  * 獲取當個商品之間的相似度
  * @param simProducts       商品相似度矩陣
  * @param userRatingProduct 用戶已經評分的商品
  * @param topSimProduct     候選商品
  * @return
  */
def getProductsSimScore(
simProducts:scala.collection.Map[Int,scala.collection.immutable.Map[Int,Double]], userRatingProduct:Int, topSimProduct:Int): Double ={
  simProducts.get(topSimProduct) match {
    case Some(sim) => sim.get(userRatingProduct) match {
      case Some(score) => score
      case None => 0.0
    }
    case None => 0.0
  }
}

而log是對數運算,這裏實現爲取10的對數(常用對數):

//取10的對數
def log(m:Int):Double ={
  math.log(m) / math.log(10)
}

5.3.4 將結果保存到mongoDB

saveRecsToMongoDB函數實現了結果的保存:

/**
  * 將數據保存到MongoDB    userId -> 1,  recs -> 22:4.5|45:3.8
  * @param streamRecs  流式的推薦結果
  * @param mongConfig  MongoDB的配置
  */

def saveRecsToMongoDB(userId:Int,streamRecs:Array[(Int,Double)])(implicit mongConfig: MongConfig): Unit ={
  //到StreamRecs的連接
  val streaRecsCollection = ConnHelper.mongoClient(mongConfig.db)(MONGODB_STREAM_RECS_COLLECTION)
  streaRecsCollection.findAndRemove(MongoDBObject("userId" -> userId))
  streaRecsCollection.insert(MongoDBObject("userId" -> userId, "recs" ->          streamRecs.map( x => MongoDBObject("productId"->x._1,"score"->x._2)) ))
}

5.3.5 更新實時推薦結果

當計算出候選商品的推薦優先級的數組updatedRecommends<productId, E>後,這個數組將被髮送到Web 後臺服務器,與後臺服務器上userId 的上次實時推薦結果recentRecommends<productId, E>進行合併、替換並選出優先級E 前K大的商品作爲本次新的實時推薦。具體而言:

a.合併:將updatedRecommends 與recentRecommends 並集合成爲一個新的<productId, E>數組;

b.替換(去重):當updatedRecommends 與recentRecommends 有重複的商品productId 時,recentRecommends 中productId 的推薦優先級由於是上次實時推薦的結果,於是將作廢,被替換成代表了更新後的updatedRecommends的productId 的推薦優先級;

c.選取TopK:在合併、替換後的<productId, E>數組上,根據每個product 的推薦優先級,選擇出前K 大的商品,作爲本次實時推薦的最終結果。

5.4 實時系統聯調

我們的系統實時推薦的數據流向是:業務系統 -> 日誌 -> flume 日誌採集 -> kafka streaming數據清洗和預處理 -> spark streaming 流式計算。在我們完成實時推薦服務的代碼後,應該與其它工具進行聯調測試,確保系統正常運行。

5.4.1 啓動實時系統的基本組件

啓動實時推薦系統StreamingRecommender以及mongodb、redis

5.4.2 啓動zookeeper

bin/zkServer.sh start

5.4.3 啓動kafka

bin/kafka-server-start.sh -daemon ./config/server.properties

測試筆記:

手動啓動kafka進行測試

/opt/module/kafka/bin/kafka-console-producer.sh --broker-list hadoop105:9092 --topic recommender

給redis添加用戶對5個商品評分數據

使用linux命令date +%s生成一個以秒爲單位的時間戳

成產一條數據4867|8195|4.0|1569489404

控制檯報異常:redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: Connection refused: connect

異常原因:Redis只能被本機訪問而不能被其他ip地址訪問

解決辦法:打開Redis安裝目錄下的redis.conf文件,”bind 127.0.0.1”註釋掉

退出保護模式

重啓redis服務,再次執行程序查看控制檯

查看MongoDB

查看StreamRecs

5.4.4 構建Kafka Streaming程序

在recommender下新建module,KafkaStreaming,主要用來做日誌數據的預處理,過濾出需要的內容。pom.xml文件需要引入依賴:

<dependencies>
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-streams</artifactId>
        <version>0.10.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-clients</artifactId>
        <version>0.10.2.1</version>
    </dependency>
</dependencies>

<build>
    <finalName>kafkastream</finalName>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <archive>
                    <manifest>
                        <mainClass>com.atguigu.kafkastream.Application</mainClass>
                    </manifest>
                </archive>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
            <executions>
                <execution>
                    <id>make-assembly</id>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

在src/main/java下新建java類com.atguigu.kafkastreaming.Application

public class Application {
    public static void main(String[] args){
        String brokers = "localhost:9092";
        String zookeepers = "localhost:2181";
        // 定義輸入和輸出的topic
        String from = "log";
        String to = "recommender";
        // 定義kafka streaming的配置
        Properties settings = new Properties();
        settings.put(StreamsConfig.APPLICATION_ID_CONFIG, "logFilter");
        settings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
        settings.put(StreamsConfig.ZOOKEEPER_CONNECT_CONFIG, zookeepers);
        StreamsConfig config = new StreamsConfig(settings);
        // 拓撲建構器
        TopologyBuilder builder = new TopologyBuilder();
        // 定義流處理的拓撲結構
        builder.addSource("SOURCE", from)
                .addProcessor("PROCESS", () -> new LogProcessor(), "SOURCE")
                .addSink("SINK", to, "PROCESS");
        KafkaStreams streams = new KafkaStreams(builder, config);
        streams.start();
    }
}

這個程序會將topic爲“log”的信息流獲取來做處理,並以“recommender”爲新的topic轉發出去。

流處理程序 LogProcess.java

public class LogProcessor implements Processor<byte[],byte[]> {
    private ProcessorContext context;
    public void init(ProcessorContext context) {
        this.context = context;
    }
    public void process(byte[] dummy, byte[] line) {
        String input = new String(line);
        // 根據前綴過濾日誌信息,提取後面的內容
        if(input.contains("PRODUCT_RATING_PREFIX:")){
            System.out.println("product rating coming!!!!" + input);
            input = input.split("PRODUCT_RATING_PREFIX:")[1].trim();
            context.forward("logProcessor".getBytes(), input.getBytes());
        }
    }
    public void punctuate(long timestamp) {
    }
    public void close() {
    }
}

完成代碼後,啓動Application。

測試筆記:

Application代碼:
package com.atguigu.KafkaStream;

import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.processor.TopologyBuilder;

import java.util.Properties;

/**
 * @author cherry
 * @create 2019-09-26-21:05
 */
public class Application {
    public static void main(String[] args) {
        String brokers = "hadoop105:9092";
        String zookeepers = "hadoop105:2183";
        //定義輸入和輸出的topic
        String from = "log";
        String to = "recommender";
        //定義kafka stream配套參數
        Properties settings = new Properties();
        settings.put(StreamsConfig.APPLICATION_ID_CONFIG, "logFilter");
        settings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
        settings.put(StreamsConfig.ZOOKEEPER_CONNECT_CONFIG, zookeepers);
        //創建kafka stream配置對象
        StreamsConfig config = new StreamsConfig(settings);
        //定義拓撲構建器
        TopologyBuilder builder = new TopologyBuilder();
        builder.addSource("SOURCE", from).addProcessor("PROCESSOR", LogProcessor::new, "SOURCE")
                .addSink("SINK", to, "PROCESSOR");
        //創建kafka stream
        KafkaStreams streams = new KafkaStreams(builder, config);
        streams.start();
        System.out.println("kafka stream started!");
    }
}
LogProcessor代碼
package com.atguigu.KafkaStream;

import org.apache.kafka.streams.processor.Processor;
import org.apache.kafka.streams.processor.ProcessorContext;


/**
 * @author cherry
 * @create 2019-09-26-21:16
 */
public class LogProcessor implements Processor<byte[], byte[]> {
    private ProcessorContext context;

    @Override
    public void init(ProcessorContext processorContext) {
        this.context = processorContext;
    }

    @Override
    public void process(byte[] dummy, byte[] line) {
        //核心處理流程
        String input = new String(line);
        //提取數據,以固定的前綴過濾日誌信息
        if (input.contains("PRODUCT_RATING_PREFIX:")) {
            System.out.println("product rating data coming!" + input);
            input=input.split("PRODUCT_RATING_PREFIX:")[1].trim();
            context.forward("logProcessor".getBytes(),input.getBytes());
        }
    }

    @Override
    public void punctuate(long l) {

    }
    @Override
    public void close() {
    }
}
運行測試並查看控制檯

 

5.4.5 配置並啓動flume

在flume的conf目錄下新建log-kafka.properties,對flume連接kafka做配置:

agent.sources = exectail
agent.channels = memoryChannel
agent.sinks = kafkasink

# For each one of the sources, the type is defined
agent.sources.exectail.type = exec
# 下面這個路徑是需要收集日誌的絕對路徑,改爲自己的日誌目錄
agent.sources.exectail.command = tail –f
/mnt/d/Projects/BigData/ECommerceRecommenderSystem/businessServer/src/main/log/agent.log
agent.sources.exectail.interceptors=i1
agent.sources.exectail.interceptors.i1.type=regex_filter
# 定義日誌過濾前綴的正則
agent.sources.exectail.interceptors.i1.regex=.+PRODUCT_RATING_PREFIX.+
# The channel can be defined as follows.
agent.sources.exectail.channels = memoryChannel

# Each sink's type must be defined
agent.sinks.kafkasink.type = org.apache.flume.sink.kafka.KafkaSink
agent.sinks.kafkasink.kafka.topic = log
agent.sinks.kafkasink.kafka.bootstrap.servers = localhost:9092
agent.sinks.kafkasink.kafka.producer.acks = 1
agent.sinks.kafkasink.kafka.flumeBatchSize = 20

#Specify the channel the sink should use
agent.sinks.kafkasink.channel = memoryChannel

# Each channel's type is defined.
agent.channels.memoryChannel.type = memory

# Other config values specific to each type of channel(sink or source)
# can be defined as well
# In this case, it specifies the capacity of the memory channel
agent.channels.memoryChannel.capacity = 10000

配置好後,啓動flume:

./bin/flume-ng agent -c ./conf/ -f ./conf/log-kafka.properties -n agent -Dflume.root.logger=INFO,console

5.4.6 啓動業務系統後臺

將業務代碼加入系統中。注意在src/main/resources/ 下的 log4j.properties中,log4j.appender.file.File的值應該替換爲自己的日誌目錄,與flume中的配置應該相同。

啓動業務系統後臺,訪問localhost:8088/index.html;點擊某個商品進行評分,查看實時推薦列表是否會發生變化。

筆記:

點擊idea右上角Maven運行tomcat

查看控制檯

遇到的問題:問題1.MongoDB突然拒絕連接,報com.mongodb.MongoSocketOpenException: Exception opening socket異常

解決辦法:由於之前能正常連接MongoDB,因此不會是防火牆及mongodb.conf配置文件的原因,檢查一下businessServer/src/main/resources/recommend.properties文件,果然是host寫錯,糾正後正常運行.

問題2.從BigInt轉換成Int類型可能出現截斷,報Exception in thread "main" org.apache.spark.sql.AnalysisException: Cannot up cast `timestamp` from bigint to int as it may truncate異常

出錯原因:覺得當前時間戳在Int範圍內,需要將Int類型的timestamp全部改成BigInt類型

登錄後查看,只有統計推薦,而沒有實時推薦和熱門推薦

原因:冷啓動,後面解決

嘗試給其中一件商品評分

查看agent.log文件發現有一個評分記錄

查看MongoDB新增的User

查看Rating表中該userId的數據,發現評分一應被插入MongoDB

執行OfflineRecommender下的OfflineRecommender.scala發現離線推薦模塊已更新

啓動kafka stream(Application.java文件),再次對另一件商品評分

重新運行OfflineRecommender,查看網頁

 

 

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