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的大致过程如上,如有问题,还请见谅!