Flink之Sink

Flink 的 DataStream API 专门提供了向外部写入数据的方法:addSink。与 addSource 类似,addSink 方法对应着一个“Sink”算子,主要就是用来实现与外部系统连接、并将数据提交写入的;Flink 程序中所有对外的输出操作,一般都是利用 Sink 算子完成的。

与 Source 算子非常类似,除去一些 Flink 预实现的 Sink,一般情况下 Sink 算子的创建是通过调用 DataStream 的 addSink()方法实现的。

stream.addSink(new SinkFunction(…))

SinkFuntion 多数情况下同样并不需要我们自己实现。Flink 官方提供了一部分的框架的 Sink 连接器。

像 Kafka 之类流式系统,Flink 提供了完美对接,Source/Sink 两端都能连接,可读可写;而对于 Elasticsearch、文件系统(FileSystem)、JDBC 等数据存储系统,则只提供了输出写入的 Sink 连接器。

除 Flink 官方之外,Apache Bahir 作为给 Spark 和 Flink 提供扩展支持的项目,也实现了一些其他第三方系统与 Flink 的连接器。

除此以外,就需要用户自定义实现 Sink 连接器了。

输出到文件

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)。

    val stream = env.addSource(new ClickSource)
    val fileSink = StreamingFileSink
      .forRowFormat(new Path("./output"), new SimpleStringEncoder[String]("UTF-8"))
      .withRollingPolicy(
        DefaultRollingPolicy.builder()
          .withRolloverInterval(TimeUnit.MINUTES.toMillis(15)) //至少包含 15 分钟的数据
          .withMaxPartSize(128 * 1024 * 1024)//文件大小已达到 128m
          .withInactivityInterval(TimeUnit.MINUTES.toMillis(5)) // 最近 5 分钟没有收到新的数据
          .build()
      )
      .build()
    stream.map(_.toString).addSink(fileSink)

输出到kafka

Flink 官方为 Kafka 提供了 Source 和 Sink 的连接器,我们可以用它方便地从 Kafka 读写数据。Flink 与 Kafka 的连接器提供了端到端的精确一次(exactly once)语义保证,这在实际项目中是最高级别的一致性保证。

 val topic = "mytest"
    val bootstrap = "192.168.0.30:9092"

    stream.map(_.toString).addSink(new FlinkKafkaProducer[String](
      bootstrap,
      topic,
      new SimpleStringSchema()
    ))

输出到redis

增加pom文件中依赖

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

示例代码如下:

  class MyRedisSink extends RedisMapper[(String, String)] {
    override def getCommandDescription: RedisCommandDescription = new RedisCommandDescription(RedisCommand.SET)

    override def getKeyFromData(data: (String, String)): String = data._1

    override def getValueFromData(data: (String, String)): String = data._2
  }
val conf = new FlinkJedisPoolConfig.Builder()
       .setHost("192.168.0.30")
       .setDatabase(2)
       .build()

stream.addSink(new RedisSink[(String, String)](conf, new MyRedisSink))

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

⚫ JFlinkJedisConfigBase:Jedis 的连接配置。

⚫ RedisMapper:Redis 映射类接口,说明怎样将数据转换成可以写入 Redis 的类型。

输出到ElasticSearch

增加pom文件中依赖

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-elasticsearch6_${scala.binary.version}</artifactId>
    <version>${flink.version}</version>
</dependency>

代码如下:

    //输出到es
    val httpHosts = new util.ArrayList[HttpHost]()
    httpHosts.add(new HttpHost("192.168.0.30", 9200, "http"))
    // 定义一个ES sink function
    val esFun = new ElasticsearchSinkFunction[Event] {
      override def process(t: Event, runtimeContext: RuntimeContext, requestIndexer: RequestIndexer): Unit = {
        val data = new util.HashMap[String, String]()
        data.put("url", t.url)
        data.put("user", t.user)

        // 包装要发送的http请求
        val indexRequest = Requests.indexRequest()
          .index("clicks")
          .source(data)
          .`type`("_doc")
          .id(t.user + Random.nextInt(111))

        // 发送请求
        requestIndexer.add(indexRequest)
      }
    }
    stream.addSink(new ElasticsearchSink.Builder[Event](httpHosts, esFun).build())

与 RedisSink 类 似 , 连 接 器 也 为 我 们 实 现 了 写 入 到 Elasticsearch 的SinkFunction——ElasticsearchSink。区别在于,这个类的构造方法是私有(private)的,我们需要使用 ElasticsearchSink 的 Builder 内部静态类,调用它的 build()方法才能创建出真正的SinkFunction。

而 Builder 的构造方法中又有两个参数:

