《從0到1學習Flink》—— Flink 讀取 Kafka 資料批量寫入到 MySQL

weixin_33763244發表於2019-02-24

<!-- more -->

前言

之前其實在 《從0到1學習Flink》—— 如何自定義 Data Sink ? 文章中其實已經寫了點將資料寫入到 MySQL,但是一些配置化的東西當時是寫死的,不能夠通用,最近知識星球裡有朋友叫我: 寫個從 kafka 中讀取資料,經過 Flink 做個預聚合,然後建立資料庫連線池將資料批量寫入到 mysql 的例子。

於是才有了這篇文章,更多提問和想要我寫的文章可以在知識星球裡像我提問,我會根據提問及時回答和儘可能作出文章的修改。

準備

你需要將這兩個依賴新增到 pom.xml 中

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.34</version>
</dependency>

讀取 kafka 資料

這裡我依舊用的以前的 student 類,自己本地起了 kafka 然後造一些測試資料,這裡我們測試傳送一條資料則 sleep 10s,意味著往 kafka 中一分鐘發 6 條資料。

package com.zhisheng.connectors.mysql.utils;

import com.zhisheng.common.utils.GsonUtil;
import com.zhisheng.connectors.mysql.model.Student;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Properties;

/**
 * Desc: 往kafka中寫資料,可以使用這個main函式進行測試
 * Created by zhisheng on 2019-02-17
 * Blog: http://www.54tianzhisheng.cn/tags/Flink/
 */
public class KafkaUtil {
    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, GsonUtil.toJson(student));
            producer.send(record);
            System.out.println("傳送資料: " + GsonUtil.toJson(student));
            Thread.sleep(10 * 1000); //傳送一條資料 sleep 10s,相當於 1 分鐘 6 條
        }
        producer.flush();
    }

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

從 kafka 中讀取資料,然後序列化成 student 物件。

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 -> GsonUtil.fromJson(string, Student.class)); //,解析字串成 student 物件

因為 RichSinkFunction 中如果 sink 一條資料到 mysql 中就會呼叫 invoke 方法一次,所以如果要實現批量寫的話,我們最好在 sink 之前就把資料聚合一下。那這裡我們開個一分鐘的視窗去聚合 Student 資料。

student.timeWindowAll(Time.minutes(1)).apply(new AllWindowFunction<Student, List<Student>, TimeWindow>() {
    @Override
    public void apply(TimeWindow window, Iterable<Student> values, Collector<List<Student>> out) throws Exception {
        ArrayList<Student> students = Lists.newArrayList(values);
        if (students.size() > 0) {
            System.out.println("1 分鐘內收集到 student 的資料條數是:" + students.size());
            out.collect(students);
        }
    }
});

寫入資料庫

這裡使用 DBCP 連線池連線資料庫 mysql,pom.xml 中新增依賴:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.1.1</version>
</dependency>

如果你想使用其他的資料庫連線池請加入對應的依賴。

這裡將資料寫入到 MySQL 中,依舊是和之前文章一樣繼承 RichSinkFunction 類,重寫裡面的方法:

package com.zhisheng.connectors.mysql.sinks;

import com.zhisheng.connectors.mysql.model.Student;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.List;

/**
 * Desc: 資料批量 sink 資料到 mysql
 * Created by zhisheng_tian on 2019-02-17
 * Blog: http://www.54tianzhisheng.cn/tags/Flink/
 */
public class SinkToMySQL extends RichSinkFunction<List<Student>> {
    PreparedStatement ps;
    BasicDataSource dataSource;
    private Connection connection;

