说明:以下所有都基于Flink1.11版本 代码都精简过了
完整例子
public class MysqlSinkExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> source = env.fromElements("a", "b", "c", "d");
//执行参数
JdbcExecutionOptions executionOptions = JdbcExecutionOptions.builder().withBatchIntervalMs(500L).withBatchSize(10000).build();
//数据库连接参数
JdbcConnectionOptions connectionOptions = (new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()).withUrl("jdbcurl").withDriverName("db.driver").withUsername("db.username").withPassword("monitor.db.password").build();
//sql
final String sql = "INSERT INTO letter (name) VALUES (?)";
//输出
source.addSink(JdbcSink.sink(
sql,
(st, r) -> {
st.setString(1, r);
},
executionOptions,
connectionOptions
));
env.execute("sink mysql");
}
}
上面需求就是将元素 "a" "b" "c" "d" 写入 letter 表!可以看到我们有配置
-
JdbcExecutionOptions 执行参数
-
batchIntervalMs :每次批量写入最小间隔时间
-
batchSize :批量写入的数据大小
-
maxRetries :插入发生异常重试次数 注意:只支持 SQLException 异常及其子类异常重试
-
-
JdbcConnectionOptions 连接参数配置
-
sql 写入的sql语句
-
里面还有一个 labmbda 函数实现它 本身是一个消费型labmbda函数
-
PreparedStatement :第一个参数
-
第二参数是元素 如代码中的a b c ...
写入数据成功时候,会不会想一些其他的问题 ?
如何实现批量写入的?
批量写入是不是会产生数据延迟?
我想转实时写入怎么配置?
数据会丢失吗?
源码
看源码之前看一张核心类关联图,请记住这三个核心类,后面都穿插着他们身影。
-

工厂类 JdbcSink
我们是通过 JdbcSink.sink(...) 构造一个 SinkFunction 提供给Flink输出数据!
-- org.apache.flink.connector.jdbc.JdbcSink
public class JdbcSink {
public static <T> SinkFunction<T> sink(
String sql,
JdbcStatementBuilder<T> statementBuilder,
JdbcExecutionOptions executionOptions,
JdbcConnectionOptions connectionOptions) {
return new GenericJdbcSinkFunction<>( // todo GenericJdbcSinkFunction
new JdbcBatchingOutputFormat<>( // todo JdbcBatchingOutputFormat 批量写出处理类
new SimpleJdbcConnectionProvider(connectionOptions), //todo Jdbc连接包装类
executionOptions, //todo 执行参数类 如重试次数 最大等待时间 最大等待条数
context -> {
Preconditions.checkState(
!context.getExecutionConfig().isObjectReuseEnabled(),
"objects can not be reused with JDBC sink function");
return JdbcBatchStatementExecutor.simple( //todo 返回的就是这个 SimpleBatchStatementExecutor 这里封装了 最终写入逻辑
sql,
statementBuilder,
Function.identity());
},
JdbcBatchingOutputFormat.RecordExtractor.identity()
));
}
根据此图结合看一下 核心类关系图!

这里我们不用去理解具体每个类作用,但是必须知道通过工厂构造出一个 GenericJdbcSinkFunction !
数据处理:GenericJdbcSinkFunction
Flink官方提供了 RichSinkFunction 供我们完成数据输出!它有两个接口供下游处理!open 函数初始化方法 在实际处理数据之前仅会调用一次 invoke 每进来一条数据都会调用一次
open(Configuration parameters)
invoke(T value, Context context)
我们再来看看 GenericJdbcSinkFunction 实现 RichSinkFunction 之后具体干了什么?
public class GenericJdbcSinkFunction<T> extends RichSinkFunction<T> {
private final AbstractJdbcOutputFormat<T > outputFormat;
public GenericJdbcSinkFunction(@Nonnull AbstractJdbcOutputFormat<T> outputFormat) {
this.outputFormat = Preconditions.checkNotNull(outputFormat);
}
@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);
}
}
简化代码发现:open执行时候会去调用 outputFormat.open(...) invoke 也只是调用了 outputFormat.writeRecord(value)
那么outputFormat是具体那个类实现? 回到构造工厂可以看到 里面构造了 JdbcBatchingOutputFormat

