《从0到1学习Flink》—— 如何自定义 Data Sink ?

<!--more-->

前言

前篇文章 《从0到1学习Flink》—— Data Sink 介绍 介绍了 Flink Data Sink,也介绍了 Flink 自带的 Sink,那么如何自定义自己的 Sink 呢?这篇文章将写一个 demo 教大家将从 Kafka Source 的数据 Sink 到 MySQL 中去。

准备工作

我们先来看下 Flink 从 Kafka topic 中获取数据的 demo,首先你需要安装好了 FLink 和 Kafka 。

运行启动 Flink、Zookepeer、Kafka,

好了,都启动了!

数据库建表

DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(25) COLLATE utf8_bin DEFAULT NULL,
  `password` varchar(25) COLLATE utf8_bin DEFAULT NULL,
  `age` int(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

实体类

Student.java

package com.zhisheng.flink.model;

/**
 * Desc:
 * weixin: zhisheng_tian
 * blog: http://www.54tianzhisheng.cn/
 */
public class Student {
    public int id;
    public String name;
    public String password;
    public int age;

    public Student() {
    }

    public Student(int id, String name, String password, int age) {
        this.id = id;
        this.name = name;
        this.password = password;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

工具类

工具类往 kafka topic student 发送数据

import com.alibaba.fastjson.JSON;
import com.zhisheng.flink.model.Metric;
import com.zhisheng.flink.model.Student;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 往kafka中写数据
 * 可以使用这个main函数进行测试一下
 * weixin: zhisheng_tian
 * blog: http://www.54tianzhisheng.cn/
 */
public class KafkaUtils2 {
    public static final String broker_list = "localhost:9092";
    public static final String topic = "student";  //kafka topic 需要和 flink 程序用同一个 topic

    public static void writeToKafka() throws InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", broker_list);
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        KafkaProducer producer = new KafkaProducer<String, String>(props);

        for (int i = 1; i <= 100; i++) {
            Student student = new Student(i, "zhisheng" + i, "password" + i, 18 + i);
            ProducerRecord record = new ProducerRecord<String, String>(topic, null, null, JSON.toJSONString(student));
            producer.send(record);
            System.out.println("发送数据: " + JSON.toJSONString(student));
        }
        producer.flush();
    }

    public static void main(String[] args) throws InterruptedException {
        writeToKafka();
    }
}

SinkToMySQL

该类就是 Sink Function,继承了 RichSinkFunction ,然后重写了里面的方法。在 invoke 方法中将数据插入到 MySQL 中。

package com.zhisheng.flink.sink;

import com.zhisheng.flink.model.Student;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

/**
 * Desc:
 * weixin: zhisheng_tian
 * blog: http://www.54tianzhisheng.cn/
 */
public class SinkToMySQL extends RichSinkFunction<Student> {
    PreparedStatement ps;
    private Connection connection;

    /**
     * open() 方法中建立连接,这样不用每次 invoke 的时候都要建立连接和释放连接
     *
     * @param parameters
     * @throws Exception
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        connection = getConnection();
        String sql = "insert into Student(id, name, password, age) values(?, ?, ?, ?);";
        ps = this.connection.prepareStatement(sql);
    }

    @Override
    public void close() throws Exception {
        super.close();
        //关闭连接和释放资源
        if (connection != null) {
            connection.close();
        }
        if (ps != null) {
            ps.close();
        }
    }

    /**
     * 每条数据的插入都要调用一次 invoke() 方法
     *
     * @param value
     * @param context
     * @throws Exception
     */
    @Override
    public void invoke(Student value, Context context) throws Exception {
        //组装数据,执行插入操作
        ps.setInt(1, value.getId());
        ps.setString(2, value.getName());
        ps.setString(3, value.getPassword());
        ps.setInt(4, value.getAge());
        ps.executeUpdate();
    }

    private static Connection getConnection() {
        Connection con = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8", "root", "root123456");
        } catch (Exception e) {
            System.out.println("-----------mysql get connection has exception , msg = "+ e.getMessage());
        }
        return con;
    }
}

Flink 程序

这里的 source 是从 kafka 读取数据的,然后 Flink 从 Kafka 读取到数据(JSON)后用阿里 fastjson 来解析成 student 对象,然后在 addSink 中使用我们创建的 SinkToMySQL,这样就可以把数据存储到 MySQL 了。

package com.zhisheng.flink;

import com.alibaba.fastjson.JSON;
import com.zhisheng.flink.model.Student;
import com.zhisheng.flink.sink.SinkToMySQL;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.PrintSinkFunction;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer011;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer011;

import java.util.Properties;

/**
 * Desc:
 * weixin: zhisheng_tian
 * blog: http://www.54tianzhisheng.cn/
 */
public class Main3 {
    public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        props.put("zookeeper.connect", "localhost:2181");
        props.put("group.id", "metric-group");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("auto.offset.reset", "latest");

        SingleOutputStreamOperator<Student> student = env.addSource(new FlinkKafkaConsumer011<>(
                "student",   //这个 kafka topic 需要和上面的工具类的 topic 一致
                new SimpleStringSchema(),
                props)).setParallelism(1)
                .map(string -> JSON.parseObject(string, Student.class)); //Fastjson 解析字符串成 student 对象

        student.addSink(new SinkToMySQL()); //数据 sink 到 mysql

        env.execute("Flink add sink");
    }
}

