五、DWD层处理
写入到ods层的数据是一张表一个topic
将事实表和维度表,join后将数据写到es里面
dwd层是数据明细层, 存储事实表的明细数据和维护维度表, 并把事实表和维度表做join, 把维度冗余到事实表中.
5.1 判断首单业务的策略分析
--事实表只要订单表
--维度表需要六张
如何判断某单是该用户的首单?
存储用户首单状态:
1.数据存储周期的长短:长。 数据量大:(hive,hbase,es)
2.数据的状态的能改变。(hbase,es)
3.从查询方便来说。K_V (hbase,redis)
4.实时响应。 (habse,redis,es,mysql)
把存户用户是否首单的状态:用Hbase + Phoenix
--hbase存状态,es存数据
判断是否首单的要点,在于该用户之前是否参与过消费(下单)
那么如何知道用户之前是否参与过消费,如果临时从所有消费记录中查询,是非常不现实的。那么只有将“用户是否消费过”这个状态进行保存并长期维护起来。在有需要的时候通过用户id进行关联查询。
在实际生产中,这种用户状态是非常常见的比如“用户是否退过单”、“用户是否投过诉”、“用户是否是高净值用户”等等。
那么现在问题就变为,如何保存并长期维护这种状态? 考虑到
-
这是一个保存周期较长的数据, 数据量较大(hive, hbase, es)。
-
必须可修改状态值(hbase, es)。
-
查询方便应该是k-v模式的查询(redis, hbase)。
-
能满足实时读取.(hbase, es)
所以综上这几点比较适合保存在****Hbase****中。
5.2 首单分析的前期准备
先写事实表
5.2.1 添加样例类OrderInfo
package com.atguigu.realtime.bean
import java.text.SimpleDateFormat
case class UserInfo(id: String,
user_level: String,
birthday: String,
gender: String, // F M
var age_group: String = null, //年龄段
var gender_name: String = null) { //性别 男 女
// 计算年龄段
val age = (System.currentTimeMillis() - new SimpleDateFormat("yyyy-MM-dd").parse(birthday).getTime) / 1000 / 60 / 60 / 24 / 365
age_group = if (age <= 20) "20岁及以下" else if (age <= 30) "21岁到 30 岁" else "30岁及以上"
// 计算gender_name
gender_name = if (gender == "F") "女" else "男"
}
5.2.2 创建DwdOrderInfoApp类(没有维度表信息)
测试时,要先将ods层数据开启
思路
--经过过滤,只剩下用户首单,把首单的记录写到es中
定义一个DwdOrderInfoApp类继承BaseApp
--sourceStream.map(str=>{
拿到的数据是json字符串,向将其封装成一个样例类
使用json4s ——————> json4s解析的时候必须类型完全一致才能解析
所以需要自定义一个格式化,告诉json4s如何把JString转成Long
'将这个属性放到BaseApp抽象类中'
"========================================================================================================"
val toLong: CustomSerializer[Long] = new CustomSerializer[Long](ser = format => ({
case JString(s) => s.toLong
case JInt(s) => s.toLong
},{
case s:Long => JLong(s)
}))
val toDouble = new CustomSerializer[Double](ser = format => ({
case JString(s) => s.toDouble
case JDouble(s) => s.toDouble
},{
case s:Long => JDouble(s)
}))
"========================================================================================================"
--已经解析出订单表数据
implicit val f = org.json4s.DefaultFormats + toDouble +toLong
JsonMethods.parse(str).extract[OrderInfo]
--})
代码
package com.atguigu.realtime.dwd
import com.atguigu.realtime.BaseApp
import com.atguigu.realtime.bean.OrderInfo
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.kafka010.OffsetRange
import org.json4s.{CustomSerializer, JsonAST}
import org.json4s.JsonAST.JString
import org.json4s.jackson.JsonMethods
import scala.collection.mutable.ListBuffer
object DwdOrderInfoApp extends BaseApp{
override val master: String = "local[2]"
override val appName: String = "DwdOrderInfoApp"
override val groupId: String = "DwdOrderInfoApp"
override val topic: String = "ods_order_info"
override val bachTime: Int = 3
override def run(ssc: StreamingContext,
offsetRanges: ListBuffer[OffsetRange],
sourceStream: DStream[String]): Unit = {
//自定义一个格式化,告诉json4s如何把JString转成Long
new CustomSerializer[String](format => ({
case JString(s) => s
},{
case s :String => JsonAST.JLong(s.toLong)
}
))
sourceStream
.map(str => {
implicit val f = org.json4s.DefaultFormats + toDouble +toLong
JsonMethods.parse(str).extract[OrderInfo]
})
}
}
问题:事实表的数据,如何补齐维度数据?
事实表的数据,如何补齐维度数据?
维度表的数据,和实时表的数据的区别?
张三下一个单
order_info 中只有用户的id,用户年龄级别等是缺失
维度表的数据一般是遭遇事实表
流中只能事实表数据,补齐维度表数据?
1.使用user_id取mysql反查用户信息
坏处:对mysql的影响特别大
2.将user_info的同步到hbase中
再写维度表
前期准备
5.2.3 维度表的构建思路
维度表数据的生成一般都早于事实表. 在事实表连接维度表的时候, 一般需要查找全部维度表才能关联.
所以维度表数据直接放在kafka不合适, kafka只能顺序消费, 没有查找能力. 考虑到有些维度表数据量也比较大, 所以放在hbase比较合适.
维度变化比较缓慢, 但是也会有一定的变化. 需要监控维度表的变化, 做成实时维度表.
实时维度表制作架构如下:
5.2.4 升级版——(一个流消费多个Topic)
5.2.4.1 MyKafkaUtil_1方法
思路
--修改
1.将topics: Seq[String],定义成多个String类型
2. Subscribe[String, String](topics, kafkaParams, offsets)
3.offsets注意一下Map[TopicPartition, Long]这个不用改
但比如:
--问:
0分区有两个topic,每个topic都有0分区,那么这个Map的topicPartition到底是谁的0分区呢? --答:
有可能是topicA的也有可能是topicB的0分区
--topicPartition里面既有partition也有topic,不用担心出现混乱,但offset会出现情况
代码
/**
* 把多个topic放到一个流中
* @param ssc
* @param groupId
* @param topics
* @param offsets
* @return
*/
def getKafkaStream(ssc: StreamingContext,
groupId: String,
topics: Seq[String],
//这里将offsets设置成了一个不可变的Map
offsets: Map[TopicPartition, Long]) = {
kafkaParams += "group.id" -> groupId
kafkaParams += "auto.offset.reset" -> "earliest" //如果没有读到上次的位置,则会从最早的位置开始消费
kafkaParams += "enable.auto.commit" -> (false: java.lang.Boolean) //需要手动维护offsets
KafkaUtils
.createDirectStream[String, String](
ssc,
PreferConsistent,
Subscribe[String, String](topics, kafkaParams, offsets)
)
//.map(_.value()) 只有从kafka直接得到的流才有offset的信息,map之后就没了
}
5.2.4.2 升级OffsetManager方法
思路
【写:】
"topic -> (topic , Map{(partition0,offset0),(partition1,offset1)}"
--保存多个offset到redis,saveOffsets
1.获取redis客户端
2.获取key值,就是topic,这个key值封装了多个topic
3.将同一个topic的多个分区分到一组
4.使用map操作,操作同一个topic的多个分区封装成的一个迭代器
5.获取迭代器里的offsetRAange里的分区和偏移量,组成一个Map[Int, Long]
6.将这个Map封装成String类型,
7.返回一个Map集合fieldAndvalue格式为(topic,Serialization.write(value))
8.将fieldAndvalue转成java的Map
9.将topic -> Map{ partition -> offset }封装进redis
val key = s"offset:${groupId}"
-- topic -> it: ListBuffer[OffsetRange]
-- 一个topic下的所有分区都在it里面
'====================写入的最后格式========================='
key: offset:xxxAPp
value: hash
field value 【分区 -> 偏移量】
【key是topic】ods_xxx {1 -> 1000, 0 -> 2000}
'========================================================='
【读:】
"topic -> (topic , Map{(partition0,offset0),(partition1,offset1)}"
--从redis读取多个offset,readOffsets
1.将需要读取的某个topic封装
2.获取redis客户端
3.将获取的hash集合里的所有key,就是topic,对应的value,就是(topic,partitionAndOffset)
4.将读取到的所有key组成的集合转成scala的Map集合
5.对读取到的集合做flatMap操作
partitionAndOffset格式如下
6.解析这个value的格式: {1 -> 1000, 0 -> 2000}
7.将这个map用json格式解析
8.遍历这个json的每个对象
{1 -> 1000, 0 -> 2000}
9.将{partition -> offset格式变成(topic,partition) -> offset 格式
10(topic,partition) -> offset 格式输出
11.关闭客户端
12.返回读取到的该topic对应的value,topicPartitionAndOffset
代码
/**
* 保存多个offset到redis
* @param offsetRanges
* @param groupId
* @param topics
----------------------------------------------------------
key: offset:xxxAPp
value: hash
field value 【分区 -> 偏移量】
【key是topic】ods_xxx {1 -> 1000, 0 -> 2000}
----------------------------------------------------------
*/
"topic -> (topic , Map{(partition0,offset0),(partition1,offset1)}"
def saveOffsets(offsetRanges: ListBuffer[OffsetRange], groupId: String, topics:Seq[String]) = {
//1.获取redis客户端
val client: Jedis = MyRedisUtil.getClient
//2.获取key值,就是topic,这个key值封装了多个topic
val key = s"offset:${groupId}" //会消费多个topic
val fieldAndvalue = offsetRanges
//3.将同一topic的多个分区分到一组
.groupBy(_.topic)
//4.使用map操作,操作同一个topic的多个分区封装成的一个迭代器
.map{
//topic -> it: ListBuffer[OffsetRange]
//一个topic下的所有分区都在it这个迭代器里面
case (topic,it: ListBuffer[OffsetRange]) =>
implicit val f: DefaultFormats.type = org.json4s.DefaultFormats
//需要将ListBuffer做成Map,想做Map必须先做元组
val value: Map[Int, Long] = it
//5.获取迭代器里的offsetRAange里的分区和偏移量,组成一个Map[Int, Long]
.map(offsetRange => (offsetRange.partition,offsetRange.untilOffset))
//转成不可变Map
.toMap
//6.将这个Map封装成String类型,再跟topic一起返回
//7.返回一个Map集合fieldAndvalue格式为(topic,Serialization.write(value))
(topic,Serialization.write(value))
//topicPartitionAndOffset = topic -> ( topic -> Map{partition,offset} )
}
//8.将fieldAndvalue转成java的Map
.asJava
//9.将topic -> Map( partition,offset )封装进redis
client.hmset(key, fieldAndvalue)
println("多个topic一个流_保存偏移量 topic_partition-> offset: " + fieldAndvalue)
client.close()
}
/**
* 从redis读取多个offset
----------------------------------------------------------
key: offset:xxxAPp
value: hash
field value 【分区 -> 偏移量】
【key是topic】ods_xxx {1 -> 1000, 0 -> 2000}
----------------------------------------------------------
*/
def readOffsets(groupId: String, topic: Seq[String]) = {
//1.将需要读取的某个topic封装
val key = s"offset:${groupId}"
//2.获取redis客户端
val client = MyRedisUtil.getClient
println("读取开始的offset")
val topicPartitionAndOffset: Map[TopicPartition, Long] = client
//3.将获取的hash集合里的所有key,就是topic,对应的value,
//就是(topic,partitionAndOffset)
.hgetAll(key)
//4.将读取到的所有key组成的集合转成scala的Map集合
.asScala
//5.对读取到的集合做flatMap操作
.flatMap{
case (topic,partitionAndOffset:String)=>
//partitionAndOffset格式如下
//6.解析这个value的格式: {1 -> 1000, 0 -> 2000}
implicit val f = org.json4s.DefaultFormats
JsonMethods
//7.将这个map用json格式解析
.parse(partitionAndOffset)
//8.遍历这个json的每个对象
.extract[Map[Int,Long]]
.map{
// {1 -> 1000, 0 -> 2000}
//9.将{partition -> offset格式变成(topic,partition) -> offset 格式
case(partition,offset) =>
new TopicPartition(topic,partition) -> offset
}
}
10.将可变Map转成不可变Map
.toMap
//10(topic,partition) -> offset 格式输出
println("多个topic一个流: 初始偏移量: " + topicPartitionAndOffset)
//11.关闭客户端
client.close()
//12.返回读取到的该topic对应的value,topicPartitionAndOffset
topicPartitionAndOffset
}
5.2.4.3 BaseAppV2方法
思路
--把多个topic放到一个流里面
代码
package com.atguigu.realtime
import com.atguigu.realtime.util.{MyKafkaUtil, OffsetManager}
import org.apache.kafka.common.TopicPartition
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.kafka010.{HasOffsetRanges, OffsetRange}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.json4s.CustomSerializer
import org.json4s.JsonAST.{JDouble, JInt, JLong, JString}
import scala.collection.mutable.ListBuffer
/**
* Author atguigu
* Date 2020/11/16 9:25
*
* 把多个topic放在一个流中
*/
abstract class BaseAppV2 {
val master: String
val appName: String
val groupId: String
val topics: Seq[String] // 同时消费多个topic
val bachTime: Int
val toLong = new CustomSerializer[Long](ser = format => ( {
case JString(s) => s.toLong
case JInt(s) => s.toLong
}, {
case s: Long => JLong(s)
}))
val toDouble = new CustomSerializer[Double](format => ( {
case JString(s) => s.toDouble
case JDouble(s) => s.toDouble
}, {
case s: Double => JDouble(s)
}))
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster(master).setAppName(appName)
val ssc: StreamingContext = new StreamingContext(conf, Seconds(bachTime))
//实现升级后的readOffsets【5.2.4.2 】
val offsets: Map[TopicPartition, Long] = OffsetManager.readOffsets(groupId, topics)
val offsetRanges: ListBuffer[OffsetRange] = ListBuffer.empty[OffsetRange]
val sourceStream = MyKafkaUtil
//getKafkaStream需要升级 【5.2.4.1 】
.getKafkaStream(ssc, groupId, topics, offsets)
//将offsets传入,需要升级【5.2.4.2 】
.transform(rdd => {
offsetRanges.clear()
val newOffsetRanges: Array[OffsetRange] = rdd
.asInstanceOf[HasOffsetRanges].offsetRanges
offsetRanges ++= newOffsetRanges //driver
rdd
})
//获取sourceStream的topic和具体的数据
.map(record => (record.topic(), record.value()))
run(ssc, sourceStream, offsetRanges)
ssc.start()
ssc.awaitTermination()
}
def run(ssc: StreamingContext,
sourceStream: DStream[(String, String)], // _1: topic _2: 数据
offsetRanges: ListBuffer[OffsetRange])
}
具体实现方法
5.2.5 UserInfo样例类
package com.atguigu.realtime.bean
import java.text.SimpleDateFormat
case class UserInfo(id: String,
user_level: String,
birthday: String,
gender: String, // F M
var age_group: String = null, //年龄段
var gender_name: String = null) { //性别 男 女
// 计算年龄段
val age = (System.currentTimeMillis() - new SimpleDateFormat("yyyy-MM-dd").parse(birthday).getTime) / 1000 / 60 / 60 / 24 / 365
age_group = if (age <= 20) "20岁及以下" else if (age <= 30) "21岁到 30 岁" else "30岁及以上"
// 计算gender_name
gender_name = if (gender == "F") "女" else "男"
}
5.2.6 初始化维度表、使用phoenix创建维度表
①初始化维度表,再将其存到phoenix
#初始化省份表
bin/maxwell-bootstrap --user maxwell --password aaaaaa --host hadoop162 --database gmall --table base_province --client_id maxwell_1
#初始化用户表
bin/maxwell-bootstrap --user maxwell --password aaaaaa --host hadoop162 --database gmall --table user_info --client_id maxwell_1
②使用phoenix创建维度表,用来存保存到维度表的数据
--创建省份表
create table gmall_province_info (id varchar primary key,name varchar,area_code varchar,iso_code varchar)SALT_BUCKETS = 2
--创建用户表
create table gmall_user_info (id varchar primary key ,user_level varchar, birthday varchar,
gender varchar, age_group varchar , gender_name varchar)SALT_BUCKETS = 2
开启maxwell,启动BaseDBMaxwellApp类将初始化的数据写到ods层,在启动DwdDimApp类将数据存到phoenix
5.2.7 创建DwdDimApp类 ——实现维度表数据写入hbase
思路
消费所有的ods所有的维度表数据, 放在一个流中, 根据数据不同的topic, 将数据写入到hbase的不同表中。
代码 —— DwdDimApp
package com.atguigu.realtime.dwd
import com.atguigu.realtime.BaseAppV2
import com.atguigu.realtime.bean.{ProvinceInfo, UserInfo}
import com.atguigu.realtime.util.OffsetManager
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.dstream.DStream
import org.apache.spark.streaming.kafka010.OffsetRange
import org.json4s.jackson.JsonMethods
import scala.collection.mutable.ListBuffer
/*
把kafka的维度表数据(ods_...),写入到HBase
消费所有ods,所有的维度表数据,就在一个流中,根据不同的数据,写入到hbase不同的表
*/
object DwdDimApp extends BaseAppV2{
override val master: String = "local[2]"
override val appName: String = "DwdDimApp"
override val groupId: String = "DwdDimApp"
override val topics: Seq[String] = Seq(
"ods_base_province",
"ods_user_info"
)
override val bachTime: Int = 3
/**
* 将saveToPhoenix方法封装
* @param rdd
* @param odsTopic
* @param tableName
* @param cols
* @param mf
* @tparam T
*/
def saveToPhoenix[T <:Product](rdd: RDD[(String, String)],
odsTopic: String,
tableName: String,
cols: Seq[String])(implicit mf:scala.reflect.Manifest[T]) = {
import org.apache.phoenix.spark._
rdd.cache()
rdd.collect().foreach(println)
rdd
.filter(_._1 == odsTopic)
.map{
case (topics,context) =>
val f = org.json4s.DefaultFormats
JsonMethods.parse(context).extract[T](f ,mf)
}
.saveToPhoenix(
tableName, //表名
cols, //表的字段
zkUrl = Option("hadoop162,hadoop163,hadoop164:2181"))
}
/**
* 实现run方法
* @param ssc
* @param sourceStream
* @param offsetRanges
*/
override def run(ssc: StreamingContext,
sourceStream: DStream[(String, String)],
offsetRanges: ListBuffer[OffsetRange]): Unit = {
sourceStream
.foreachRDD((rdd: RDD[(String, String)]) =>{
//不同topic的数据写入到不同的表中
topics.foreach{
case "ods_base_province" =>
saveToPhoenix[ProvinceInfo](rdd,
"ods_base_province",
"gmall_province_info",
Seq("ID", "NAME", "AREA_CODE", "ISO_CODE")
)
case "ods_user_info" =>
saveToPhoenix[UserInfo](rdd,
"ods_user_info",
"gmall_user_info",
Seq("ID", "USER_LEVEL", "BIRTHDAY", "GENDER", "AGE_GROUP", "GENDER_NAME")
)
}
OffsetManager.saveOffsets(offsetRanges, groupId, topics)
})
}
}
测试
1.开启zk。kafka。canal。redis。ods层数据,
2.执行DwdDimApp类
3.用Phoenix查看数据是否保存进去
5.3 首单分析的具体实现
5.3.1 SparkSqlUtil工具类 —将SparkSql读取的数据转成rdd
用于读取存在hbase中的维度表的信息
传过来一堆参数,返回想要结果
因为后面要跟rdd做join,所以这个方法返回一个rdd就可以
package com.atguigu.realtime.util
import org.apache.spark.sql.{Encoder, SparkSession}
object SparkSqlUtil {
//从spark-sql读取的数据,转成rdd
val url ="jdbc:phoenix:hadoop162,hadoop163,hadoop164:2181"
//传入一个sparksession和sql语句
def getRDD[T:Encoder](spark:SparkSession,sql:String) ={
spark
.read //得到一个DataFrameReader
.format("jdbc") //格式,是jdbc里面
.option("url",url) //驱动是什么,根据url获取驱动
.option("query",sql)
.load()
//这时得到的是一个rdd需要的是df
//将rdd转成df,转成rdd存到应该是样例类
//封装成一个T类型的样例类
//T:Encoder是一个sql编码器
//泛型上下文需要一个隐式值def as[U : Encoder]: Dataset[U] = Dataset[U](sparkSession, logicalPlan)
.as[T]
.rdd
}
}
5.3.2 创建DwdOrderInfoApp类(补充维度表信息)
object DwdOrderInfoApp extends BaseApp{
override val master: String = "local[2]"
override val appName: String = "DwdOrderInfoApp"
override val groupId: String = "DwdOrderInfoApp"
override val topic: String = "ods_order_info"
override val bachTime: Int = 3
override def run(ssc: StreamingContext,
offsetRanges: ListBuffer[OffsetRange],
sourceStream: DStream[String]): Unit = {
val spark: SparkSession = SparkSession.builder()
.config(ssc.sparkContext.getConf)
.getOrCreate()
**用于将JVM中的对象,和SparkSql使用的相关xxx转换!**
**只要涉及到RDD和DS或DF之间的转换,都需要提供一个Encoder,通常,常见的基本数据类型的Encoder不需要我们手动提供,只要导入**
import spark.implicits._
=》【涉及到DF和RDD的转换】 -需要定义样例类
/**
* 自定义一个格式化,告诉json4s如何把JString转成Long
*/
val orderInfoStream: DStream[OrderInfo] = sourceStream
.map(str => {
implicit val f = org.json4s.DefaultFormats + toDouble + toLong
JsonMethods.parse(str).extract[OrderInfo]
})
5.3.2.1 补充维度表信息
思路
--维度表已经写在sql里了
0.获取维度表的id
val userIds: String = rdd.map(_.user_id).collect().mkString("','")
val provinceIds: String = rdd.map(_.province_id).collect().mkString("','")
先得到一个sparksession
导入import spark.implicits._【sparkSession.implicits._】
1.补充维度表信息
'transform(rdd => {'
1.1 读取维度信息
'如何读呢?'用sparkSql --定义一个sparkSqlUtil工具类 【5.3.1 】
调用sparkSqlUtil的getRDD方法,传入sparksession,和sql语句读取维度表信息
调用getRDD方法,记得导入
'sal语句的id怎么确定呢?'---0.获取维度表的id
得到userInfoRDD和provinceRDD
1.2 rdd join维度信息,返回join后的rdd
不能直接join必须是k-v类型的rdd才能join
所以要先将rdd编程kv类型的,这里使用map算子
将rdd变成一个元组
再做join
--先与用户表join
.join(userInfoRDD) //与用户表join
//得到一个user_id , key是一个元组
.map {
case (userId, (orderInfo: OrderInfo, userInfo: UserInfo)) =>
orderInfo.user_age_group = userInfo.age_group
orderInfo.user_gender = userInfo.gender_name
(orderInfo.province_id.toString, orderInfo)
}
--再与省份表join
.join(provinceRDD) //与省份表join
.map {
case (proId, (orderInfo: OrderInfo, proInfo: ProvinceInfo)) => //补齐省份信息
orderInfo.province_name = proInfo.name
orderInfo.province_area_code = proInfo.area_code
orderInfo.province_iso_code = proInfo.iso_code
orderInfo
}
'})'
代码:
//1.补充维度表信息
val orderInfoStreamWithAllDim: DStream[OrderInfo] = orderInfoStream.transform(rdd => {
rdd.cache()
//0.先获取这次用到的所有用户的id
//先将id拿出来,在做collect,再讲其拼接成字符串 1 2 3 1','2','3
//def mkString(sep: String): String = mkString("", sep, "")
val userIds: String = rdd.map(_.user_id).collect().mkString("','")
val provinceIds: String = rdd.map(_.province_id).collect().mkString("','")
//1.1读取维度信息
val userInfoSql = s"select * from gmall_user_info where id in('${userIds}')"
val userInfoRDD: RDD[(String, UserInfo)] = SparkSqlUtil
.getRDD[UserInfo](spark, userInfoSql) "==》【涉及到DF和RDD的转换】 -需要定义样例类"
.map(user => (user.id, user))
val provinceSql = s"select * from gmall_province_info where id in('${provinceIds}')"
val provinceRDD: RDD[(String, ProvinceInfo)] = SparkSqlUtil
.getRDD[ProvinceInfo](spark, provinceSql) "==》【涉及到DF和RDD的转换】 -需要定义样例类"
.map(pro => (pro.id, pro))
//1.2 rdd join 维度信息,返回json后的rdd
rdd
.map(info => (info.user_id.toString, info))
.join(userInfoRDD) //与用户表join
//得到一个user_id , key是一个元组
.map {
case (userId, (orderInfo: OrderInfo, userInfo: UserInfo)) =>
orderInfo.user_age_group = userInfo.age_group
orderInfo.user_gender = userInfo.gender_name
(orderInfo.province_id.toString, orderInfo)
}
.join(provinceRDD) //与省份表join
.map {
case (proId, (orderInfo: OrderInfo, proInfo: ProvinceInfo)) => //补齐省份信息
orderInfo.province_name = proInfo.name
orderInfo.province_area_code = proInfo.area_code
orderInfo.province_iso_code = proInfo.iso_code
orderInfo
}
})
orderInfoStreamWithAllDim.print()
5.3.2.2 处理首单
思路
2.处理首单
使用transform有返回值
'transform(rdd => {'
--问?
如何判断是不是首单
如何标记首单和非首单,从hbase中读出哪些用户下过单,下过单就是false,没有就是true
--误差
有一种情况会出现误差,就是有人三秒内下了两单,我们这种写法,会把这两单算成首单
--解决
将该用户的所有订单的下单时间进行排序,取时间戳小为首单
--代码实现
将这个用户时间戳小的那单的is_first_order定义成true,其它的定义成false
1.先去hbase读出用户的状态,使用sparksql
--先用phoenix在hbase创建保存订单状态的表
'create table user_status(user_id varchar primary key ,is_first_order boolean) SALT_BUCKETS = 5;'
2.用SparkSqlUtil工具类调用getRDD方法,这时需要定义一个样例类
①获取新的用户id
②调用rdd需要执行的sql语句,读取订单状态表中的已有的用户的id
--再判断是否是首单
3.判断老的id中是否包含新的id,
if (oldUserIds.contains(orderInfo.user_id.toString)) {
--如果是,将is_first_order变为false,说明这次不是首单
orderInfo.is_first_order = false
} else {
--如果不是,将is_first_order变为true,说明这次不是首单
orderInfo.is_first_order = true
}
--返回用户id和用户下单订单
(orderInfo.user_id, orderInfo)
4.解决误差,我们使用groupByKey,根据id分组,
5.分完组之后做flatmap操作
.flatMap {
//zs
case (_, it) =>
--目前写法对这个用户下的单,如果it里面有一旦是true是首单,则这个人的订单集合的所有订单都是首单
--解决
val list: List[OrderInfo] = it.toList
--过滤出用户的订单集合中首单的is_first_order是true的进行处理
if (list.head.is_first_order) {
--将这个人的订单按时间戳排序,将这个人的其它订单的is_first_order变成false
val listOrdered: List[OrderInfo] = list.sortBy(_.create_time)//3个
--取拍好序的订单的最后一个做map操作
--假设有三个,第一个原封不动,后两个的is_first_order都变成false
listOrdered.head :: listOrdered.tail.map(info => { //后两个
info.is_first_order = false
info
})
}else it --如果不是首单,是啥就返回啥
}
')}'
--有一种情况会出现误差,就是有人三秒内下了两单,我们这种写法,会把这两单算成首单
--我们需要去时间戳小为首单
代码:
//2.处理首单
val resultStream: DStream[OrderInfo] = orderInfoStreamWithAllDim.transform(transformFunc = rdd => {
//2.1 去hbase读出用户的状态,读出的数据放到一个样例类中
//create table user_status(user_id varchar primary key ,is_first_order boolean) SALT_BUCKETS = 5;
rdd.cache() //做一个缓存
//新的用户id
val userIds = rdd.map(_.user_id).collect().mkString("','")
val sql = "select * from user_status where user_id in ('1','2')"
//状态信息表中已有的用户id
val oldUserIds: Array[String] = SparkSqlUtil
.getRDD[UserStatus](spark, sql)
.map(_.user_id)
.collect()
//判断是否首单
rdd.map((orderInfo: OrderInfo) => {
if (oldUserIds.contains(orderInfo.user_id.toString)) {
orderInfo.is_first_order = false
} else {
orderInfo.is_first_order = true
}
(orderInfo.user_id, orderInfo)
})
.groupByKey() //如果一个用户第一次下单,一个批次
.flatMap {
//zs
case (_, it) =>
val list: List[OrderInfo] = it.toList
if (list.head.is_first_order) { //如果有任何一旦是首单,前面都会被标记为首单
val listOrdered: List[OrderInfo] = list.sortBy(_.create_time)
listOrdered.head::listOrdered.tail.map(info => { //后两个
info.is_first_order = false
info
})
}else it
}
})
5.3.2.3 把首单的用户id写入到hbase中
思路
3.把首单的用户id写入到hbase中
'使用foreachRDD没有返回值'
--foreachRDD(rdd =>{
1.先过滤,使用sparksql写
2.先在rdd里存样例类
3.再将其存入phoenix中
--})
代码:
UserStatus样例类:
case class UserStatus(user_id:String,
is_first_order:Boolean) {
}
主方法:
//3.把首单的用户id写入到hbase中,使用sparksql
resultStream.foreachRDD(rdd =>{
//调用saveToPhoenix方法需要导入
import org.apache.phoenix.spark._
println("rdd...")
rdd.cache() //先将第一次数据缓存,下一次就有可能出现false
rdd.collect().foreach(println)
rdd
.filter(_.is_first_order)
//先封装成样例类,先在rdd里存样例类 --定义UserStatus样例类
.map(orderInfo =>UserStatus(orderInfo.user_id.toString,true))
//将其保存到phoenix中
.saveToPhoenix("user_status",
Seq("USER_ID","IS_FIRST_ORDER"),
zkUrl = Option("hadoop162,hadoop163,hadoop164:2181"))
5.3.2.4 将数据写进es
思路
用MyEsUtil工具类,调用他的insertBulk方法,
将index索引 --先去es创建一个模板
以及要插入到数据it传入,进行批量插入数据
代码:
//写进es
rdd.foreachPartition(it =>{
MyEsUtil.insertBulk(s"gmall_order_info_${LocalDate.now()}",it)
})
OffsetManager.saveOffsets(offsetRanges, groupId, topic)
})
}
}
总结
①transform和foreachRDD
1.transform对rdd操作需要有返回值
2.foreachRDD对rdd进行操作不需要返回值
②map和flatMap
map:一对一
flatMap:a 进去-->出来的是 a和a和a包含的value值 {a ->(1,2,3)}
③Encoder
1.介绍
用于将JVM中的对象,和SparkSql使用的相关xxx转换!
只要涉及到RDD和DS或DF之间的转换,都需要提供一个Encoder,通常,常见的基本数据类型 的Encoder不需要我们手动提供,只要导入
sparkSession.implicits._
在转换时会自动调用隐式方法,创建要转换类型的Encoder
2. 构造
手动创建: 对于基本数据类型: Encoders.scalaxx 或者 Encoders.xxx
自定义的类型:
- 样例类:
Encoders.product[T]
- 不是样例类:
ExpressionEncoder[T]()
④SpqrkSql
使用sparksql将数据写进hbase中,需要导入
import org.apache.phoenix.spark._
⑤phoenix的相关操作
--启动phoenix
bin/sqlline.py hadoop162:2181
--关闭
!quit
⑥kafka的相关操作
#删除kafka主题
bin/kafka-topic.sh --delete --bootstrap-server hadoop162:9092 --topic 主题名
#查看所有主题
bin/kafka-topics.sh --list --bootstrap-server hadoop102:9092
#启动生产者
bin/kafka-console-producer.sh --broker-list hadoop162:9092 --topic 主题名
#启动消费者
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic 主题名
#表示重头开始消费
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic tn --from-beginning
⑦redis的相关操作
--查看所有key值
keys *
--清空全库
FLUSHALL
--删除指定的key
DEL KEY [KEY ...]
--根据key得到值,只能用于string类型。
get key
--
⑧zk相关操作
#删除某个元数据
deleteall /节点名
#查看所有元数据节点
ls /
⑨es相关操作
--查询某个节点状态
GET /_cat/节点名
--查询所有节点状态
GET /-cat/indices?v
--创建索引
PUT 索引名?pretty
-- 删除
DELETE /索引名
⑩首单bug解决
.groupByKey()
.flatMap {
//zs
case (_, it) =>
--目前写法对这个用户下的单,如果it里面有一旦是true是首单,则这个人的订单集合的所有订单都是首单
--解决
val list: List[OrderInfo] = it.toList
--过滤出用户的订单集合中首单的is_first_order是true的进行处理
if (list.head.is_first_order) {
--将这个人的订单按时间戳排序,将这个人的其它订单的is_first_order变成false
val listOrdered: List[OrderInfo] = list.sortBy(_.create_time)//3个
--取拍好序的订单的最后一个做map操作
--假设有三个,第一个原封不动,后两个的is_first_order都变成false
listOrdered.head :: listOrdered.tail.map(info => { //后两个
info.is_first_order = false
info
})
}else it --如果不是首单,是啥就返回啥
}
')}'
--有一种情况会出现误差,就是有人三秒内下了两单,我们这种写法,会把这两单算成首单
--我们需要去时间戳小为首单
KV类型的RDD才能用join