    /**
     * open() 方法中建立連線,這樣不用每次 invoke 的時候都要建立連線和釋放連線
     *
     * @param parameters
     * @throws Exception
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        dataSource = new BasicDataSource();
        connection = getConnection(dataSource);
        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(List<Student> value, Context context) throws Exception {
        //遍歷資料集合
        for (Student student : value) {
            ps.setInt(1, student.getId());
            ps.setString(2, student.getName());
            ps.setString(3, student.getPassword());
            ps.setInt(4, student.getAge());
            ps.addBatch();
        }
        int[] count = ps.executeBatch();//批量後執行
        System.out.println("成功了插入了" + count.length + "行資料");
    }


    private static Connection getConnection(BasicDataSource dataSource) {
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        //注意,替換成自己本地的 mysql 資料庫地址和使用者名稱、密碼
        dataSource.setUrl("jdbc:mysql://localhost:3306/test");
        dataSource.setUsername("root");
        dataSource.setPassword("root123456");
        //設定連線池的一些引數
        dataSource.setInitialSize(10);
        dataSource.setMaxTotal(50);
        dataSource.setMinIdle(2);

        Connection con = null;
        try {
            con = dataSource.getConnection();
            System.out.println("建立連線池:" + con);
        } catch (Exception e) {
            System.out.println("-----------mysql get connection has exception , msg = " + e.getMessage());
        }
        return con;
    }
}

核心類 Main

核心程式如下:

public class Main {
    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 -> GsonUtil.fromJson(string, Student.class)); //
        student.timeWindowAll(Time.minutes(1)).apply(new AllWindowFunction<Student, List<Student>, TimeWindow>() {
            @Override
            public void apply(TimeWindow window, Iterable<Student> values, Collector<List<Student>> out) throws Exception {
                ArrayList<Student> students = Lists.newArrayList(values);
                if (students.size() > 0) {
                    System.out.println("1 分鐘內收集到 student 的資料條數是:" + students.size());
                    out.collect(students);
                }
            }
        }).addSink(new SinkToMySQL());

        env.execute("flink learning connectors kafka");
    }
}

執行專案

執行 Main 類後再執行 KafkaUtils.java 類!

下圖是往 Kafka 中傳送的資料:

下圖是執行 Main 類的日誌,會建立 4 個連線池是因為預設的 4 個並行度,你如果在 addSink 這個運算元設定並行度為 1 的話就會建立一個連線池:

下圖是批量插入資料庫的結果:

總結

本文從知識星球一位朋友的疑問來寫的,應該都滿足了他的條件(批量/資料庫連線池/寫入mysql),的確網上很多的例子都是簡單的 demo 形式,都是單條資料就建立資料庫連線插入 MySQL,如果要寫的資料量很大的話,會對 MySQL 的寫有很大的壓力。這也是我之前在 《從0到1學習Flink》—— Flink 寫入資料到 ElasticSearch 中,資料寫 ES 強調過的,如果要提高效能必定要批量的寫。就拿我們現在這篇文章來說,如果資料量大的話,聚合一分鐘資料達萬條,那麼這樣批量寫會比來一條寫一條效能提高不知道有多少。

本文原創地址是: http://www.54tianzhisheng.cn/2019/01/15/Flink-MySQL-sink/ , 未經允許禁止轉載。

關注我

微信公眾號:zhisheng

另外我自己整理了些 Flink 的學習資料,目前已經全部放到微信公眾號了。你可以加我的微信:zhisheng_tian,然後回覆關鍵字:Flink 即可無條件獲取到。

更多私密資料請加入知識星球!

Github 程式碼倉庫

https://github.com/zhisheng17/flink-learning/

以後這個專案的所有程式碼都將放在這個倉庫裡,包含了自己學習 flink 的一些 demo 和部落格。

本文的專案程式碼在 https://github.com/zhisheng17/flink-learning/tree/master/flink-learning-connectors/flink-learning-connectors-mysql

相關文章

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 ?

8、《從0到1學習Flink》—— Flink Data transformation(轉換)

9、《從0到1學習Flink》—— 介紹Flink中的Stream Windows

10、《從0到1學習Flink》—— Flink 中的幾種 Time 詳解

11、《從0到1學習Flink》—— Flink 寫入資料到 ElasticSearch

12、《從0到1學習Flink》—— Flink 專案如何執行?

13、《從0到1學習Flink》—— Flink 寫入資料到 Kafka

14、《從0到1學習Flink》—— Flink JobManager 高可用性配置

15、《從0到1學習Flink》—— Flink parallelism 和 Slot 介紹

16、《從0到1學習Flink》—— Flink 讀取 Kafka 資料批量寫入到 MySQL

相關文章