结果

运行 Flink 程序,然后再运行 KafkaUtils2.java 工具类,这样就可以了。

如果数据插入成功了,那么我们查看下我们的数据库:

数据库中已经插入了 100 条我们从 Kafka 发送的数据了。证明我们的 SinkToMySQL 起作用了。是不是很简单?

项目结构

怕大家不知道我的项目结构,这里发个截图看下:

最后

本文主要利用一个 demo,告诉大家如何自定义 Sink Function,将从 Kafka 的数据 Sink 到 MySQL 中,如果你项目中有其他的数据来源,你也可以换成对应的 Source,也有可能你的 Sink 是到其他的地方或者其他不同的方式,那么依旧是这个套路:继承 RichSinkFunction 抽象类,重写 invoke 方法。

关注我

转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/10/31/flink-create-sink/

另外我自己整理了些 Flink 的学习资料,目前已经全部放到微信公众号了。你可以加我的微信:zhisheng_tian,然后回复关键字:Flink 即可无条件获取到。

相关文章

1、《从0到1学习Flink》—— Apache Flink 介绍

2、《从0到1学习Flink》—— Mac 上搭建 Flink 1.6.0 环境并构建运行简单程序入门

3、《从0到1学习Flink》—— Flink 配置文件详解

4、《从0到1学习Flink》—— Data Source 介绍

5、《从0到1学习Flink》—— 如何自定义 Data Source ?

6、《从0到1学习Flink》—— Data Sink 介绍

7、《从0到1学习Flink》—— 如何自定义 Data Sink ?

