接上篇文章第4章的4.3.3:电商推荐系统(上):推荐系统架构、数据模型、离线统计与机器学习推荐、历史热门商品、最近热门商品、商品平均得分统计推荐、基于隐语义模型的协同过滤推荐、用户商品推荐列表、商品相似度矩阵、模型评估和参数选取
第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 实时推荐算法的实现
实时推荐算法的前提:
- 在Redis集群中存储了每一个用户最近对商品的K次评分。实时算法可以快速获取。
- 离线推荐算法已经将商品相似度矩阵提前计算到了MongoDB中。
- 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,查看网页
附:经修改过的源码链接:https://download.csdn.net/download/qq_43265673/12920888
下载是免积分的,如果发现需要积分是由于CSDN自动生成累加的积分,可联系我免积分下载。