【Flink流式计算框架】:Flink中的API

1.Environment

1.1.getExecutionEnvironment

创建一个执行环境,表示当前执行程序的上下文。如果程序是独立调用的,则此方法返回本地执行环境;如果从命令行客户端调用程序以提交到集群,则此方法返回此集群的执行环境,也就是说,getExecutionEnvironment 会根据查询运行的方式决定返回什么样的运行环境,是最常用的一种创建执行环境的方式。

val env: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment

val env = StreamExecutionEnvironment.getExecutionEnvironment

如果没有设置并行度,会以flink-conf.yaml中的parallelism.default配置为准,默认是1。

1.2.local模式

1.2.1.createLocalEnvironment

返回本地执行环境,需要在调用时指定默认的并行度。

val env = StreamExecutionEnvironment.createLocalEnvironment(1)

1.2.2createLocalEnvironmentWithWebUI

首先,需要添加对应的依赖

<dependency>
	<groupId>org.apache.flink</groupId>
	<artifactId>flink-runtime-web_${scala.binary.ersion}</artifactId>
	<version>${flink.version}</version>
</dependency>

修改执行环境代码为

StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration());

程序运行起来打开 http://localhost:8081/,可以查看任务运行情况。

1.3createRemoteEnvironment

返回集群执行环境,将Jar 提交到远程服务器。需要在调用时指定JobManager的IP 和端口号,并指定要在集群中运行的Jar 包。如下代码

val env = ExecutionEnvironment.createRemoteEnvironment("jobmanage-hostname",6123,"/usr/local/wordcount.jar")

 

2.source

source是程序的数据源输入,你可以通过StreamExecutionEnvironment.addSource(sourceFunction)来为你的程序添加一个source。

flink提供了大量的已经实现好的source方法,也可以自定义source:

1. 通过实现sourceFunction接口来自定义无并行度的source

2. 通过实现ParallelSourceFunction 接口 or 继承RichParallelSourceFunction 来自定义有并行度的source

大多数情况下,我们使用自带的source即可。

2.1获取source的方式(自带的)

(1)基于文件

readTextFile(path):读取文本文件,文件遵循TextInputFormat 读取规则,逐行读取并返回。

(2)基于socket

socketTextStream:从socker中读取数据,元素可以通过一个分隔符切开。

(3)基于集合

fromCollection(Collection) :通过java 的collection集合创建一个数据流,集合中的所有元素必须是相同类型的。

(4)扩展数据源

addSource:可以实现读取第三方数据源的数据系统内置提供了一批connectors,连接器会提供对应的source支持【kafka】

实践代码如下:

// 定义样例类,温度传感器
case class SensorReading( id: String, timestamp: Long, temperature: Double )

object SourceTest {
  def main(args: Array[String]): Unit = {
    // 创建执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 1. 从集合中读取数据
    val dataList = List(
      SensorReading("sensor_1", 1547718199, 35.8),
      SensorReading("sensor_6", 1547718201, 15.4),
      SensorReading("sensor_7", 1547718202, 6.7),
      SensorReading("sensor_10", 1547718205, 38.1)
    )
    val stream1 = env.fromCollection(dataList)
//    env.fromElements(1.0, 35, "hello")

    // 2. 从文件中读取数据
    val inputPath = "E:\workspace\FlinkCase11\FlinkBase\src\main\resources\sensor.txt"
    val stream2 = env.readTextFile(inputPath)

    // 3. 从kafka中读取数据
    val properties = new Properties()
    properties.setProperty("bootstrap.servers", "localhost:9092")
    properties.setProperty("group.id", "consumer-group")
    val stream3 = env.addSource( new FlinkKafkaConsumer011[String]("sensor", new SimpleStringSchema(), properties) )

    // 4. 自定义Source
    val stream4 = env.addSource( new MySensorSource() )

    stream4.print()

    // 执行
    env.execute("source test")
  }
}

// 自定义SourceFunction
class MySensorSource() extends SourceFunction[SensorReading]{
  // 定义一个标识位flag,用来表示数据源是否正常运行发出数据
  var running: Boolean = true

  override def cancel(): Unit = running = false