这个时候可以理解: AbstractJdbcOutputFormatoutputFormat = JdbcBatchingOutputFormatoutputFormat
初始化:JdbcBatchingOutputFormat#Open
public class JdbcBatchingOutputFormat extend AbstractJdbcOutputFormat{
private transient JdbcExec jdbcStatementExecutor;
@Override
public void open(int taskNumber, int numTasks) throws IOException {
//初始化jdbc连接
super.open(taskNumber, numTasks);
// todo 创建执行器 SimpleBatchStatementExecutor
jdbcStatementExecutor = createAndOpenStatementExecutor(statementExecutorFactory);
// todo 如果 配置输出到jdbc最小间隔不等于0 且最小条数不是1 就创建一个固定定时线程池
if (executionOptions.getBatchIntervalMs() != 0 && executionOptions.getBatchSize() != 1) {
this.scheduler = Executors.newScheduledThreadPool(
1,
new ExecutorThreadFactory("jdbc-upsert-output-format"));
this.scheduledFuture = this.scheduler.scheduleWithFixedDelay(
() -> {
synchronized (JdbcBatchingOutputFormat.this) {
if (!closed) {
try {
// todo 批量缓存的数据都写到jdbc
flush();
} catch (Exception e) {
flushException = e;
}
}
}
},
executionOptions.getBatchIntervalMs(),
executionOptions.getBatchIntervalMs(),
TimeUnit.MILLISECONDS);
}
}
}
其实这里就干了三件事情: 第一件事:super.open(taskNumber, numTasks)就是调用了抽象父类中 AbstractJdbcOutputFormat的open方法
public abstract class AbstractJdbcOutputFormat{
protected transient Connection connection;
protected final JdbcConnectionProvider connectionProvider;
@Override
public void open(int taskNumber, int numTasks) throws IOException {
try {
establishConnection();
} catch (Exception e) {
throw new IOException("unable to open JDBC writer", e);
}
}
protected void establishConnection() throws Exception {
//创建jdbc连接
connection = connectionProvider.getConnection();
}
}
而这个JdbcConnectionProvider这个类就是在在构造JdbcBatchingOutputFormat构造方法传入的SimpleJdbcConnectionProvider

SimpleJdbcConnectionProvider#getConnection 可以看到就是创建了Connection
public class SimpleJdbcConnectionProvider{
private final JdbcConnectionOptions jdbcOptions;
private transient volatile Connection connection;
@Override
public Connection getConnection() throws SQLException, ClassNotFoundException {
if (connection == null) {
synchronized (this) {
if (connection == null) {
Class.forName(jdbcOptions.getDriverName());
if (jdbcOptions.getUsername().isPresent()) {
connection = DriverManager.getConnection(jdbcOptions.getDbURL(), jdbcOptions.getUsername().get(), jdbcOptions.getPassword().orElse(null));
} else {
connection = DriverManager.getConnection(jdbcOptions.getDbURL());
}
}
}
}
return connection;
}
}
第二件事:createAndOpenStatementExecutor(statementExecutorFactory) 创建执行器 SimpleBatchStatementExecutor

