Flink-Exactly-once一致性系列实践2

Flink-Exactly-once系列实践-KafkaToMysql


简述

这里我们要实现的是flink流式计算里面的kafka->mysql的一致性测试,目的是,使用flink的两阶段提交来完成,两阶段提交要求外部系统需要支持事务,我们都知道mysql很明显是支持事务的,对于事务要么都成功,要么都失败,这里我们需要测试的就是一致性,当发生异常时,数据并不会被写入Mysql的模拟实现。

一、Mysql建立测试表

在这里插入图片描述
这里我们建立一张简单的测试表,需要注意的是我们后续只涉及到了插入语句,因为主要是实现一致性的测试,并没有涉及过于复杂,这里表以word为主键,count对应其出现的次数(这里肯定都是一次了,可以考虑后面如果插入异常,然后可以执行更新操作哦)!

二、编写MysqlTwoPhaseCommitSink方法

public class MysqlTwoPhaseCommitSink<T> extends TwoPhaseCommitSinkFunction<T,MysqlConnectState,Void> {

    public String sql;

    PreparedStatement preparedStatement;


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

    @Override
    protected void invoke(MysqlConnectState transaction, T value, Context context) throws Exception {
        sql="insert into test.kafkatomysqltest values(?,?)";
        Connection connection = transaction.connection;
        System.out.println("拿到连接");
        preparedStatement = connection.prepareStatement(sql);
        System.out.println("执行预编译sql");
        Class<?> aClass = value.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (int i = 1; i <= fields.length; i++) {
            fields[i-1].setAccessible(true);
            Object o = fields[i - 1].get(value);
            System.out.println(o);
            preparedStatement.setObject(i,o);
        }
        preparedStatement.executeUpdate();
        System.out.println("预编译execute");
    }

    @Override
    protected MysqlConnectState beginTransaction() throws Exception {
        return new MysqlConnectState();
    }

    @Override
    protected void preCommit(MysqlConnectState transaction) throws Exception {
        System.out.println("执行预提交中!!!!!!");
    }

    @SneakyThrows
    @Override
    protected void commit(MysqlConnectState transaction) {
        Connection connection = transaction.connection;
        System.out.println("正在提交数据");
        try {
            connection.commit();
            System.out.println("正在提交数据");
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }finally {
            if(connection!=null){
                connection.close();
            }
        }
    }

    @SneakyThrows
    @Override
    protected void abort(MysqlConnectState transaction) {
        Connection connection = transaction.connection;
        try {
            connection.rollback();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }finally {
            if(connection!=null){
                connection.close();
            }
        }
    }
}

自定义Mysql两阶段提交详细分解步骤以及注意点

2.1继承TwoPhaseCommitSinkFunction

首先TwoPhaseCommitSinkFunction这个父类是一个抽象类,继承的时候其有三个参数:
IN :数据流中的数据类型
TXN : 存储处理事务所需的所有信息
CONTEXT :上下文,该上下文将在给定对象的所有调用中共享
这里我的理解是:
对于IN我们使用了泛型,目的是为了可以接收不同的类型的数据,增强复用性,这里使用泛型就需要注意写法如下

public class MysqlTwoPhaseCommitSink<T> extends TwoPhaseCommitSinkFunction<T,TXN,CONTEXT> {

对于TXN,这里其实对应的是事务也就是transaction,而对于mysql来说事务相关的信息无非就是Connection也就是数据库连接,拿到后我们就可以开启事务,并且提交或者选择回滚,但是这里我们简单实现,并没有用连接池因此我们创建了一个类,用来拿到唯一的connection,如下

public class MysqlConnectState  {

    //transient,这里目的是后续不需要参与序列化,不然flink的kyro序列化器会报错,final,保证我们每次的connection都是唯一的,满足事务
    public final transient Connection connection;

    public MysqlConnectState() throws Exception {
        InputStream in = MysqlConnectState.class.getClassLoader().getResourceAsStream("mysql.properties");
        Properties properties=new Properties();
        properties.load(in);
        Class.forName(properties.getProperty("driver"));
        this.connection = DriverManager.getConnection(properties.getProperty("url"),properties.getProperty("username"),properties.getProperty("password"));
        connection.setAutoCommit(false);
    }
}

这个地方需要注意的有三点:
1.connection 需要是final的,保证该实例唯一,满足事务
2.transient关键字,为了让connection不加入序列化,Kyro序列化器有问题
3. connection.setAutoCommit(false);设置自动提交关闭,开启事务。

对于CONTEXT,这里我们并不需要上下文,因此使用Void,需要注意的是对于构造函数因为我们继承自TwoPhaseCommitSinkFunction,该抽象类中的构造函数需要传入两个序列化的实例对象