  override def run(ctx: SourceFunction.SourceContext[SensorReading]): Unit = {
    // 定义一个随机数发生器
    val rand = new Random()

    // 随机生成一组(10个)传感器的初始温度: (id,temp)
    var curTemp = 1.to(10).map( i => ("sensor_" + i, rand.nextDouble() * 100) )

    // 定义无限循环,不停地产生数据,除非被cancel
    while(running){
      // 在上次数据基础上微调,更新温度值
      curTemp = curTemp.map(
        data => (data._1, data._2 + rand.nextGaussian())
      )
      // 获取当前时间戳,加入到数据中,调用ctx.collect发出数据
      val curTime = System.currentTimeMillis()
      curTemp.foreach(
        data => ctx.collect(SensorReading(data._1, curTime, data._2))
      )
      // 间隔500ms
      Thread.sleep(500)
    }
  }
}

自定义单并行度数据源与多并行度数据源的区别在于实现的接口不同。分别是SourceFunction和ParallelSourceFunction。

public class MyNoParalleSource implements SourceFunction<Long> {
    private long number = 1L;
    private boolean isRunning = true;

    @Override
    public void run(SourceContext<Long> sct) throws Exception {
        while (isRunning) {
            sct.collect(number);
            number++;
            //每秒生成一条数据
            Thread.sleep(1000);
        }
    }

    @Override
    public void cancel() {
        isRunning = false;
    }
}

 

3.Transform

因为有做过Spark任务开发,在此对于一些常用的transform不做介绍。主要是介绍大数据开发中少见但是又在Flink中常用的算子。

3.1KeyedStream

DataStream → KeyedStream:逻辑地将一个流拆分成不相交的分区,每个分区包含具有相同key 的元素,在内部以hash的形式实现的。

val dataStream = inputStream
  .map( data => {
	val arr = data.split(",")
	SensorReading(arr(0), arr(1).toLong, arr(2).toDouble)
  } )
val resultStream = dataStream
  .keyBy("id")
  .reduce( (curState, newData) =>
	SensorReading( curState.id, newData.timestamp, curState.temperature.min(newData.temperature) )
  )

3.2Union

合并多个流,新的流会包含所有流中的数据,但是union是一个限制,就是所有合并的流类型必须是一致的。 例如,下面的案例是每隔5秒进行一次求和

DataStream → DataStream:对两个或者两个以上的DataStream 进行union 操作,产生一个包含所有DataStream 元素的新DataStream。

//获取Flink的运行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
//获取数据源
// a
val source1 = env.socketTextStream("192.168.72.111", 7777).map((_,1))
// b
val source2 = env.socketTextStream("192.168.72.111", 8888).map((_,1))
// (a,2)
//把source1和source2组装到一起
val source = source1.union(source2)
//每5秒钟处理一次数据
val sum = source.timeWindowAll(Time.seconds(5)).sum(1)
//打印结果
sum.print.setParallelism(1)
env.execute("UnionTest")

 如果直接调用source.print()。会直接将所有的元素打印出来。

3.3connect,conMap和conFlatMap

connect:和union类似,但是只能连接两个流,两个流的数据类型可以不同,会对两个流中的数据应用不同的处理方法

DataStream,DataStream → ConnectedStreams:连接两个保持他们类型的数据流,两个数据流被Connect 之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。

 CoMap,CoFlatMap:

ConnectedStreams → DataStream:作用于ConnectedStreams 上,功能与map和flatMap 一样,对ConnectedStreams 中的每一个Stream分别进行map 和flatMap处理。

 

Connect与Union的区别:

1.Union 之前两个流的类型必须是一样,Connect 可以不一样,在之后的coMap中再去调整成为一样的。
2.Connect 只能操作两个流,Union 可以操作多个。

3.4Split 和Select

根据规则把一个数据流切分为多个流
应用场景:
可能在实际工作中,源数据流中混合了多种类似的数据,多种类型的数据处理规则不一样,所以就可以在根据一定的规则,把一个数据流切分成多个数据流,这样每个数据流就可以使用不用的处理逻辑了。

Split:DataStream → SplitStream:根据某些特征把一个DataStream 拆分成两个或者多个DataStream。

