Flink 之 MySQL二阶提交

本文开头附:Flink 学习路线系列 ^ _ ^

1.2PC简介

       两阶段提交(two-phase commit, 2PC)是最基础的分布式一致性协议,应用广泛。在分布式系统中,为了让每个节点都能够感知到其他节点的事务执行状况,需要引入一个中心节点来统一处理所有节点的执行逻辑,这个中心节点叫做协调者(coordinator),被中心节点调度的其他业务节点叫做参与者(participant)。

       2PC。顾名思义,2PC将分布式事务分成了两个阶段,两个阶段分别为提交请求(投票)和提交(执行)。协调者根据参与者的响应来决定是否需要真正地执行事务,具体流程如下。

       ZooKeeper 中的数据一致性,也是用到了2PC协议。如需了解,请参考:ZooKeeper数据一致性协议—2PC协议

2.Flink 中的 2PC 应用

       在 Flink 中,2PC的最常见应用场景其实是关系型数据库,比如MySQL InnoDB存储引擎的XA事务系统。

       Flink作为流式处理引擎,自然也提供了对 Exactly-Once 语义的保证。端到端的 Exactly-Once 语义,是输入、处理逻辑、输出三部分协同作用的结果。Flink内部依托检查点机制(CheckPoint)轻量级分布式快照算法(SnapShot)来保证 Exactly-Once。而要实现精确一次的输出逻辑,则需要施加以下两种限制之一:幂等性写入(idempotent write)、事务性写入(transactional write)。

       在文章:自定义Redis Sink 中,我们有介绍过 Redis 的容错机制是 At Least Once,我们通过幂等操作,使用新数据覆盖旧数据的方式,以此来实现 Exactly-Once 。

       在 Spark Streaming 中,要实现事务性写入完全靠用户自己,框架本身并没有提供任何实现。但是在 Flink 中提供了基于 2PC 的 SinkFunction ,名为 TwoPhaseCommitSinkFunction,帮助我们做了一些基础的工作。

3.关系型数据库如何实现二阶提交

       在日常,我们使用到最多的还是关系型数据库(MySQL),但是针对关系型数据库,我们并不能通过幂等机制来实现 Exactly-Once。我们从 Flink官方文档 中可以了解到,将数据 Sink(下沉)到 Kafka 可以保证 Exactly-Once。
Flink 之 MySQL二阶提交_第1张图片
       Flink 已经为我们提供了实现 Exactly-Once 的 FlinkKafkaProducer 类。如下图所示:它实现了 TwoPhaseCommitSinkFunction类,并重写了其中的方法,通过 2PC (Two Phase Comit) 二阶提交的方式,实现了 Exactly-Once。
在这里插入图片描述
       使用关系型数据库 MySQL,开启 CheckPoint 机制的前提下,为了保证前一次 CheckPoint 成功后到这次 CheckPoint 成功之前这段时间内的数据不丢失,如果执行到一半过程任务失败了,从而导致前一次CheckPoint成功后到任务失败前的数据已经存储到了MySQL,然而这部分数据并没有写入到 CheckPoint。如果任务重启后,前一次CheckPoint成功后到任务失败前的数据便会再次写入MySQL,从而导致数据重复的问题。这种情况,便使用到了 TwoPhaseCommitSinkFunction类,以此来实现 MySQL 关系型数据库的二阶提交。

4.MySQL 二阶提交实现

4.1 DBConnectUtil.java

package com.ddky.twoPhase;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * 数据库连接工具类
 */
public class DBConnectUtil {

    private static final Logger log = LoggerFactory.getLogger(DBConnectUtil.class);

    /**
     * 获取连接
     *
     * @param url
     * @param user
     * @param password
     * @return
     * @throws SQLException
     */
    public static Connection getConnection(String url, String user, String password) throws SQLException {
        Connection conn = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            log.error("获取mysql.jdbc.Driver失败");
            e.printStackTrace();
        }
        try {
            conn = DriverManager.getConnection(url, user, password);
            log.info("获取连接:{" + conn + "} 成功...");
        }catch (Exception e){
            log.error("获取连接失败,url:" + url + ",user:" + user);
        }

        //设置手动提交
        conn.setAutoCommit(false);
        return conn;
    }

    /**
     * 提交事务
     */
    public static void commit(Connection conn) {
        if (conn != null) {
            try {
                conn.commit();
            } catch (SQLException e) {
                log.error("提交事务失败,Connection:" + conn);
                e.printStackTrace();
            } finally {
                close(conn);
            }
        }
    }

    /**
     * 事务回滚
     *
     * @param conn
     */
    public static void rollback(Connection conn) {
        if (conn != null) {
            try {
                conn.rollback();
            } catch (SQLException e) {
                log.error("事务回滚失败,Connection:" + conn);
                e.printStackTrace();
            } finally {
                close(conn);
            }
        }
    }

    /**
     * 关闭连接
     *
     * @param conn
     */
    public static void close(Connection conn) {
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                log.error("关闭连接失败,Connection:" + conn);
                e.printStackTrace();
            }
        }
    }
}

4.2 MySQLTwoPhaseCommitSink.java

package com.ddky.twoPhase;

import org.apache.flink.api.common.ExecutionConfig;
import org.apache.flink.api.common.typeutils.base.VoidSerializer;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.typeutils.runtime.kryo.KryoSerializer;
import org.apache.flink.streaming.api.functions.sink.TwoPhaseCommitSinkFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Timestamp;

/**
 * 自定义 Kafka to mysql,继承TwoPhaseCommitSinkFunction,实现两阶段提交
 * @author ThinkPad
 */