	public TwoPhaseCommitSinkFunction(
			TypeSerializer<TXN> transactionSerializer,
			TypeSerializer<CONTEXT> contextSerializer)

子类继承父类之后,获取到了父类的内容(属性/字段),而这些内容在使用之前必须先初始化,所以必须先调用父类的构造函数进行内容的初始化,如果父类默认是无参构造,则默认调用父类无参构造,如果没有无参构造,就需要子类先调用父类的有参构造,否则父类的内容无法初始化,如下

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

这里KryoSerializer<>以及VoidSerializer.INSTANCE可以看到都是TypeSerializer<>的子类,一次我们可以这样构造,注意的是我们这里传入的都是实例对象

2.2创建实体类

实体POJO,用于对象的获取,如下

public class Pojo {
    private String word;
    private Integer wordcount;

    public Pojo(String word, Integer wordcount) {
        this.word = word;
        this.wordcount = wordcount;
    }
}

2.3具体TwoPhaseCommitSinkFunction的方法的实现(顺序)

####2.3.1 beginTransaction
开启事务
####2.3.1 invoke
这里我们把预编译的操作实际放在invoke里面,并且我们使用的反射来获取传入的class的属性字段

        Class<?> aClass = value.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (int i = 1; i <= fields.length; i++) {
            fields[i-1].setAccessible(true);
            Object o = fields[i - 1].get(value);
            System.out.println(o);
            preparedStatement.setObject(i,o);
        }

注意setAccessible(),因为我们前面pojo里面是private,需要关闭校验
使用field.get(Object)来获取属性字段,并且注意预编译从1开始。
####2.3.3 preCommit
预提交,这里我们放在前面的invoke方法里面了
####2.3.4 commit
connection.commit();
真正提交事务,别忘了关闭connect连接哦
####2.3.5 abort
connection.rollback();
如果前面包括任务某个阶段发生错误,触发回滚,别忘了关闭connect连接哦!

三、编写主任务方法(涉及kafka输入流请参考上篇)

代码如下(示例):

public class KafkaToMysql {

    public static void main(String[] args) throws Exception{
        //1.获取流式执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //把connection加入序列化,否则在flink里面无法序列化
        env.getConfig().addDefaultKryoSerializer(Connection.class, TBaseSerializer.class);
        //1.1设置并行度
        env.setParallelism(1);
        //2.设置CheckPoint和StateBackend
        CkAndStateBacked.setCheckPointAndStateBackend(env,"FS");

        //3.获取KafkaSourceStream
        InputStream in = KafkaToMysql.class.getClassLoader().getResourceAsStream("kafka.properties");
        ParameterTool parameterTool=ParameterTool.fromPropertiesFile(in);
        SimpleStringSchema simpleStringSchema = new SimpleStringSchema();
        Class<? extends SimpleStringSchema> aClass = simpleStringSchema.getClass();
        DataStream<String> kafkaDataStream = KafkaUtil.getKafkaDataStream(parameterTool, aClass, env);

        //4.map转换(value,1)
        SingleOutputStreamOperator<Tuple2<String, Integer>> mapStream = kafkaDataStream.map(new MapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(String value) throws Exception {
                if("error".equals(value)){
                    throw new RuntimeException("主动触发异常!!!");
                }
                return new Tuple2<>(value, 1);
            }
        });


        //5.key并且分组聚合
        SingleOutputStreamOperator<Tuple2<String, Integer>> reduceStream = mapStream.keyBy(new KeySelector<Tuple2<String, Integer>, String>() {
            @Override
            public String getKey(Tuple2<String, Integer> value) throws Exception {
                return value.f0;
            }
        }).reduce(new ReduceFunction<Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
                return new Tuple2<>(value1.f0, value1.f1 + value2.f1);
            }
        });

        //6.转换为pojo
        SingleOutputStreamOperator<Pojo> pojoStream = reduceStream.map(new MapFunction<Tuple2<String, Integer>, Pojo>() {
            @Override
            public Pojo map(Tuple2<String, Integer> value) throws Exception {
                return new Pojo(value.f0, value.f1);
            }
        });

        //7.将数据输出到Mysql
        pojoStream.addSink(new MysqlTwoPhaseCommitSink<Pojo>());
        
        //7.任务执行
        env.execute();
    }
}

简单介绍下流程以及验证过程

3.1 首先获取kafka输入流,开启kafka生产者如下图

在这里插入图片描述

3.2 启动程序,并且在kafka生产者处生产数据,这里我们程序里面设置了error时会触发异常

在这里插入图片描述

3.3 可以看到flink按照checkpoint的时间间隔来提交当前CheckPoint之间的数据,当触发异常时,数据并没有写到mysql

在这里插入图片描述
在这里插入图片描述

总结

Flink两阶段提交,这里首先对于数据源,当开启CheckPoint的时候,Kafka的offset默认提交关闭,会根据CheckPoint每次保存到状态里面,从而保证源端的一致性,对于MysqlSink端这里因为支持事务,所以实现两阶段提交方法,当任务都正常,则正常提交,否则回滚。
Flink实现kafka->Mysql的大致过程如上,如有问题,还请见谅!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值