Datastream输出算子

输出算子(sink)

在这里插入图片描述

连接到外部系统

Flink 的 DataStream API 专门提供了向外部写入数据的方法:addSink。与 addSource类似,
addSink 方法对应着一个“Sink”算子,主要就是用来实现与外部系统连接、并将数据提交写
入的;Flink程序中所有对外的输出操作,一般都是利用Sink算子完成的。
与Source 算子非常类似,除去一些Flink预实现的Sink,一般情况下Sink算子的创建是
通过调用DataStream的addSink()方法实现的。
stream.addSink(new SinkFunction(…))
addSource 的参数需要实现一个SourceFunction接口;类似地,addSink方法同样需要传入
一个参数,实现的是SinkFunction接口。在这个接口中只需要重写一个方法invoke(),用来将指
定的值写入到外部系统中。这个方法在每条数据记录到来时都会调用:
default void invoke(IN value, Context context) throws Exception
当然,SinkFuntion 多数情况下同样并不需要我们自己实现。Flink官方提供了一部分的框
架的Sink连接器

如图所示,列出了Flink官方目前支持的第三方系统连接器:
在这里插入图片描述
我们可以看到,像Kafka之类流式系统,Flink提供了完美对接,Source/Sink两端都能连
接,可读可写;而对于Elasticsearch、文件系统(FileSystem)、 JDBC 等数据存储系统,则只
提供了输出写入的Sink连接器。
除Flink 官方之外,Apache Bahir 作为给Spark和Flink提供扩展支持的项目,也实现了一
些其他第三方系统与Flink的连接器 如图所示。

在这里插入图片描述
除此以外,就需要用户自定义实现Sink连接器了。

输出到文件

最简单的输出方式,当然就是写入文件了。对应着读取文件作为输入数据源,Flink 本来
也有一些非常简单粗暴的输出到文件的预实现方法:如 writeAsText()、writeAsCsv(),可以直
接将输出结果保存到文本文件或Csv文件。目前这些简单的方法已经要被弃用。
Flink 为此专门提供了一个流式文件系统的连接器:StreamingFileSink,它继承自抽象类
RichSinkFunction,而且集成了 Flink 的检查点(checkpoint)机制,用来保证精确一次(exactly
once)的一致性语义。
StreamingFileSink 为批处理和流处理提供了一个统一的Sink,它可以将分区文件写入Flink
支持的文件系统。它可以保证精确一次的状态一致性,大大改进了之前流式文件输出的方式。
它的主要操作是将数据写入桶(buckets),每个桶中的数据都可以分割成一个个大小有限的分
区文件。
StreamingFileSink 支持行编码(Row-encoded)和批量编码(Bulk-encoded,比如 Parquet)
格式。这两种不同的方式都有各自的构建器(builder),调用方法也非常简单,可以直接调用
StreamingFileSink 的静态方法:
⚫ 行编码:StreamingFileSink.forRowFormat t(basePath,rowEncoder)。
⚫ 批量编码:StreamingFileSink.forBulkFormat(basePath,bulkWriterFactory)。
在创建行或批量编码 Sink 时,我们需要传入两个参数,用来指定存储桶的基本路径
(basePath)和数据的编码逻辑(rowEncoder或bulkWriterFactory)。
下面我们就以行编码为例,将一些测试数据直接写入文件:

import org.apache.flink.api.common.serialization.SimpleStringEncoder 
import org.apache.flink.core.fs.Path 
import 
org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink 
import 
org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.Defa
 ultRollingPolicy 
73 
74 
 
import org.apache.flink.streaming.api.scala._ 
 
import java.util.concurrent.TimeUnit 
 
object SinkToFileTest { 
  def main(args: Array[String]): Unit = { 
    val env = StreamExecutionEnvironment.getExecutionEnvironment 
    env.setParallelism(4) 
 
    val stream = env.fromElements( 
      Event("Mary", "./home", 1000L), 
      Event("Bob", "./cart", 2000L), 
      Event("Alice", "./prod?id=100", 3000L), 
      Event("Alice", "./prod?id=200", 3500L), 
      Event("Bob", "./prod?id=2", 2500L), 
      Event("Alice", "./prod?id=300", 3600L), 
      Event("Bob", "./home", 3000L), 
      Event("Bob", "./prod?id=1", 2300L), 
      Event("Bob", "./prod?id=3", 3300L) 
    ) 
 
    val fileSink = StreamingFileSink 
      .forRowFormat( 
        new Path("./output"), 
        new SimpleStringEncoder[String]("UTF-8") 
      ) 
      //通过.withRollingPolicy()方法指定“滚动策略” 
      .withRollingPolicy( 
      DefaultRollingPolicy.builder() 
        .withRolloverInterval(TimeUnit.MINUTES.toMillis(15)) 
        .withInactivityInterval(TimeUnit.MINUTES.toMillis(5)) 
        .withMaxPartSize(1024 * 1024 * 1024) 
        .build() 
    ) 
      .build 
 
    stream.map(_.toString).addSink(fileSink) 
 
    env.execute() 
  } 
}

