【Flink快速入门-4.流处理之基于 Key 的算子】

流处理之基于 Key 的算子

实验介绍

在 SQL 中我们经常会用到分组(group by)操作,在 group 关键词之后指定要聚合的键,在 group 之前指定要聚合的逻辑(计数、求和、求最大值等),通过分区键将数据集分到不同的区块中,然后在各个区块中使用指定的聚合逻辑做操作。当然,在 Flink 的 API 也支持同样的操作,本节实验的内容就是通过学习基于 Key 的算子来实现 SQL 中的分组操作。

知识点

基于 Key 的算子可以分为三大类:

  • KeyBy
  • Rolling Aggregation
    • sum
    • min
    • max
    • minBy
    • maxBy
  • reduce

算子演示

在我们 FlinkLearning 工程的 com.vlab.operator 包下创建一个名为 KeyByOperator 的 Scala object,本节实验的示例代码都在此文件中执行。

紧接着我们拟造一下本节实验需要使用的数据。在 /home/vlab/ 路径下执行 touch userlog.txt 命令创建一个名为 userlog.txt 的文件,使用 vim 编辑该文件,添加以下内容:

Jack Beijing 100
Bob Chengdu 300
William Chengdu 600
Lily Shanghai 200
Loius Beijing 400
Joker Shanghai 200

我们用以上文本中的每一行表示一条用户访问日志,每行中的三个字段分别表示访问用户名、访问者城市以及访问时长。例如第一行所表示的是 Jack 在北京访问某服务器 100 秒。如果你不嫌麻烦可以多添加一些数据。

KeyBy

KeyBy 算子的作用是将一个 DataStream 转换成 KeyedStream,输⼊必须是 Tuple 类型,逻辑地将⼀个流拆分成不相交的分区, 每个分区包含具有相同 key 的元素,在其内部以 hash 的形式实现的。

但是 keyBy 算子一般不会单独使用,会和我们后面介绍的 Rolling Aggregation 算子和 reduce 算子集合使用。

Rolling Aggregation

Rolling Aggregation 算子又被称作滚动聚合算子,是在经过 keyBy 之后的 KeyedStream 做聚合操作。sum、min、max 分别表示求和、求最小值和最大值。minBy 和 maxBy 稍微有点特殊,我们接下来用代码来解释。

package com.vlab.operator

import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}

object KeyByOperator {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val data: DataStream[String] = env.readTextFile("/home/vlab/userlog.txt")
    val userLogStream = data.map(log => {
      val arr = log.split(" ")

      // 将读取的数据集转为 UserLog
      UserLog(arr(0), arr(1), arr(2).toInt)
    })

    userLogStream
      .keyBy("city")
      .sum("duration")
      .print()

    env.execute("KeyByOperator")
  }

  case class UserLog(name: String, city: String, duration: Int)
}

在上面的代码中,我们创建了一个 UserLog 样例类,然后读取我们前面准备的数据集,使用 map 函数将读取的日志转为 UserLog 对象,并使用 keyBy 算子按照 city 分组,最后对 duration 求和并打印结果。输出如下:

在这里插入图片描述

你可能会疑惑,输出的不应该是有两列,city 和 sum(duration)吗?请注意,我们这里的计算是流处理,而不是离线的批处理,我们创建的环境是 StreamExecutionEnvironment。程序从上往下一行一行读取文本,然后按照 city 字段分组,当执行到第一行的时候,只有它自己,所以输出自己本身。当执行到第二行的时候,cityChengdu 的也只有一行,所以也输出了它自己。当程序执行到第三行的时候,第二个 Chengdu 出现了,所以 sum 的结果是 900(300 + 600)。当程序执行到第四行的时候,Shanghai 第一次出现,所以也只有它自己。当程序执行到第 5 行的时候,第二个 Beijing 出现了,所以输出的是 500(100 + 400)。当程序执行到第 6 行,第二个 Shanghai 出现了,所以输出的是 400(200 + 200)。

min 和 max 的执行原理和 sum 类似,同样是按照从上往下的原则依次在同一个分组寻 找截止目前的最小值或者最大值,如果当前数据是该分组的第一条,则输出自己本身。接下来我们看一下 max 和 maxBy 的区别,其中 min 和 minBy 的区别也是类似。请看代码:

package com.vlab.operator

import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}

object KeyByOperator {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val data: DataStream[String] = env.readTextFile("/home/vlab/userlog.txt")
    val userLogStream = data.map(log => {
      val arr = log.split(" ")

      UserLog(arr(0), arr(1), arr(2).toInt)
    })

    userLogStream
      .keyBy("city")
      .max("duration")
      .print()

    userLogStream
      .keyBy("city")
      .maxBy("duration")
      .print()

    env.execute("KeyByOperator")
  }

  case class UserLog(name: String, city: String, duration: Int)
}

max 输出的结果应该是:
在这里插入图片描述

maxBy 输出的结果应该是:
在这里插入图片描述

乍眼一看好像并没有什么区别,但是请注意,这里要和原始数据进行对比,从第三行开始就不一致了。当程序执行到第三行的时候 Chengdu 分组中,duration 最大是 600,最大值对应的 name 是 William。maxBy 所对应的 name 正好是 William,而 max 对应的 name 依然是 Bob。同样在第五行第六行也有类似的情况出现。也就是说,max 和 maxBy 算子都会返回被聚合的最大值,但是 max 只会返回最大值,不会更新最大值所对应的其它字段,而 maxBy 会更新最大值对应的其它字段。

reduce

reduce 算子和 Spark 中的 reduceBy 的逻辑是一模一样的。它将 KeyedStream 转换成 DataStream,即⼀个有初始值的分组数据流的滚动折叠操作,合并当前元素和前⼀次折叠操作的结果,并产⽣⼀个新的值,返回的流中包含每⼀次折叠的结果,⽽不是只返回最后⼀次折叠的最终结果。

修改代码为如下:

package com.vlab.operator

import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}

object KeyByOperator {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val data: DataStream[String] = env.readTextFile("/home/vlab/userlog.txt")
    val userLogStream = data.map(log => {
      val arr = log.split(" ")

      UserLog(arr(0), arr(1), arr(2).toInt)
    })


    userLogStream
      .keyBy("city")
      .reduce((x, y) => UserLog(y.name, y.city, x.duration + y.duration))
      .print()

    env.execute("KeyByOperator")
  }

  case class UserLog(name: String, city: String, duration: Int)
}

在上面的代码中,我们使用 keyBy 算子对 UserLog 按照 city 做了分组,然后使用 reduce 算子对每个分组内的 UserLog 做了聚合。返回的 namecity 从后面一条日志中取,duration 是两条日志中的 duration 相加的结果。最终输出如下:
在这里插入图片描述

实验总结

在本节实验中我们介绍了 Flink 中基于 Key 的算子,keyBy 和 reduce 好理解,但是 Rolling Aggregation 算子的输出结果可能会和大家预想的不一样,特别是 min(minBy)和 max(maxBy),请一定要注意它们之间的区别。需要自己练习,确定效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值