<think>我们正在讨论如何在ApacheFlink自定义KafkaSink。根据引用[2],Flink提供了KafkaSink的构建器,其中关键步骤是设置序列化器(RecordSerializer)。自定义Kafka Sink的核心在于实现自定义的序列化逻辑。用户问题:如何在Flink自定义Kafka Sink?回答步骤:1.理解KafkaSink的基本构建方式。2.自定义序列化器(KafkaRecordSerializationSchema)的实现方法。3.示例代码展示。注意:引用[1]说明KafkaSinkFlink与Kafka集成的连接器,用于可靠传输数据。引用[2]给出了使用KafkaSink的基本代码结构,其中使用了SimpleStringSchema作为默认的序列化器。要自定义序列化,我们需要实现自己的序列化模式。具体实现:自定义序列化器需要实现`KafkaRecordSerializationSchema`接口(在Flink1.15+中,它是KafkaRecordSerializationSchema,而在之前版本可能是KeyedSerializationSchema,但根据引用[2]的代码,现在推荐使用KafkaRecordSerializationSchema)。接口要求实现两个方法(以Flink1.15+为例):-`open`方法:用于初始化,例如可以初始化Kafka生产者相关的配置(如果需要)。- `serialize`方法:将流中的元素转换为ProducerRecord(即Kafka的消息)。但是,根据Flink官方文档,通常我们使用`KafkaRecordSerializationSchema.builder()`来构建,而自定义序列化器主要是通过实现`SerializationSchema`(用于值的序列化)或`KafkaRecordSerializationSchema`(更灵活,可以设置主题、分区等)。实际上,在Flink1.15+中,自定义序列化器有两种方式:方式1:使用`KafkaRecordSerializationSchema.builder()`并设置一个`SerializationSchema`(如SimpleStringSchema)作为值的序列化器,同时还可以设置主题、分区等。方式2:直接实现`KafkaRecordSerializationSchema`接口,这样可以完全控制ProducerRecord的生成。用户想要自定义,可能涉及到更复杂的逻辑,因此我们重点介绍方式2。步骤:1.创建一个类实现`KafkaRecordSerializationSchema<T>`接口(注意:泛型T是流中元素的类型)。2.实现`serialize`方法,该方法将流元素转换为ProducerRecord(指定主题、分区、键、值等)。3.在构建KafkaSink时,使用`setRecordSerializer`方法设置这个自定义的序列化器。示例:假设我们有一个自定义的POJO类`MyEvent`,我们想将它序列化为Kafka消息,其中主题可能是动态的,并且消息体是JSON格式。首先,添加依赖(在pom.xml中): ```xml<dependency><groupId>org.apache.flink</groupId><artifactId>flink-connector-kafka</artifactId><version>${flink.version}</version> </dependency>```然后,自定义序列化器: ```javaimport org.apache.flink.connector.kafka.sink.KafkaRecordSerializationSchema; importorg.apache.kafka.clients.producer.ProducerRecord;import com.fasterxml.jackson.databind.ObjectMapper;//使用Jackson进行JSON序列化publicclass MyEventKafkaSerializationSchemaimplements KafkaRecordSerializationSchema<MyEvent>{private Stringtopic;privateObjectMapperobjectMapper;public MyEventKafkaSerializationSchema(String topic) {this.topic= topic;}@Overridepublic voidopen(SerializationSchema.InitializationContext context, KafkaSinkContext sinkContext){//初始化objectMapper =new ObjectMapper();}@OverridepublicProducerRecord<byte[],byte[]>serialize(MyEventelement,KafkaSinkContextcontext,Long timestamp) {try{//将MyEvent对象序列化为JSON字符串,再转为字节数组byte[]value =objectMapper.writeValueAsBytes(element);//这里我们假设没有键(key),所以为null//如果主题是固定的,则使用构造时传入的主题returnnew ProducerRecord<>(topic,null,value);}catch (Exception e) {thrownew IllegalArgumentException("Couldnot serializerecord:" +element,e);}}} ```注意:在Flink1.15+中,`KafkaRecordSerializationSchema`接口的`serialize`方法返回`ProducerRecord<byte[], byte[]>`,并且方法参数包括元素、上下文和时间戳。然后,在构建KafkaSink时使用这个自定义序列化器: ```javaDataStream<MyEvent>stream =...;KafkaSink<MyEvent> sink= KafkaSink.<MyEvent>builder().setBootstrapServers("broker1:9092,broker2:9092").setRecordSerializer(newMyEventKafkaSerializationSchema("target-topic")).setDeliveryGuarantee(DeliveryGuarantee.AT_LEAST_ONCE) //设置交付保证.build(); stream.sinkTo(sink);```如果我们需要动态主题(例如根据事件中的某个字段确定主题),则可以在`serialize`方法中根据元素决定主题名称。例如,修改自定义序列化器:```javapublicclass DynamicTopicKafkaSerializationSchemaimplements KafkaRecordSerializationSchema<MyEvent>{private ObjectMapper objectMapper;@Overridepublicvoid open(InitializationContextcontext,KafkaSinkContextsinkContext) {objectMapper =new ObjectMapper();}@OverridepublicProducerRecord<byte[], byte[]> serialize(MyEvent element, KafkaSinkContext context, Longtimestamp){try {//假设MyEvent有一个getTargetTopic()方法返回目标主题StringtargetTopic =element.getTargetTopic();byte[]value =objectMapper.writeValueAsBytes(element);returnnew ProducerRecord<>(targetTopic, null, value);} catch(Exceptione){throw newIllegalArgumentException("Could notserialize record: "+ element, e);}} }```然后构建sink时:```java.setRecordSerializer(newDynamicTopicKafkaSerializationSchema())```注意事项:-自定义序列化器需要确保序列化是线程安全的,因为Flink会并行调用。-异常处理:在序列化过程中,如果发生异常,需要根据业务需求决定是抛出异常(导致任务失败)还是记录错误并跳过。引用说明:[^1]介绍了KafkaSink的基本作用。[^2]提供了Flink KafkaSink的基本构建方式,其中关键点在于设置RecordSerializer。[^3]是一篇博客的标题,可能包含更多细节,但未提供具体内容,因此我们主要依据官方文档方式。因此,自定义Kafka Sink的核心在于实现自定义的`KafkaRecordSerializationSchema`接口,并在构建KafkaSink时设置这个自定义的序列化器。</think>### Apache Flink 自定义 Kafka Sink 实现方法 在 Apache Flink自定义 Kafka Sink 的核心是实现**自定义序列化器**(`KafkaRecordSerializationSchema`),用于控制数据如何转换为 Kafka 的 ProducerRecord。以下是详细实现步骤: --- #### 一、自定义序列化器实现 需实现 `KafkaRecordSerializationSchema<T>` 接口,主要重写 `serialize()` 方法: ```java import org.apache.flink.connector.kafka.sink.KafkaRecordSerializationSchema; import org.apache.kafka.clients.producer.ProducerRecord; public class CustomKafkaSerializer<T> implements KafkaRecordSerializationSchema<T> { private final String topic; private final ObjectMapper objectMapper = new ObjectMapper(); public CustomKafkaSerializer(String topic) { this.topic = topic; } @Override public ProducerRecord<byte[], byte[]> serialize( T element, KafkaSinkContext context, Long timestamp ) { try { // 1. 自定义序列化逻辑(示例:JSON序列化) byte[] value = objectMapper.writeValueAsBytes(element); // 2. 动态分区分配(示例:根据字段哈希) int partition = Math.abs(element.getKey().hashCode()) % 4; // 3. 构造ProducerRecord return new ProducerRecord<>( topic, // 目标Topic partition, // 分区ID null, // Key(可为null) value // 序列化后的Value ); } catch (Exception e) { throw new RuntimeException("Serialization failed", e); } } } ``` --- #### 二、集成到 KafkaSinkFlink 作业中配置自定义序列化器: ```java // 1. 创建自定义序列化器实例 KafkaRecordSerializationSchema<MyEvent> serializer = new CustomKafkaSerializer<>("target-topic"); // 2. 构建KafkaSink KafkaSink<MyEvent> sink = KafkaSink.<MyEvent>builder() .setBootstrapServers("kafka-broker1:9092,kafka-broker2:9092") .setRecordSerializer(serializer) // 注入自定义序列化器 .setDeliveryGuarantee(DeliveryGuarantee.AT_LEAST_ONCE) // 交付语义 .setProperty("transaction.timeout.ms", "600000") // 调优参数 .build(); // 3. 将Sink添加到DataStream dataStream.sinkTo(sink); ``` --- #### 三、关键配置说明 1. **序列化控制** - 在 `serialize()` 中自由实现: - 数据格式转换(JSON/Protobuf/Avro) - 动态 Topic 选择(基于数据字段) - 自定义分区策略(替代默认轮询) 2. **交付语义保证** - `AT_LEAST_ONCE`:至少一次(默认) - `EXACTLY_ONCE`:精确一次(需开启 Kafka 事务) - `NONE`:无保证(高性能但可能丢失数据) 3. **性能调优参数** ```java .setProperty("batch.size", "16384") // 批次大小 .setProperty("linger.ms", "100") // 批次等待时间 .setProperty("compression.type", "lz4")// 压缩算法 ``` --- #### 四、应用场景 1. **动态路由** 根据数据内容分发到不同 Topic(如日志分级处理)。 2. **复杂格式转换** 将 Flink 的 POJO 转换为 Protobuf 等紧凑格式[^3]。 3. **分区优化** 自定义分区策略避免数据倾斜(如按用户ID哈希)。 --- #### 五、注意事项 1. **线程安全** 序列化器会在多个并行实例中被调用,确保无状态或使用 ThreadLocal。 2. **异常处理** 在 `serialize()` 中妥善处理序列化失败,避免作业崩溃。 3. **Schema注册** 若使用 Avro/Protobuf,需集成 Schema Registry(如 Confluent Schema Registry)。 通过自定义序列化器,可实现高度灵活的 Kafka 数据写入逻辑,满足复杂业务场景需求[^1][^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值