这里我们创建了一个简单的文件Sink,通过withRollingPolicy()方法指定了一个“滚动策
略”。上面的代码设置了在以下3种情况下,我们就会滚动分区文件:
⚫ 至少包含15分钟的数据
⚫ 最近5分钟没有收到新的数据
75

⚫ 文件大小已达到1 GB

输出到Kafka

Flink官方为Kafka提供了Source和Sink的连接器,我们可以用它方便地从Kafka读写数
据。Flink与Kafka的连接器提供了端到端的精确一次(exactly once)语义保证,这在实际项
目中是最高级别的一致性保证。
(1)添加Kafka 连接器依赖
由于我们已经测试过从Kafka数据源读取数据,连接器相关依赖已经引入,这里就不重复
介绍了。
(2)启动Kafka集群
(3)编写输出到Kafka的示例代码
我们可以直接将用户行为数据保存为文件clicks.csv,读取后不做转换直接写入Kafka,主
题(topic)命名为“clicks”

import org.apache.flink.api.common.serialization.SimpleStringSchema 
import org.apache.flink.streaming.api.scala._ 
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer 
 
import java.util.Properties 
 
object SinkToKafka { 
  def main(args: Array[String]): Unit = { 
    val env = StreamExecutionEnvironment.getExecutionEnvironment 
    env.setParallelism(1) 
 
    val properties = new Properties() 
    properties.put("bootstrap.servers", "master:9092") 
    val stream = env.readTextFile("input/clicks.csv") 
    stream 
      .addSink(new FlinkKafkaProducer[String]( 
        "clicks", 
        new SimpleStringSchema(), 
        properties 
      )) 
 
    env.execute() 
  } 
}

这里我们可以看到,addSink()方法传入的参数是一个FlinkKafkaProducer。
FlinkKafkaProducer继承了抽象类TwoPhaseCommitSinkFunction,这是一个实现了“两阶段提
76

交”的RichSinkFunction。两阶段提交提供了Flink向Kafka写入数据的事务性保证,能够真正
做到精确一次(exactly once)的状态一致性。
(4)运行代码,在Linux主机启动一个消费者, 查看是否收到数据

kafka-console-consumer.sh --bootstrap-server master:9092 --topic clicks

输出到Redis

Flink没有直接提供官方的Redis连接器,不过Bahir项目还是担任了合格的辅助角色,为
我们提供了Flink-Redis的连接工具。但版本升级略显滞后,目前连接器版本为1.1,支持的
Scala版本最新到2.11。
具体测试步骤如下:
(1)导入的Redis连接器依赖

<dependency> 
  <groupId>org.apache.bahir</groupId> 
  <artifactId>flink-connector-redis_2.11</artifactId> 
  <version>1.0</version> 
</dependency>

(2)启动Redis集群
(3)编写输出到Redis的示例代码
连接器为我们提供了一个RedisSink,它继承了抽象类RichSinkFunction,这就是已经实现
好的向Redis写入数据的SinkFunction。我们可以直接将Event数据输出到Redis:

import org.apache.flink.streaming.api.scala._ 
import org.apache.flink.streaming.connectors.redis.RedisSink 
import 
org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfi
 g 
import org.apache.flink.streaming.connectors.redis.common.mapper.{RedisCommand, 
RedisCommandDescription, RedisMapper} 
 
object SinkToRedis { 
  def main(args: Array[String]): Unit = { 
    val env = StreamExecutionEnvironment.getExecutionEnvironment 
    env.setParallelism(1) 
 
    val conf = new FlinkJedisPoolConfig.Builder().setHost("master").build() 
    env.addSource(new ClickSource) 
      .addSink(new RedisSink[Event](conf, new MyRedisMapper())) 
 
    env.execute() 
  } 
} 

这里RedisSink的构造方法需要传入两个参数:
77

⚫ JFlinkJedisConfigBase:Jedis的连接配置。
⚫ RedisMapper:Redis映射类接口,说明怎样将数据转换成可以写入Redis的类型。
接下来主要就是定义一个Redis的映射类,实现RedisMapper接口。

class MyRedisMapper extends RedisMapper[Event] { 
    override def getKeyFromData(t: Event): String = t.user 
 
    override def getValueFromData(t: Event): String = t.url 
 
    override def getCommandDescription: RedisCommandDescription = new 
RedisCommandDescription(RedisCommand.HSET, "clicks") 
  }

在这里我们可以看到,保存到Redis时调用的命令是HSET,所以是保存为哈希表(hash),
表名为“clicks”;保存的数据以user为key,以url为value,每来一条数据就会做一次转换。
(4)运行代码,Redis查看是否收到数据。

首先启动 redis服务

redis-server redis.conf

然后启动和客户端

redis-cli

输入密码
在这里插入图片描述

运行结果

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值