Flink - 尚硅谷- 大数据高级 Flink 技术精讲 - 3

注:次文档参考 【尚硅谷】大数据高级 flink技术精讲(2020年6月) 编写。

1.由于视频中并未涉及到具体搭建流程,Flink 环境搭建部分并未编写。
2.视频教程 Flink 版本为 1.10.0,此文档根据 Flink v1.11.1 进行部分修改。
3.文档中大部分程序在 Windows 端运行会有超时异常,需要打包后在 Linux 端运行。
4.程序运行需要的部分 Jar 包,请很具情况去掉 pom 中的 “scope” 标签的再进行打包,才能在集群上运行。
5.原始文档在 Markdown 中编写,此处目录无法直接跳转。且因字数限制,分多篇发布

此文档仅用作个人学习,请勿用于商业获利。

附.项目实战

此处大部分为代码部分,具体操作略

1. 项目整体介绍

1.1 电商用户行为分析

日志分类:

  • 用户
    • 登陆方式
    • 上线时间点和时长
    • 页面停留和跳转
  • 用户对商品的行为数据
    • 收藏/喜欢/评分/评价/打标签
    • 点击/浏览/购买/支付

日志分析有哪几类

  • 统计分析
    • 点击、浏览
    • 热门商品,近期热门商品、分类热门商品、流量统计
  • 偏好统计
    • 收藏、喜欢、评分、打标签
    • 用户画像,推荐列表
  • 风险控制
    • 下订单、支付、登陆
    • 刷单监控、订单失效监控、恶意登陆(短时间内频繁登陆失败)
1.2 项目模块设计
  • 实时统计分析

    • 热门商品
    • 流量统计
      • 热门页面统计
      • PV
      • UV
    • 市场营销指标
      • APP 市场推广
      • 页面广告分析
    • 实时访问流量统计
    • 页面广告点击量统计
  • 业务流程及风险控制

    • 页面广告黑名单过滤
    • 恶意登陆监控
    • 订单支付
      • 超时失效
      • 实时对账

2. 实时热门商品统计

需求

求一段时间内商品访问量的 Top N

需求分析

一段时间 -> 需要使用时间窗进行分组求和

Top N -> 需要对这个窗口内的数据进行统计排序。也就是接收上一步的数据,按窗口分组,然后排序

数据处理流程

source == map、assignAscendingTimestamps ==> 样例类 == filter、keyBy、timeWindow ==> 开窗
== aggregate(new CountAgg(), new ItemCountWindowResult()) ==> 窗口内进行分组求和,并获取窗口信息
== keyBy(windowEnd) ==> 取当前分组内的TopN,通过定时器触发 Sink

Pom

    <properties>
        <flink.version>1.11.1</flink.version>
        <scala.binary.version>2.11</scala.binary.version>
        <kafka.version>2.4.1</kafka.version>
    </properties>

    <dependencies>
        <!--  ======== Flink Core ========  -->
        <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-scala -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-scala_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
            <!-- 由于集群上已经有该 jar 包,若要上传到集群上执行,则去掉以下注释 -->
            <scope>provided</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-streaming-scala -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
            <!-- 由于集群上已经有该 jar 包,若要上传到集群上执行,则去掉以下注释 -->
            <scope>provided</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-clients -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope>
        </dependency>

        <!--  ======== Flink Sink Connector ========  -->
        <!-- https://mvnrepository.com/artifact/org.apache.flink/flink-connector-kafka -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
<!--            <scope>provided</scope>-->
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka -->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka_${scala.binary.version}</artifactId>
            <version>${kafka.version}</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>*.properties</include>
                    <include>*.txt</include>
<!--                    <include>*.csv</include>-->
                </includes>
                <excludes>
                    <exclude>*.xml</exclude>
                    <exclude>*.yaml</exclude>
                </excludes>
            </resource>
        </resources>

        <plugins>
            <!-- 编译 Scala 需要用到的插件 -->
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.4.6</version>
                <configuration>
                    <addScalacArgs>-target:jvm-1.8</addScalacArgs>
                </configuration>
                <executions>
                    <execution>
                        <!-- 声明绑定到 maven 的 compile 阶段 -->
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- 项目打包需要用到的插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.0.0</version>
                <configuration>
                    <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>

