前言
前几天学习了一下Flink的状态编程,于是自己想了个需求实现一下来加深对状态编程的理解。
需求是这样的:给出一个配置文件,用户在配置文件中定义库名,表名,以及 key列 和 value列等等,通过Flink从kafka中读取这样的数据,继而写入到Doris,在Doris中自动创建库创建表(该表根据配置文件构建,字段及字段类型与数据保持一致,是否分区表由配置文件决定)
状态编程
什么是有状态编程
通俗的说就是当发生一个或者多个事件
时,将这些事件信息保存
起来的操作,这些操作就是有状态编程。
什么情况下需要进行状态编程
比如在风控场景下,我们可能需要了解到哪些事件可能是一个风险事件,并进行报警,在传感器应用中,假设当连续的两次温度之差不在阈值范围内,我们需要进行报警时,那么我们可以将上一次的温度进行保存,与本次温度进行比较。
当然也有需要聚合的场景,比如每分钟每小时每天数据的聚合状态,我们可以将数据保存在状态中来进行操作。
如何进行状态编程
首先应该先了解状态是如何被保存的,这里简单介绍下Keyed State,KeyedState 是通过key-value形式进行保存的,这种状态仅仅适用于 Keyed Streams。当 kv流进入invoke方法中,我们定义的 KeyedState 会与 key 流中的key进行绑定,每个key都会携带自身的 状态。如下图:
代码实现
简单了解后,我们开始coding
① 首先开始构造一条KV流,可以通过keyBy()算子实现。
SingleOutputStreamOperator<JSONObject> map = dataStreamSource.map(data -> {
JSONObject jsonObject = JSONObject.parseObject(data);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String partitionDate = sdf.format(
new Date(Long.parseLong(jsonObject.get("date_received").toString().substring(0, 10)) * 1000L));
jsonObject.put("partition_date", partitionDate);
return jsonObject;
});
KeyedStream<JSONObject, String> result = map.keyBy(new KeySelector<JSONObject, String>() {
@Override
public String getKey(JSONObject value) throws Exception {
return value.get("partition_date").toString();
}
});
② 在定义的Function中重写open方法。
当第一条数据到达时,首先需要对Doris中的库/表进行检查,是否存在库/表,如果不存在,那么需要建库建表,如果存在,那么直接执行插入。如果每个分区中每条数据来临时都要去连接数据库并检查,那么是不是有点过分了。此时,我将Doris中数据库/表状态保存在状态中,下次数据到来时,只需要通过读取状态中的值来进行判断是不是好很多呢?于是就采用了状态编程试一下。
//定义一个状态,保存当前是否与数据库链接
private ValueState<Boolean> flag;
//定义一个状态,保存sql
private ValueState<String> sql;
//定义一个状态,保存列
private ValueState<ArrayList<String>> colList;
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
ValueStateDescriptor<Boolean> valueStateDescriptor = new ValueStateDescriptor<>("isExistsTable", Boolean.class);
ValueStateDescriptor<String> sqlValueStateDescriptor = new ValueStateDescriptor<>("sql", String.class);
ValueStateDescriptor<ArrayList<String>> colListValueStateDescriptor =
new ValueStateDescriptor("colList", ArrayList.class);
flag = getRuntimeContext().getState(valueStateDescriptor);
sql = getRuntimeContext().getState(sqlValueStateDescriptor);
colList = getRuntimeContext().getState(colListValueStateDescriptor);
//获取Connection
conn = getConnection();
}
在open方法中先创建了三个valueStateDescriptior,第一个用来保存是否存在表,第二个用来保存首次根据数据构建的sql,第三个保存首次根据数据获取到的所有列。
② 在invoke()方法中使用state
@Override
public void invoke(JSONObject value, Context context) throws Exception {
super.invoke(value,context);
//获取状态,是否已经存在表
if(flag.value() == null || !flag.value()){
logger.info("check table ...");
cols.addAll(value.keySet());
colList.update(cols);
sql.update(SQLUtil.getInsertSQL(databaseName,tableName,colList.value()));
boolean existsTable = DorisClient.isExistsTable(databaseName, tableName);
if(!existsTable){
logger.info("create table ...");
boolean isCreate = creatDatabase(databaseName, tableName, keyCols, partitionKey, distributeKey, valueCols);
flag.update(isCreate);
}else{
flag.update(true);
}
}
if(sql.value() != null ){
preparedStatement = conn.prepareStatement(sql.value());
}
int i =0;
for (String key : colList.value()) {
i++;
preparedStatement.setObject(i,value.getString(key));
}
//数据刷写
flush();
}