⚫ httpHosts:连接到的 Elasticsearch 集群主机列表。

⚫ elasticsearchSinkFunction:这并不是我们所说的 SinkFunction,而是用来说明具体处理逻辑、准备数据向 Elasticsearch 发送请求的函数。

具体的操作需要重写中 elasticsearchSinkFunction 中的 process()方法,我们可以将要发送的数据放在一个 HashMap 中,包装成 IndexRequest 向外部发送 HTTP 请求。

输出到MySQL(JDBC)

增加pom文件中依赖

<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-connector-jdbc_${scala.binary.version}</artifactId>
 <version>${flink.version}</version>
</dependency>
<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.47</version>
</dependency>

默认已经创建好相关表。

 //    输出到MySQL
    stream.addSink(
      JdbcSink.sink(
        "insert into clicks(user,url) values(?,?)",
        new JdbcStatementBuilder[Event] {
          override def accept(t: PreparedStatement, u: Event): Unit = {
            t.setString(1, u.user)
            t.setString(2, u.url)
          }
        },
        new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
          .withUrl("jdbc:mysql://localhost:3306/test")
          .withDriverName("com.mysql.jdbc.Driver")
          .withUsername("root")
          .withPassword("123456")
          .build()
      )
    )

自定义输出

与 Source 类似,Flink 为我们提供了通用的 SinkFunction 接口和对应的 RichSinkFunction抽象类,只要实现它,通过简单地调用 DataStream 的 addSink()方法就可以自定义写入任何外部存储。

### Flink CDC Sink 的使用指南 Flink CDC 提供了强大的功能用于处理数据流中的变更捕获和同步操作。Sink 是指将数据从 Flink 流写入目标存储系统的组件,常见的目标系统包括 TiDB、MySQL、PostgreSQL、HBase 和 Kafka 等[^1]。 #### 配置 Flink CDC Sink 为了配置 Flink CDC 的 Sink 功能,通常需要指定目标连接器以及相应的参数。以下是一个典型的 MySQL 数据库作为目标的示例: ```sql CREATE TABLE sink_table ( id INT, name STRING, price DOUBLE, ts BIGINT, dt STRING, PRIMARY KEY (id) NOT ENFORCED ) WITH ( 'connector' = 'jdbc', 'url' = 'jdbc:mysql://localhost:3306/sink_db?useSSL=false&serverTimezone=UTC', 'table-name' = 'sink_table', 'username' = 'root', 'password' = 'root-password' ); ``` 此 SQL 脚本创建了一个名为 `sink_table` 的表,并将其与 JDBC 连接器绑定。JDBC URL 指定了目标数据库的位置和其他必要的属性[^3]。 #### 常见问题及其解决方案 1. **无法找到合适的 JAR 文件** 如果在运行过程中遇到错误提示找不到特定的数据源或目标连接器,则可能是因为缺少所需的依赖项。解决方法是从 Maven 中央仓库下载正确的 JAR 文件并加载到作业环境中[^1]。 2. **主键冲突** 当尝试向目标表插入重复主键时会发生异常。可以通过设置唯一约束或者调整业务逻辑避免此类情况发生[^3]。 3. **性能优化** 对于大规模数据传输场景,建议启用批量模式以提高吞吐量。例如,在 JDBC 输出端可通过如下方式开启批处理: ```sql 'sink.buffer-flush.max-rows'='1000', 'sink.buffer-flush.interval'='1s' ``` 这些参数控制每次提交前缓冲的最大行数及时长间隔[^2]。 --- ### 示例代码:完整的 Flink CDC Pipeline 下面展示如何构建一个简单的 ETL 流程,其中包含 Source 和 Sink 表定义及查询语句。 ```sql -- 创建 Source 表 CREATE TABLE source_table ( id INT PRIMARY KEY NOT ENFORCED, name STRING, price DOUBLE ) WITH ( 'connector' = 'mysql-cdc', 'hostname' = 'source-host', 'port' = '3306', 'username' = 'user', 'password' = 'pass', 'database-name' = 'db_name', 'table-name' = 'tbl_name' ); -- 创建 Sink 表 CREATE TABLE sink_table ( id INT, name STRING, price DOUBLE, PRIMARY KEY(id) NOT ENFORCED ) WITH ( 'connector' = 'kafka', 'topic' = 'output_topic', 'properties.bootstrap.servers' = 'broker:9092', 'format' = 'json' ); -- 插入数据至 Sink INSERT INTO sink_table SELECT * FROM source_table; ``` 以上脚本展示了如何利用 MySQL-CDC 读取变化事件并通过 Kafka 发送出去。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不加班程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值