Select:SplitStream→DataStream:从一个SplitStream 中获取一个或者多个DataStream。

 val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    // 0.读取数据
    val inputPath = "E:\\workspace\\FlinkCase11\\FlinkBase\\src\\main\\resources\\sensor.txt"
    val inputStream = env.readTextFile(inputPath)

    // 1.先转换成样例类类型(简单转换操作)
    val dataStream = inputStream
      .map( data => {
        val arr = data.split(",")
        SensorReading(arr(0), arr(1).toLong, arr(2).toDouble)
      } )

    // 2. 多流转换操作
    // 2.1 分流,将传感器温度数据分成低温、高温两条流
    val splitStream = dataStream
      .split( data => {
        if( data.temperature > 30.0 ) Seq("high") else Seq("low")
      } )
    val highTempStream = splitStream.select("high")
    val lowTempStream = splitStream.select("low")
    val allTempStream = splitStream.select("high", "low")

    highTempStream.print("high")
    lowTempStream.print("low")
    allTempStream.print("all")

    // 2.2 合流,connect
    val warningStream = highTempStream.map( data => (data.id, data.temperature) )
    val connectedStreams = warningStream.connect(lowTempStream)
    // 用coMap对数据进行分别处理
    val coMapResultStream: DataStream[Any] = connectedStreams
      .map(
        waringData => (waringData._1, waringData._2, "warning") ,
        lowTempData => (lowTempData.id, "healthy")
      )
    coMapResultStream.print("111")
    // 2.3 union合流
    val unionStream = highTempStream.union(lowTempStream, allTempStream)

    unionStream.print("coMap")

    env.execute("transform test")

3.5MapPartition

类似map,一次处理一个分区的数据【如果在进行map处理的时候需要获取第三方资源链接,建议使用MapPartition】

3.6groupBy、first和sortGroup

//根据数据中的第一列进行分组,获取每组的前2个元素
text.groupBy(0).first(2).print();

//根据数据中的第一列分组,再根据第二列进行组内排序[升序],获取每组的前2个元素
text.groupBy(0).sortGroup(1, Order.ASCENDING).first(2).print();

//不分组,全局排序获取集合中的前3个元素,针对第一个元素升序,第二个元素倒序
text.sortPartition(0,Order.ASCENDING).sortPartition(1,Order.DESCENDING).first(3).print();

 

4.Sink

查看附录:flink已有的connector,可知道flink已经支持的一些sink。如常用的Kafka、Elasticsearch等。

4.1自定义Sink

这里以flink写入mysql为例:

object JDBCSinkDemo {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    // 读取数据
    val inputPath = "D:\\Projects\\BigData\\FlinkTutorial\\src\\main\\resources\\sensor.txt"
    val inputStream = env.readTextFile(inputPath)

    val stream = env.addSource( new MySensorSource() )

    // 先转换成样例类类型(简单转换操作)
    val dataStream = inputStream
      .map(data => {
        val arr = data.split(",")
        SensorReading(arr(0), arr(1).toLong, arr(2).toDouble)
      })

    stream.addSink( new MyJdbcSinkFunc() )

    env.execute("jdbc sink test")
  }
}

class MyJdbcSinkFunc() extends RichSinkFunction[SensorReading]{
  // 定义连接、预编译语句
  var conn: Connection = _
  var insertStmt: PreparedStatement = _
  var updateStmt: PreparedStatement = _

  //
  override def open(parameters: Configuration): Unit = {
    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456")
    insertStmt = conn.prepareStatement("insert into example (id, temp) values (?, ?)")
    updateStmt = conn.prepareStatement("update sensor_temp set temp = ? where id = ?")
  }

  // 每条记录插入时调用一次
  override def invoke(value: SensorReading): Unit = {
    // 先执行更新操作,查到就更新
    updateStmt.setDouble(1, value.temperature)
    updateStmt.setString(2, value.id)
    updateStmt.execute()
    // 如果更新没有查到数据,那么就插入
    if( updateStmt.getUpdateCount == 0 ){
      insertStmt.setString(1, value.id)
      insertStmt.setDouble(2, value.temperature)
      insertStmt.execute()
    }
  }

  override def close(): Unit = {
    insertStmt.close()
    updateStmt.close()
    conn.close()
  }

}

 

 

 

 

附录:flink已有的connector

Connectors provide code for interfacing with various third-party systems. Currently these systems are supported:

Additional streaming connectors for Flink are being released through Apache Bahir, including:

具体可查看https://ci.apache.org/projects/flink/flink-docs-release-1.11/dev/connectors/

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值