Code

package com.mso.hotitems_analysis

import java.sql.Timestamp
import java.util.Properties

import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.api.common.state.{ListState, ListStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
import org.apache.flink.util.Collector

import scala.collection.mutable.ListBuffer

// 定义输入数据的样例类
case class UserBehavior(userId: Long, itemId: Long, categoryId: Int, behavior: String, timestamp: Long)

// 定义窗口聚合结果样例类
case class ItemViewCount(itemId: Long, windowEnd: Long, count: Long)

/**
 * 热门商品统计。
 *
 * 设置滑动事件窗口。窗口大小 1h,滑动步长 5min
 * 窗口聚合:定义窗口聚合规则 和 输出数据结构 - .aggregate( new CountAgg(), new WindowResultFunction())
 * 进行统计整理:按照关窗的时间分组,使用状态编程,并定义定时器定时输出 - keyBy("windowEnd")
 *
 * 最终排序输出 - keyedProcessFunction :
 * - 针对有状态流的 API
 * - KeyedProcessFunction 会对分区后的每一条子流进行处理
 * - 以 windowEnd 作为 key,保证分流以后每一条流的数据都在一个时间窗口内
 * - 从 ListState 中读取当前流的状态,存储数据进行排序输出
 *
 * 用 ProcessFunction 来定义 KeyedStream 的处理逻辑
 * 分区之后,每个 KeyedStream 都有其自己的生命周期
 * - open : 初始化,在这里可以获取当前流的状态
 * - processElement : 处理流中每一个元素时调用
 * - onTimer : 定时调用,注册定时器 Timer 并触发之后的回调操作
 */
object HotItems {
  def main(args: Array[String]): Unit = {
    val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

    // 1. Source - data file : UserBehavior.csv
    //    val parameterTool: ParameterTool = ParameterTool.fromArgs(args)
    //    val inputStream: DataStream[String] = environment.readTextFile(parameterTool.get("input-path"))
    val properties = new Properties()
    properties.setProperty("bootstrap.servers", "test01:9092")
    properties.setProperty("group.id", "consumer-group")
    properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    properties.setProperty("auto.offset.reset", "latest")
    val inputStream: DataStream[String] = environment.addSource(new FlinkKafkaConsumer[String]("HotItems", new SimpleStringSchema(), properties))

    // 2. 将数据转换为样例类,并提取 timestamp 定义为 watermark
    val dataStream: DataStream[UserBehavior] = inputStream
      .map((data: String) => {
        val dataArray: Array[String] = data.split(",")
        UserBehavior(dataArray(0).toLong, dataArray(1).toLong, dataArray(2).toInt, dataArray(3), dataArray(4).toLong)
      })
      .assignAscendingTimestamps((_: UserBehavior).timestamp * 1000L) // 此处的测试数据为升序排序,因此不定义延迟时间

    // 3. Transform
    val processedStream: DataStream[String] = dataStream.filter((_: UserBehavior).behavior == "pv")
      .keyBy((data: UserBehavior) => data.itemId)
      .timeWindow(Time.hours(1), Time.minutes(5))
      .aggregate(new CountAgg(), new ItemCountWindowResult())
      .keyBy((data: ItemViewCount) => data.windowEnd)
      .process(new TopNHotItems(5))

    // 4、sink,控制台输出
    processedStream.print()
    environment.execute("HotItems Job")
  }
}

// 自定义聚合函数,来一条数据就 +1,
// * @param <IN>  The type of the values that are aggregated (input values)
// * @param <ACC> The type of the accumulator (intermediate aggregate state).
// * @param <OUT> The type of the aggregated result
class CountAgg() extends AggregateFunction[UserBehavior, Long, Long] {
  override def createAccumulator(): Long = 0L

  override def add(value: UserBehavior, accumulator: Long): Long = accumulator + 1

  override def getResult(accumulator: Long): Long = accumulator

  override def merge(a: Long, b: Long): Long = a + b
}

// 自定义窗口函数,结合 window 信息,包装成样例类
class ItemCountWindowResult extends WindowFunction[Long, ItemViewCount, Long, TimeWindow] {
  override def apply(key: Long, window: TimeWindow, input: Iterable[Long], out: Collector[ItemViewCount]): Unit = {
    out.collect(ItemViewCount(key, window.getEnd, input.iterator.next()))
  }
}

// 自定义 KeyedProcessFunction。 对窗口聚合结果进行分组,并做排序取 TopN 输出
class TopNHotItems(topSize: Int) extends KeyedProcessFunction[Long, ItemViewCount, String] {
  // 定义一个 ListState,用来保存当前窗口所有的 count 结果
  private var itemCountListState: ListState[ItemViewCount] = _

  override def open(parameters: Configuration): Unit = {
    itemCountListState = getRuntimeContext.getListState(new ListStateDescriptor[ItemViewCount]("itemCount-ListState", classOf[ItemViewCount]))
  }

  override def processElement(value: ItemViewCount, ctx: KeyedProcessFunction[Long, ItemViewCount, String]#Context, out: Collector[String]): Unit = {
    // 每来一条数据就添加到状态中
    itemCountListState.add(value)
    // 注册定时器,在 windowEnd + 1 触发
    ctx.timerService().registerEventTimeTimer(value.windowEnd + 1)
  }

  // 定时器触发时,从状态中取数据,然后排序输出
  override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Long, ItemViewCount, String]#OnTimerContext, out: Collector[String]): Unit = {
    // 将所有state中的数据取出,放到一个List Buffer中
    val allItemCountList: ListBuffer[ItemViewCount] = new ListBuffer()
    import scala.collection.JavaConversions._
    for (itemCount <- itemCountListState.get()) {
      allItemCountList += itemCount
    }

    // 按照 count 大小排序,并取前 n 个
    val sortedItemsCountList: ListBuffer[ItemViewCount] = allItemCountList.sortBy((_: ItemViewCount).count)(Ordering.Long.reverse).take(topSize)

    // 清空状态
    itemCountListState.clear()
    // 将排名结果格式化输出
    val result: StringBuilder = new StringBuilder()
    result.append("Time : ").append(new Timestamp(timestamp - 1)).append("\n")
    // 遍历 sorted 列表,输出每一个商品的信息
    for (i <- sortedItemsCountList.indices) {
      val currentItemCount: ItemViewCount = sortedItemsCountList(i)
      result.append("No").append(i + 1).append(":")
        .append(" 商品ID=").append(currentItemCount.itemId)
        .append(" 浏览量=").append(currentItemCount.count)
        .append("\n")
    }
    result.append("================================")
    out.collect(result.toString())

    // 由于测试是历史数据,所有数据都会一次性输出,此处需要控制输出频率
    Thread.sleep(500)
  }
}

