1、JdbcSink
用于DataStream增加Jdbc的Sink输出,主要两个接口:sink()和exactlyOnceSink()。其中exactlyOnceSink()是13版本新增的支持事务性的接口,本次主要介绍sink()接口。
public static SinkFunction sink(
String sql,
JdbcStatementBuilder statementBuilder,
JdbcExecutionOptions executionOptions,
JdbcConnectionOptions connectionOptions) {
return new GenericJdbcSinkFunction<>(
new JdbcBatchingOutputFormat<>(
new SimpleJdbcConnectionProvider(connectionOptions),
executionOptions,
context -> {
Preconditions.checkState(
!context.getExecutionConfig().isObjectReuseEnabled(),
"objects can not be reused with JDBC sink function");
return JdbcBatchStatementExecutor.simple(
sql, statementBuilder, Function.identity());
},
JdbcBatchingOutputFormat.RecordExtractor.identity()));
}
1.1、参数
接口有四个参数,其中第三个参数executionOptions可以省略使用默认值,具体样例参看1、JdbcSink方式
sql
String类型,一个SQL语句模板,就是通常使用的PreparedStatement那种形式,例如:insert into wordcount (wordcl, countcl) values (?,?)
statementBuilder
JdbcStatementBuilder类型,作用是完成流数据与SQL具体列的对应,基于上一个参数的PreparedStatement形式,完成对应关系
executionOptions
Flink Jdbc输出的执行规则,主要设置执行触发机制,主要设置三个参数:数据量触发阈值、时间触发阈值、最大重试次数。其中,数据量触发默认为5000,时间触发默认为0,即关闭时间触发。注意触发阈值不要设置的过低,否则可能造成数据库的阻塞。
connectionOptions
JdbcConnectionOptions类型,用于设置数据库连接属性,包括Url、Driver、Username、Password等
1.2、返回
接口返回的是一个基于SinkFunction实现的GenericJdbcSinkFunction类,其核心是参数JdbcBatchingOutputFormat。
GenericJdbcSinkFunction的结果核心方法如下,都是基于JdbcBatchingOutputFormat的操作。
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
RuntimeContext ctx = getRuntimeContext();
outputFormat.setRuntimeContext(ctx);
outputFormat.open(ctx.getIndexOfThisSubtask(), ctx.getNumberOfParallelSubtasks());
}
@Override
public void invoke(T value, Context context) throws IOException {
outputFormat.writeRecord(value);
}
@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
outputFormat.flush();
}
2、JdbcBatchingOutputFormat
JdbcBatchingOutputFormat是进行Jdbc交互的实现类,在向Jdbc输出前进行数据聚合
2.1、参数
接口有四个参数
JdbcConnectionProvider
提供Jdbc连接
JdbcExecutionOptions
执行参数
StatementExecutorFactory
Statement执行工厂,也就是流数据与数据库字段对应关系的处理
RecordExtractor
数据提取的执行类
2.2、open方法
Open方法是进行数据库连接初始化及前期准备的接口,存在调用关系
Task.doRun()
->invokable.invoke()->DataSinkTask.invoke()
->format.open()->JdbcBatchingOutputFormat.open()
2.2.1、连接数据库
Open方法的第一步是连接数据库,调用上层方法AbstractJdbcOutputFormat的open方法,之后调用JdbcConnectionProvider的实现类SimpleJdbcConnectionProvider的getOrEstablishConnection()方法建立连接,getOrEstablishConnection的具体操作如下
public Connection getOrEstablishConnection() throws SQLException, ClassNotFoundException {
if (connection != null) {
return connection;
}
if (jdbcOptions.getDriverName() == null) {
connection =
DriverManager.getConnection(
jdbcOptions.getDbURL(),
jdbcOptions.getUsername().orElse(null),
jdbcOptions.getPassword().orElse(null));
} else {
Driver driver = getLoadedDriver();
Properties info = new Properties();
jdbcOptions.getUsername().ifPresent(user -> info.setProperty("user", user));
jdbcOptions.getPassword().ifPresent(password -> info.setProperty("password", password));
connection = driver.connect(jdbcOptions.getDbURL(), info);
if (connection == null) {
// Throw same exception as DriverManager.getConnection when no driver found to match
// caller expectation.
throw new SQLException(
"No suitable driver found for " + jdbcOptions.getDbURL(), "08001");
}
}
return connection;
}
此处根据有没有设置Drive有两种处理。如果没有设置,会根据设置的URL自动解析,用到了Java的DriverManager,这个类用于管理Jdbc驱动。DriverManager会自动识别classpath里的Driver,然后可以根据URL自动解析配对。如果设置了Driver,那就直接加载这个Driver来进行连接处理。
2.2.2、JdbcExec
这个是基于StatementExecutorFactory创建的,此处最后使用的实现类是JdbcBatchStatementExecutor,在sink()接口中设定。这一步实际的操作就是做一个prepareStatements
@Override
public void prepareStatements(Connection connection) throws SQLException {
this.st = connection.prepareStatement(sql);
}
2.2.3、scheduler
数据库性能有限,所以Flink写数据库通常采用批的方式,此处就是设置时间调度的,具体参数可以参看第一章。需要注意的是两个特殊配置值:时间为0或者条数为1则不创建这个调度器。
if (executionOptions.getBatchIntervalMs() != 0 && executionOptions.getBatchSize() != 1) {
此处创建的调度线程池只包含一个线程
this.scheduler =
Executors.newScheduledThreadPool(
1, new ExecutorThreadFactory("jdbc-upsert-output-format"))
调度器最终执行的操作就是整个类最大的一点,flush数据到数据库
synchronized (JdbcBatchingOutputFormat.this) {
if (!closed) {
try {
flush();
} catch (Exception e) {
flushException = e;
}
}
}
2.3、writeRecord方法
writeRecord是类的核心方法,进行数据的写入。主要进行两个操作,将数据加入列表,达到条件时flush到数据库中。
try {
addToBatch(record, jdbcRecordExtractor.apply(record));
batchCount++;
if (executionOptions.getBatchSize() > 0
&& batchCount >= executionOptions.getBatchSize()) {
flush();
}
} catch (Exception e) {
throw new IOException("Writing records to JDBC failed.", e);
}
2.3.1、缓存数据
缓存数据使用的就是一个简单的ArrayList,其定义在SimpleBatchStatementExecutor
SimpleBatchStatementExecutor(
String sql, JdbcStatementBuilder statementBuilder, Function valueTransformer) {
this.sql = sql;
this.parameterSetter = statementBuilder;
this.valueTransformer = valueTransformer;
this.batch = new ArrayList<>();
}
如上,batch就是用于缓存数据的,添加数据操作如下。
@Override
public void addToBatch(T record) {
batch.add(valueTransformer.apply(record));
}
其中valueTransformer的作用就是返回输入,在sink初始时定义:
return JdbcBatchStatementExecutor.simple(
sql, statementBuilder, Function.identity());
/**
* Returns a function that always returns its input argument.
*
* @param the type of the input and output objects to the function
* @return a function that always returns its input argument
*/
static Function identity() {
return t -> t;
}
2.3.2、flush
flush就是把缓存数据向数据库刷出,最终调用的是SimpleBatchStatementExecutor的executeBatch方法
@Override
public void executeBatch() throws SQLException {
if (!batch.isEmpty()) {
for (V r : batch) {
parameterSetter.accept(st, r);
st.addBatch();
}
st.executeBatch();
batch.clear();
}
}