public class MySqlTwoPhaseCommitSink extends TwoPhaseCommitSinkFunction<Tuple2<String,Integer>,Connection,Void> {

    private static final Logger log = LoggerFactory.getLogger(MySqlTwoPhaseCommitSink.class);

    public MySqlTwoPhaseCommitSink(){
        super(new KryoSerializer<>(Connection.class,new ExecutionConfig()), VoidSerializer.INSTANCE);
    }

    /**
     * 执行数据库入库操作  task初始化的时候调用
     * @param connection
     * @param tp
     * @param context
     * @throws Exception
     */
    @Override
    protected void invoke(Connection connection, Tuple2<String,Integer> tp, Context context) throws Exception {
        log.info("start invoke...");
        String value = tp.f0;
        Integer total = tp.f1;
        String sql = "insert into `t_test` (`value`,`total`,`insert_time`) values (?,?,?)";
        log.info("====执行SQL:{}===",sql);
        PreparedStatement ps = connection.prepareStatement(sql);
        ps.setString(1, value);
        ps.setInt(2, total);
        ps.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
        log.info("要插入的数据:{}----{}",value,total);
        if (ps != null) {
            String sqlStr = ps.toString().substring(ps.toString().indexOf(":")+2);
            log.error("执行的SQL语句:{}",sqlStr);
        }
        //执行insert语句
        ps.execute();
    }

    /**
     * 获取连接,开启手动提交事物(getConnection方法中)
     * @return
     * @throws Exception
     */
    @Override
    protected Connection beginTransaction() throws Exception {
        log.info("start beginTransaction.......");
        String url = "jdbc:mysql://192.168.86.245:3306/rh_datatool?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&autoReconnect=true";
        Connection connection = DBConnectUtil.getConnection(url, "root", "123456");
        return connection;
    }

    /**
     *预提交,这里预提交的逻辑在invoke方法中
     * @param connection
     * @throws Exception
     */
    @Override
    protected void preCommit(Connection connection) throws Exception {
        log.info("start preCommit...");
    }

    /**
     * 如果invoke方法执行正常,则提交事务
     * @param connection
     */
    @Override
    protected void commit(Connection connection) {
        log.info("start commit...");
        DBConnectUtil.commit(connection);
    }

    /**
     * 如果invoke执行异常则回滚事物,下一次的checkpoint操作也不会执行
     * @param connection
     */
    @Override
    protected void abort(Connection connection) {
        log.info("start abort rollback...");
        DBConnectUtil.rollback(connection);
    }
}

4.3 StreamDemoKafka2Mysql.java

package com.ddky.twoPhase;

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.runtime.state.filesystem.FsStateBackend;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.CheckpointConfig;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;

import java.util.Properties;

/**
 * 消费 Kafka 中的消息,Sink(自定义)到 MySQL中,保证 Kafka to MySQL 的 Exactly-Once
 */

public class StreamDemoKafka2Mysql {
    //topic
    private static final String topic_ExactlyOnce = "test_TwoPhaseCommit";

    public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //设置并行度,为了方便测试,查看消息的顺序,这里设置为1,可以更改为多并行度
        env.setParallelism(1);
        //checkpoint的设置
        //每隔10s进行启动一个检查点【设置checkpoint的周期】
        env.enableCheckpointing(30000);
        //设置模式为:exactly_one,仅一次语义
        env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
        //确保检查点之间有1s的时间间隔【checkpoint最小间隔】
        env.getCheckpointConfig().setMinPauseBetweenCheckpoints(1000);
        //检查点必须在10s之内完成,或者被丢弃【checkpoint超时时间】
        env.getCheckpointConfig().setCheckpointTimeout(10000);
        //同一时间只允许进行一次检查点
        env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
        //表示一旦Flink程序被cancel后,会保留checkpoint数据,以便根据实际需要恢复到指定的checkpoint
        env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
        //设置statebackend,将检查点保存在hdfs上面,默认保存在内存中。这里先保存到本地
        env.setStateBackend(new FsStateBackend("file:///D:/study_workspace/flink_demo/flink-java/StateBackEnd"));

        //设置kafka消费参数
        Properties properties = new Properties();
        properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.204.210:9092,192.168.204.211:9092,192.168.204.212:9092");
        properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, topic_ExactlyOnce);

        /*SimpleStringSchema可以获取到kafka消息,JSONKeyValueDeserializationSchema可以获取都消息的key,value,metadata:topic,partition,offset等信息*/
        FlinkKafkaConsumer<String> kafkaConsumer011 = new FlinkKafkaConsumer<>(
                topic_ExactlyOnce,
                new SimpleStringSchema(),
                properties);

        //加入kafka数据源
        DataStreamSource<String> streamSource = env.addSource(kafkaConsumer011);

        SingleOutputStreamOperator<Tuple2<String, Integer>> tupleStream = streamSource.map(str -> Tuple2.of(str, 1)).returns(Types.TUPLE(Types.STRING, Types.INT));

        tupleStream.print();
        //数据传输到下游
        tupleStream.addSink(new MySqlTwoPhaseCommitSink()).name("MySqlTwoPhaseCommitSink");
        //触发执行
        env.execute("StreamDemoKafka2Mysql");
    }
}

附代码:

       MySQL二阶提交代码


博主写作不易,来个关注呗

求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙

博主不能保证写的所有知识点都正确,但是能保证纯手敲,错误也请指出,望轻喷 Thanks♪(・ω・)ノ

你可能感兴趣的:(Flink)