// 自定义预聚合函数计算平均数,状态为 (sum, count)
class AverageAgg() extends AggregateFunction[UserBehavior, (Long, Int), Double] {
  override def createAccumulator(): (Long, Int) = (0L, 0)

  override def add(value: UserBehavior, accumulator: (Long, Int)): (Long, Int) =
    (accumulator._1 + value.timestamp, accumulator._2 + 1)

  override def getResult(accumulator: (Long, Int)): Double = accumulator._1 / accumulator._2.toDouble

  override def merge(a: (Long, Int), b: (Long, Int)): (Long, Int) =
    (a._1 + b._1, a._2 + b._2)
}

KafkaUtil Code

package com.mso.hotitems_analysis

import java.io.InputStream
import java.util.Properties

import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}

object KafkaProducerUtil {
  def main(args: Array[String]): Unit = {
    writeToKafka("HotItems")
  }

  def writeToKafka(topic: String): Unit = {
    val properties = new Properties()
    properties.put("bootstrap.servers", "test01:9092")
    properties.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
    properties.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer")

    // 定义一个 kafka producer
    val producer = new KafkaProducer[String, String](properties)

    val stream: InputStream = getClass.getResourceAsStream("/UserBehavior.csv")
    val lines: Iterator[String] = scala.io.Source.fromInputStream(stream).getLines
    for (line <- lines) {
      val record = new ProducerRecord[String, String](topic, line)
      producer.send(record)
    }

  
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值