然后调用 SimpleBatchStatementExecutor#prepareStatements(connection) 初始化了 PreparedStatement st
第二件事:根据条件判断是否创建固定定时启动线程池!这个线程池会根据你配置的时间定时启动去执行 flush 方法!这个 flush 方法主要作用定时将缓存的数据触发jdbc写入!
扩展一下:为什么还要判断 executionOptions.getBatchSize() != 1 呢?你想如果最大推送大小只有一条那就是来一条写入jdbc一条那么定时去触发写入就没啥意义了!如果想要实时写入那么也就是来一条我立马写入到jdbc。这个时候在flink内部不会有延迟的!
处理数据:JdbcBatchingOutputFormat#writeRecord
每条数据进来每次都会进入一下逻辑 我们宏观先看一遍 有个印象再来看细节
-
GenericJdbcSinkFunction#invoke
-
SimpleBatchStatementExecutor#addToBatch
-
JdbcBatchingOutputFormat#flush
-
SimpleBatchStatementExecutor#executeBatch
-
JdbcBatchingOutputFormat#writeRecord
设计思路:
我们想要实现批量写入,那就必须要有批量数据,可是我们数据一条条进来是不是先得有个容器容纳这些批量数据?当容器到达一定的数据量我们就得批量写出去,然后清空容器,再重新装数据。
flink的实现:
JdbcBatchingOutputFormat#writeRecord
@Override public final synchronized void writeRecord(In record) throws IOException { //写入数据之前发现有之前存在异常 就抛出异常 结束写入!否则则继续写入。 checkFlushException(); try { //将数据添加到容器 addToBatch(record, jdbcRecordExtractor.apply(record)); //计数器 统计容器已经有多少数据了 batchCount++; //判断计数器是不是达到 指定容量 if (executionOptions.getBatchSize() > 0 && batchCount >= executionOptions.getBatchSize()) { flush(); //达到就执行批量输出jdbc } } catch (Exception e) { throw new IOException("Writing records to JDBC failed.", e); } }JdbcBatchingOutputFormat#addToBatch 如何批量写入容器
protected void addToBatch(In original, JdbcIn extracted) throws SQLException { //这个jdbcStatementExecutor 前面说了就等于 SimpleBatchStatementExecutor jdbcStatementExecutor.addToBatch(extracted); }SimpleBatchStatementExecutor#addToBatch
class SimpleBatchStatementExecutor<T, V>{ private final List<V> batch; SimpleBatchStatementExecutor(...) { ... this.batch = new ArrayList<>(); } @Override public void addToBatch(T record) { batch.add(valueTransformer.apply(record)); } }可以看到内部使用一个ArrayList容器容纳待写入的元素, valueTransformer 这里的就是record数据进去record数据出来,没有做什么处理!系统留下的扩展!
系统批量写入jdbc时机?
有三个他们都调用了 JdbcBatchingOutputFormat#flush
-
JdbcBatchingOutputFormat#writeRecord 写入元素时候判断批量条数达到阈值调用
-
JdbcBatchingOutputFormat#open 上面讲了open里面启动一个定时调度器会调用
-
GenericJdbcSinkFunction#snapshotState 还有一个发生checkPoint时候会调用

-
JdbcBatchingOutputFormat#flush 方法解析
@Override
public synchronized void flush() throws IOException {
checkFlushException();
//循环判断是否小于最大重试次数
for (int i = 1; i <= executionOptions.getMaxRetries(); i++) {
try {
//执行jdbc批量写入 然后将统计数据条数值重置为0 跳出循环结束
attemptFlush();
batchCount = 0;
break;
} catch (SQLException e) {
LOG.error("JDBC executeBatch error, retry times = {}", i, e);
if (i >= executionOptions.getMaxRetries()) {
throw new IOException(e); //如果循环次数达到最大重试次数 就抛出异常
}
try {
//验证 连接是否有效 无效会重新生成connection和PreparedStatement
if (!connection.isValid(CONNECTION_CHECK_TIMEOUT_SECONDS)) {
connection = connectionProvider.reestablishConnection();
jdbcStatementExecutor.closeStatements();
jdbcStatementExecutor.prepareStatements(connection);
}
} catch (Exception excpetion) {
LOG.error(
"JDBC connection is not valid, and reestablish connection failed.",
excpetion);
throw new IOException("Reestablish JDBC connection failed", excpetion);
}
try {
Thread.sleep(1000 * i);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IOException(
"unable to flush; interrupted while doing another attempt",
e);
}
}
}
}
protected void attemptFlush() throws SQLException {
//这个jdbcStatementExecutor 前面说了就等于 SimpleBatchStatementExecutor
jdbcStatementExecutor.executeBatch();
}
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();
}
}
这个parameterSetter.accept(st, r) 方法就是调用我例子里面的
st = PreparedStatement r = 每一个元素
这里执行批量写入,然后清空ArrayList容器!
连接器参数Flink19 #
| 参数 | 是否必填 | 默认值 | 类型 | 描述 |
|---|---|---|---|---|
connector | 必填 | (none) | String | 指定使用什么类型的连接器,这里应该是'jdbc'。 |
url | 必填 | (none) | String | JDBC 数据库 url。 |
table-name | 必填 | (none) | String | 连接到 JDBC 表的名称。 |
driver | 可选 | (none) | String | 用于连接到此 URL 的 JDBC 驱动类名,如果不设置,将自动从 URL 中推导。 |
compatible-mode | 可选 | (none) | String | 数据库的兼容模式。 |
username | 可选 | (none) | String | JDBC 用户名。如果指定了 'username' 和 'password' 中的任一参数,则两者必须都被指定。 |
password | 可选 | (none) | String | JDBC 密码。 |
connection.max-retry-timeout | 可选 | 60s | Duration | 最大重试超时时间,以秒为单位且不应该小于 1 秒。 |
scan.partition.column | 可选 | (none) | String | 用于将输入进行分区的列名。请参阅下面的分区扫描部分了解更多详情。 |
scan.partition.num | 可选 | (none) | Integer | 分区数。 |
scan.partition.lower-bound | 可选 | (none) | Integer | 第一个分区的最小值。 |
scan.partition.upper-bound | 可选 | (none) | Integer | 最后一个分区的最大值。 |
scan.fetch-size | 可选 | 0 | Integer | 每次循环读取时应该从数据库中获取的行数。如果指定的值为 '0',则该配置项会被忽略。 |
scan.auto-commit | 可选 | true | Boolean | 在 JDBC 驱动程序上设置 auto-commit 标志, 它决定了每个语句是否在事务中自动提交。有些 JDBC 驱动程序,特别是 Postgres,可能需要将此设置为 false 以便流化结果。 |
lookup.cache | 可选 | NONE | 枚举类型 可选值: NONE, PARTIAL | 维表的缓存策略。 目前支持 NONE(不缓存)和 PARTIAL(只在外部数据库中查找数据时缓存)。 |
lookup.cache.max-rows | 可选 | (none) | Integer | 维表缓存的最大行数,若超过该值,则最老的行记录将会过期。 使用该配置时 "lookup.cache" 必须设置为 "PARTIAL”。请参阅下面的 Lookup Cache 部分了解更多详情。 |
lookup.partial-cache.expire-after-write | 可选 | (none) | Duration | 在记录写入缓存后该记录的最大保留时间。 使用该配置时 "lookup.cache" 必须设置为 "PARTIAL”。请参阅下面的 Lookup Cache 部分了解更多详情。 |
lookup.partial-cache.expire-after-access | 可选 | (none) | Duration | 在缓存中的记录被访问后该记录的最大保留时间。 使用该配置时 "lookup.cache" 必须设置为 "PARTIAL”。请参阅下面的 Lookup Cache 部分了解更多详情。 |
lookup.partial-cache.cache-missing-key | 可选 | true | Boolean | 是否缓存维表中不存在的键,默认为true。 使用该配置时 "lookup.cache" 必须设置为 "PARTIAL”。 |
lookup.max-retries | 可选 | 3 | Integer | 查询数据库失败的最大重试次数。 |
sink.buffer-flush.max-rows | 可选 | 100 | Integer | flush 前缓存记录的最大值,可以设置为 '0' 来禁用它。 |
sink.buffer-flush.interval | 可选 | 1s | Duration | flush 间隔时间,超过该时间后异步线程将 flush 数据。可以设置为 '0' 来禁用它。注意, 为了完全异步地处理缓存的 flush 事件,可以将 'sink.buffer-flush.max-rows' 设置为 '0' 并配置适当的 flush 时间间隔。 |
sink.max-retries | 可选 | 3 | Integer | 写入记录到数据库失败后的最大重试次数。 |
sink.parallelism | 可选 | (none) | Integer | 用于定义 JDBC sink 算子的并行度。默认情况下,并行度是由框架决定:使用与上游链式算子相同的并行度。 |
总结
-
JdbcExecutionOptions 可以配置 批量写入间隔时间 最大写入数量 和 异常容错次数(只支持sql异常)
-
JdbcConnectionOptions 可以配置数据库的连接参数
-
关闭定时写入可以把 BatchIntervalMs设置为0
-
实时写入可以把 BatchSize 设置为1
-
间隔时间 或者 最大写入数 或者 触发检查点的时候 这三个地方 会触发写入批量写入jdbc
-
未开启检查点可能会丢失数据的,开启了检查点需要保证数据库幂等性插入,因为可能会重复插入!
cdc挂掉时间
2024-09-30 07:51
2024-10-02 01:00
2024-10-05 12:50
中间过节没发现
2024-10-09 17:50
2024-10-11 12:50
2024-10-13 12:50
2024-10-15 7:50
source端的时间 wait_timeout 28800 8H 5.7.32-log
sink wait_timeout 86400 24H 8.0.39

被折叠的 条评论
为什么被折叠?



