大数据技术Flink电商实时数仓DWD数据层准备--第3章 功能2:准备用户日志DWD层

          们前面采集的日志数据已经保存到Kafka中,作为日志数据的ODS层,从kafka的ODS层读取日志数据分为3类:页面日志启动日志曝光日志。这三类数据虽然都是用户行为数据,但是有着完全不一样的数据结构,所以要拆分处理。将拆分的不同的日志写回Kafka不同主题中,作为日志DWD层。

        页面日志输出到主流,启动日志输出到启动侧输出流,曝光日志输出到曝光侧输出流

3.1 主要任务

3.1.1 识别新老用户

        本身客户端有新老用户的标识,但是不够准确,需要用实时计算再次确认(不涉及业务操作,只是单纯做一个状态确认)

3.1.2 利用侧输出流实现数据拆分

        根据日志数据内容,将日志数据分为3类,页面日志,启动日志和曝光日志。页面日志输出到主流,启动日志输出到启动侧输出流,曝光日志输出曝光侧数据流。

3.1.3 将不同的流的数据推送到下游的kafka不同Topic中

3.2 代码实现

3.2.1 接收Kafka数据,并进行转换

1、封装操作kafka的工具类,并提供获取kafka消费者的方法(读)

package com.atguigu.gmall.realtime.utils

import java.util.Properties

import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.connectors.kafka.{FlinkKafkaConsumer, FlinkKafkaProducer}
import org.apache.kafka.clients.consumer.ConsumerConfig
/**
 * @Author: lb
 *          Create on 2021/4/12
 *          describe:操作kafka工具类
 */
object MyKafka {

  val kafkaServer:String = "hadoop102:9092,hadoop103:9092,hadoop104:9092"

  // 封装kafka消费者
  def getKafkaSource(topic:String,groupId:String):FlinkKafkaConsumer[String]={
    val prop:Properties = new Properties()
    prop.setProperty(ConsumerConfig.GROUP_ID_CONFIG,groupId)
    prop.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,kafkaServer)
    new FlinkKafkaConsumer[String](topic,new SimpleStringSchema(),prop)

  }

  
}

2、Flink 调用工具类读取数据的主程序

package com.atguigu.gmall.realtime.app.dwd

import java.lang
import java.text.SimpleDateFormat
import java.util.Date

import com.alibaba.fastjson.{JSON, JSONArray, JSONObject}
import com.atguigu.gmall.realtime.utils.MyKafka
import org.apache.flink.api.common.functions.RichMapFunction
import org.apache.flink.api.common.state.{ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.runtime.state.filesystem.FsStateBackend
import org.apache.flink.streaming.api.CheckpointingMode
import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.{FlinkKafkaConsumer, FlinkKafkaProducer}
import org.apache.flink.util.Collector
import org.slf4j.LoggerFactory

/**
 * @Author: lb
 *          Create on 2021/4/12
 *          describe:从kafka中读取ods层用户行为数据
 */
object BaseLogApp {

  // 定义用户主题
  val TOPIC_START: String = "dwd_start_log"
  val TOPIC_PAGE: String = "dwd_page_log"
  val TOPIC_DISPLAY: String = "dwd_display_log"

  // 定义启动和曝光数据的侧输出流标签
  val startTag: OutputTag[String] = new OutputTag[String]("start")
  val displayTag: OutputTag[String] = new OutputTag[String]("display")
  
  val logger = LoggerFactory.getLogger(this.getClass)
  def main(args: Array[String]): Unit = {
    // TODO 0 准备基本环境
    // 创建 Flink 流处理执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment


    // 设置并行度,这里和kafka分区数保持一直
    env.setParallelism(4)
    
    // 设置CK相关参数
    // 设置精确一次性保证(默认)每5000ms开始一次checkPoint
    env.enableCheckpointing(5000,CheckpointingMode.EXACTLY_ONCE)
    // CheckPoint 必须在一分钟内完成,否则就会被抛弃
    env.getCheckpointConfig.setCheckpointTimeout(60000)
    env.setStateBackend(new FsStateBackend("hdfs://hadoop102:8020/gmall/flink/checkpoint"))
    System.setProperty("HADOOP_USER_NAME","hadoop")
   

    // 指定消费者配置信息
    val groupId: String = "ods_dwd_base_log_app"
    val topic: String = "ods_base_log"

    // TODO 1. 从kafka中读取数据
    // 调用kafka工具类,从指定kafka 主题读取数据
    val kafkaSource: FlinkKafkaConsumer[String] = MyKafka.getKafkaSource(topic, groupId)
    val kafkaDS: DataStream[String] = env.addSource(kafkaSource)

    // 转成Json 字符串
    val jsonObjectDS: DataStream[JSONObject] = kafkaDS.map(item => {
      val jsonObject: JSONObject = JSON.parseObject(item)
      jsonObject
    })
    // 打印测试
    jsonObjectDS.print()

    // 执行
    env.execute()
  }


}

测试流程:

这里使用checkPoint,所以需要将HDFS 打开。Kafka 打开 ,启动日志日志处理jar,启动模拟日志服务。可参考 大数据技术Flink电商实时数仓-数据采集--第四章 日志数据采集

可看到控制台有输出,说明环境通了。

3.2.2 识别新老访客

        保存每个mid 的首次访问日期,每条进入该算子的访问记录,都会把mid对应的首次访问时间读取出来,跟当前日期进行比较,只有首次访问时间不为空,且首次访问时间早于当日的,则认为访客是老访客,否则是新访客。

        同时如果是新访客且没有访问记录的话,会写入首次访问时间。

   // TODO 2.识别新老客户
    // 按照mid 进行分组
    val midKeyedDS: KeyedStream[JSONObject, String] =
    jsonObjectDS.keyBy(data => data.getJSONObject("common").getString("mid"))

    val midWithNewFlagDS: DataStream[JSONObject] = midKeyedDS.map(new RichMapFunction[JSONObject, JSONObject] {
      // 声明第一访问的状态
      lazy val firstVisitDataState: ValueState[String]
      = getRuntimeContext.getState(new ValueStateDescriptor[String]("firstVisitDataState", classOf[String]))

      // 声明时间格式
      var simpleDateFormat: SimpleDateFormat = _

      override def open(parameters: Configuration): Unit = {
        simpleDateFormat = new SimpleDateFormat("yyyyMMdd")
      }

      override def map(jsonObject: JSONObject) = {

        // 获取访问标识 0 表示老访客 1 表示新访客
        var isNew: String = jsonObject.getJSONObject("common").getString("is_new")
        // 获取时间戳
        val ts: lang.Long = jsonObject.getLong("ts")

        // 如果是1 则开始校验数据是否正确
        if ("1".equals(isNew)) {
          // 获取访客的状态
          val newMidDate: String = firstVisitDataState.value()
          // 获取当前访问的时间
          val tsDate: String = simpleDateFormat.format(new Date(ts))
          // 如果访客的状态不为空,则说明该设备已经访问过,则将记录标识修改为“0”
          if (newMidDate != null && newMidDate.length != 0) {
            if (!newMidDate.equals(tsDate)) {
              isNew = "0"
              jsonObject.getJSONObject("common").put("is_new", isNew)
            }
          } else {
            //如果复检后,该设备的确没有访问过,那么更新状态为当前日期
            firstVisitDataState.update(tsDate)
          }
        }
        //返回确认过新老访客的 json 数据
        jsonObject
      }
    })
    //打印测试
     midWithNewFlagDS.print()

3.2.3 利用侧输出流实现数据拆分

        根据日志数据内容,将日志数据分为3类,页面日志,启动日志和曝光日志。页面日志输出到主流,启动日志输出到启动侧输出流,曝光日志输出到曝光侧输出流。

// 定义启动和曝光数据的侧输出流标签
  val startTag: OutputTag[String] = new OutputTag[String]("start")
  val displayTag: OutputTag[String] = new OutputTag[String]("display")


 // TODO 3.利用侧输出流实现数据拆分
    // 将不同的日志输出到不同的流中 页面日志输出到主流,启动日志输出到启动侧输出流,曝光日志输出到    曝光日志侧输出流
    val pageDStream: DataStream[String] = midWithNewFlagDS.process(new splitStream())
    val startDS: DataStream[String] = pageDStream.getSideOutput(startTag)
    val displayDS: DataStream[String] = pageDStream.getSideOutput(displayTag)

    pageDStream.print("page")
    startDS.print("start")
    displayDS.print("display")
splitStream 内部类
class splitStream extends ProcessFunction[JSONObject, String] {

    override def processElement(jsonObj: JSONObject,
                                context: ProcessFunction[JSONObject, String]#Context,
                                collector: Collector[String]) = {
      // 获取到数据中的启动相关数据
      val startJsonObj: JSONObject = jsonObj.getJSONObject("start")
      // 将数据转换成字符串,准备向流中输出
      val dataStr: String = jsonObj.toString
      if (startJsonObj != null && startJsonObj.size() > 0) {
        context.output(startTag, dataStr)
      } else {
        // 非启动日志,则为页面日志或者曝光日志(携带页面信息)
        // 将页面数据输出到主流
        collector.collect(dataStr)
        // 获取数据中曝光的数据,如果不为空,则将每条曝光日志取出然后发送到曝光侧输出流
        val displays: JSONArray = jsonObj.getJSONArray("displays")
        if (displays != null && displays.size() > 0) {
          for (i <- 0 until displays.size()) {
            val displayJsonObj = displays.getJSONObject(i)
            val pageId = jsonObj.getJSONObject("page").getString("page_id")
            displayJsonObj.put("page_id", pageId)
            // 将曝光日志输出到测输出流
            context.output(displayTag, displayJsonObj.toString)
          }
        }
      }
    }
  }

3.2.4 将不同的流的数据推送到下游kafka的不同topic(分流)

1、在MyKafkaUtil工具类中封装获取生产者的方法(写)

  def getKafkaSink(topic:String)={
    new FlinkKafkaProducer(kafkaServer,topic,new SimpleStringSchema())
  }

2、程序中调用kafka工具类获取sink

      // TODO 将数据输出到不同的kafka主题中    
    val startSink: FlinkKafkaProducer[String] = MyKafka.getKafkaSink(TOPIC_START)
    val displaySink: FlinkKafkaProducer[String] = MyKafka.getKafkaSink(TOPIC_DISPLAY)
    val pageSink: FlinkKafkaProducer[String] = MyKafka.getKafkaSink(TOPIC_PAGE)

    startDS.addSink(startSink)
    displayDS.addSink(displaySink)
    pageDStream.addSink(pageSink)

3)测试

  • Idea 中运行DwdBaseLog 类
  • 运行日志处理服务rt_gmall
  • 运行rt_applog 下模拟生成数据的jar包
  • 到kafka不同的主题下查看输出效果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一个中型公司在实际生产中使用Flink电商数仓,按照传统的数据仓库架构,可以将数据处理分为ODS、DWD、DWS、ADS四个次。这些次的数据量会因为公司业务规模和数据存储周期等因素而有所不同。 ODS:ODS是原始数据,主要用于存储源系统中的数据,包括电商平台中用户行为数据、商品数据、订单数据、支付数据、物流数据等等。ODS数据量通常比较大,可能会达到数十亿或者数百亿级别。 DWDDWD数据加工,主要用于对ODS数据进行清洗、转换和统一,以便后续的处理使用。DWD数据量相对于ODS会有所减少,但仍然相对较大,可能会达到数十亿或者数百亿级别。 DWS:DWS数据存储,主要用于存储经过加工处理的数据,以便后续的分析和计算使用。DWS数据量相对于DWD会有所减少,但仍然相对较大,可能会达到数十亿或者数百亿级别。 ADS:ADS数据应用,主要用于生成各种报表、图表和统计结果,以便业务人员进行分析和决策。ADS数据量比较小,通常是在DWS的基础上进行聚合和汇总生成的。 总的来说,中型公司在实际生产中使用Flink电商数仓,每个次的数据量会相对较大,可能会达到数十亿或者数百亿级别。因此,在设计和实现数据处理流程时需要考虑数据的规模和处理效率,以保证数据处理的准确性和效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值