Flink源码阅读(四)checkPoint之数据一致性

1,664 阅读16分钟

通过上篇文章,Flink源码阅读(三)checkPoint之容错恢复

回顾一下要思考的四个问题,其中第1,2个问题已经在前两篇文章中解决

  • 那为什么在一个输入流的情况下也有checkpoint,如果是的话,是怎么生成checkpint快照的
  • 假设一条数据落盘失败了,checkpoint能否支持从故障中恢复
  • checckpoint保证一致性是指状态(state)的一致性,还是指数据(record)的一致性?
  • 这里说的buffers在源码层面指代什么?

本文负责解析第三个问题 checckpoint保证一致性是指状态(state)的一致性,还是指数据(Record)的一致性?

同样参考Flink1.11官方文档 ci.apache.org/projects/fl…

下图引用Unaligned Checkpointing的提出论文: arxiv.org/pdf/1506.08…

先说结论: checkpoint保证的是state(状态)的一致性

  • 状态一致性分类 Flink的一个重大价值在于,它既保证了exactly-once,也具有低延迟和高吞吐的处理能力。

    • AT-MOST-ONCE(最多一次): 当任务故障时,最简单的做法是什么都不干,既不恢复丢失的状态,也不重播丢失的数据。 At-most-once 语义的含义是最多处理一次事件。这其实是没有正确性保障的委婉说法——故障发生之后,计数结果可能丢失。同样的还有udp。

    • AT-LEAST-ONCE(至少一次) 在大多数的真实应用场景,我们希望不丢失事件。这种类型的保障称为 at-least-once, 意思是所有的事件都得到了处理,而一些事件还可能被处理多次。 这表示计数结果可能大于正确值,但绝不会小于正确值。也就是说,计数程序在发生故障后可能多算,但是绝不会少算。

    • EXACTLY-ONCE(精确一次) 恰好处理一次是最严格的保证,也是最难实现的。恰好处理一次语义不仅仅意味着没有事件丢失, 还意味着针对每一个数据,内部状态仅仅更新一次。这指的是系统保证在发生故障后得到的计数结果与正确值一致。

Flink 宣称支持 Exactly-once 其针对的是 Flink 应用内部的数据流处理。但 Flink 应用内部要想处理数据首先要有数据流入到 Flink 应用,其次 Flink 应用对数据处理完毕后也理应对数据做后续的输出。在 Flink 中数据的流入称为 Source,数据的后续输出称为 Sink,对于 Source 和 Sink 完全依靠外部系统支撑(比如 Kafka)。

阅读文档 Flink 数据源和接收器的容错保证(ci.apache.org/projects/fl…

下面以Flink kafka的Source段为例

FlinkKafkaProducer

1. Semantic.EXACTLY_ONCE Flink生产者将在Kafka事务中写入所有消息,致力于在发生一次checkPoint的时候提交消息给kafka。
在此模式下,kafka produer设置一个资源池。这个资源池其实就是一个实例锁(详见FlinkKafkaInternalProducer),会记录归属的produerId以及当前事务Id。
其次,在每个资源池之间创建一个kafka的checkpoint事务,如果checkpoint完成通知迟到了,kafka produer实例可能会耗尽池中的资源。
在那里面如果任何后续生成checkpoint的请求都将失败,以及kafka生产者将被继续使用,
换言之,在事务提交/中止线程与生产者关闭线程之间需要避免死锁


生产者将继续使用的选项如下:
		 - 从上一个checkpoint开始。
		- 为了减少checkpoint失败的机会,有四个选项:
			- 减少最大并发checkpoint数
		 	- 使checkpoint更可靠(以便更快完成)
		 	- 增加checkpoint之间的延迟
		 	- 增加kafka produer池的大小

2.Semantic.AT_LEAST_ONCE,Flink生产者将等待Kafka缓冲区中的所有未完成消息,在由Kafka生产者在接受到checkpoint的ack时。

3.Semantic.NONE表示不会保证任何事情。消息可能丢失和/或重复,以防万一失败。
/**
 * Flink Sink将数据生成到Kafka主题中。默认情况下
 * 将使用{@link FlinkKafkaProducer.Semantic#AT_LEAST_ONCE}语义。
 * 在使用{@link FlinkKafkaProducer.Semantic#EXACTLY_ONCE}之前,请参阅Flink的
 * Kafka连接器文档。
 */
@PublicEvolving
public class FlinkKafkaProducer<IN>
	extends TwoPhaseCommitSinkFunction<IN, FlinkKafkaProducer.KafkaTransactionState, FlinkKafkaProducer.KafkaTransactionContext> {

	/**
	 *  Semantics that can be chosen.
	 *  <li>{@link #EXACTLY_ONCE}</li>
	 *  <li>{@link #AT_LEAST_ONCE}</li>
	 *  <li>{@link #NONE}</li>
	 */
	public enum Semantic {

		/**
		 * Semantic.EXACTLY_ONCE Flink生产者将在Kafka事务中写入所有消息,
		 * 致力于在发生一次checkPoint的时候提交消息给kafka。
		 *
		 * <p>在此模式下,{@ link FlinkKafkaProducer}设置{@link FlinkKafkaInternalProducer}的池。每个之间
		 * 创建一个kafka的checkpoint事务,该事务在
		 * {@link FlinkKafkaProducer#notifyCheckpointComplete(long)}。如果checkpoint完成通知
		 * 迟到了,{@ link FlinkKafkaInducerProducer}可能会耗尽池中的{@link FlinkKafkaInternalProducer}。在那里面
		 * 如果任何后续{@link FlinkKafkaProducer#snapshotState(FunctionSnapshotContext)}请求都将失败
		 * 和{@link FlinkKafkaProducer}将继续使用{@link FlinkKafkaInternalProducer}
		 * 从上一个checkpoint开始。
		 * 为了减少checkpoint失败的机会,有四个选项:
		 * <li>减少最大并发checkpoint数</ li>
		 * <li>使checkpoint更可靠(以便更快完成)</ li>
		 * <li>增加checkpoint之间的延迟</ li>
		 * <li>增加{@link FlinkKafkaInternalProducer}池的大小</ li>
		 */
		EXACTLY_ONCE,

		/**
		 * Semantic.AT_LEAST_ONCE,Flink生产者将等待Kafka缓冲区中的所有未完成消息
 		 * 由Kafka生产者在接受到checkpoint的ack时。
		 */
		AT_LEAST_ONCE,

		/**
		 * Semantic.NONE表示不会保证任何事情。消息可能丢失和/或重复,以防万一
		 *失败。
		 */
		NONE
	}

EXACTLY-ONCE(精确一次)

Flink 自身是无法保证外部系统的 Exactly-once 语义。但这样一来其实并不能称为完整的 Exactly-once,或者说 Flink 并不能保证端到端 Exactly-once。而对于数据精准性要求极高的系统必须要保证端到端的 Exactly-once,所谓端到端是指Flink应用从Source一端开始到Sink一端结束,数据必经的起始和结束两个端点。

那么如何实现端到端的 Exactly-once 呢?Flink 应用所依赖的外部系统需要提供 Exactly-once 支撑,并结合 Flink 提供的 Checkpoint 机制和 Two Phase Commit 才能实现 Flink 端到端的 Exactly-once。

我们来看下FlinkkafkaProduer的继承结构,发现父类正好是TwoPhaseCommitSinkFunction

TwoPhaseCommitSinkFunction

/**
 * 对于打算实现一次语义的所有{@link SinkFunction},这是推荐的基类。
 * 通过在{@link CheckpointedFunction}和
 * {@link CheckpointListener}。用户应提供自定义{@code TXN}(事务句柄)并实现抽象
 * 处理此事务句柄的方法。
 *
 * @param <IN> {@link SinkFunction}的输入类型。
 * @param <TXN>事务存储处理事务所需的所有信息。
 * @param <CONTEXT>将在给定{@link TwoPhaseCommitSinkFunction}的所有调用之间共享的上下文
 *                 instance. Context is created once
 */
@PublicEvolving
public abstract class TwoPhaseCommitSinkFunction<IN, TXN, CONTEXT>
		extends RichSinkFunction<IN>
		implements CheckpointedFunction, CheckpointListener {

	private static final Logger LOG = LoggerFactory.getLogger(TwoPhaseCommitSinkFunction.class);

	protected final LinkedHashMap<Long, TransactionHolder<TXN>> pendingCommitTransactions = new LinkedHashMap<>();

	protected transient Optional<CONTEXT> userContext;

	protected transient ListState<State<TXN, CONTEXT>> state;

	private final Clock clock;

	private final ListStateDescriptor<State<TXN, CONTEXT>> stateDescriptor;

	private TransactionHolder<TXN> currentTransactionHolder;

	/**
	 * Specifies the maximum time a transaction should remain open.
	 */
	private long transactionTimeout = Long.MAX_VALUE;

	/**
	 * If true, any exception thrown in {@link #recoverAndCommit(Object)} will be caught instead of
	 * propagated.
	 */
	private boolean ignoreFailuresAfterTransactionTimeout;

	/**
	 * If a transaction's elapsed time reaches this percentage of the transactionTimeout, a warning
	 * message will be logged. Value must be in range [0,1]. Negative value disables warnings.
	 */
	private double transactionTimeoutWarningRatio = -1;

	/**
	 * 使用默认的{@link ListStateDescriptor}进行内部状态序列化。使用有用的实用程序
	 * 构造函数是{@link TypeInformation#of(Class)},{@ link org.apache.flink.api.common.typeinfo.TypeHint}和
	 * {@link TypeInformation#of(TypeHint)}。例子:
	 * <pre>
	 * {@代码
	 * TwoPhaseCommitSinkFunction(TypeInformation.of(new TypeHint <State <TXN,CONTEXT >>(){}));;
	 * }
	 * </pre>
	 *
	 * @param transactionSerializer {@link TypeSerializer} for the transaction type of this sink
	 * @param contextSerializer {@link TypeSerializer} for the context type of this sink
	 */
	public TwoPhaseCommitSinkFunction(
			TypeSerializer<TXN> transactionSerializer,
			TypeSerializer<CONTEXT> contextSerializer) {
		this(transactionSerializer, contextSerializer, Clock.systemUTC());
	}

	@VisibleForTesting
	TwoPhaseCommitSinkFunction(
		TypeSerializer<TXN> transactionSerializer,
		TypeSerializer<CONTEXT> contextSerializer,
		Clock clock) {
		this.stateDescriptor =
			new ListStateDescriptor<>(
				"state",
				new StateSerializer<>(transactionSerializer, contextSerializer));
		this.clock = clock;
	}

	protected Optional<CONTEXT> initializeUserContext() {
		return Optional.empty();
	}

	protected Optional<CONTEXT> getUserContext() {
		return userContext;
	}

	@Nullable
	protected TXN currentTransaction() {
		return currentTransactionHolder == null ? null : currentTransactionHolder.handle;
	}

	@Nonnull
	protected Stream<Map.Entry<Long, TXN>> pendingTransactions() {
		return pendingCommitTransactions.entrySet().stream()
			.map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), e.getValue().handle));
	}

	// ------ methods that should be implemented in child class to support two phase commit algorithm ------

	/**
	 * Write value within a transaction.
	 */
	protected abstract void invoke(TXN transaction, IN value, Context context) throws Exception;

	/**
	 * Method that starts a new transaction.
	 *
	 * @return newly created transaction.
	 */
	protected abstract TXN beginTransaction() throws Exception;

	/**
	 * 预提交先前创建的事务。预提交必须采取所有必要的步骤来准备
	 * 将来可能发生的提交事务。在此之后,事务可能仍然是
	 * 中止,但是基础实现必须确保对已经预先提交的事务进行提交调用
	 * 将永远成功。
	 *
	 * <p>通常,实现涉及刷新数据。
 	 * /
	protected abstract void preCommit(TXN transaction) throws Exception;

	/**
	 * 提交预先提交的事务。如果此方法失败,则Flink应用程序将
	 * 重新启动,{@ link TwoPhaseCommitSinkFunction#recoverAndCommit(Object)}将再次被调用
	 *同一个事务。
	 */
	protected abstract void commit(TXN transaction);

	/**
	 * Invoked on recovered transactions after a failure. User implementation must ensure that this call will eventually
	 * succeed. If it fails, Flink application will be restarted and it will be invoked again. If it does not succeed
	 * eventually, a data loss will occur. Transactions will be recovered in an order in which they were created.
	 */
	protected void recoverAndCommit(TXN transaction) {
		commit(transaction);
	}

	/**
	 * Abort a transaction.
	 */
	protected abstract void abort(TXN transaction);

	/**
	 * Abort a transaction that was rejected by a coordinator after a failure.
	 */
	protected void recoverAndAbort(TXN transaction) {
		abort(transaction);
	}

	/**
	 * Callback for subclasses which is called after restoring (each) user context.
	 *
	 * @param handledTransactions
	 * 		transactions which were already committed or aborted and do not need further handling
	 */
	protected void finishRecoveringContext(Collection<TXN> handledTransactions) {
	}

	// ------ entry points for above methods implementing {@CheckPointedFunction} and {@CheckpointListener} ------


	/**
	 * This should not be implemented by subclasses.
	 */
	@Override
	public final void invoke(IN value) throws Exception {}

	@Override
	public final void invoke(
		IN value, Context context) throws Exception {
		invoke(currentTransactionHolder.handle, value, context);
	}

	@Override
	public final void notifyCheckpointComplete(long checkpointId) throws Exception {
		// the following scenarios are possible here
		//
		//  (1)从最近的checkpoint开始只有一笔事务表明
		//      已触发并完成。那应该是常见的情况。
		//      在这种情况下,只需提交该事务即可。
		//
		//	(2)由于前一个跳过了checkpoint。那是一种罕见的情况,但是有可能发生
		//      例如,在以下情况下:
		//          - 主服务器无法保留最后一个的元数据
		//			- checkpoint(存储系统中的临时中断),但是
		//			- 可以保留一个连续的checkPoint(此处通知的checkPoint)
		//
		//			- 其他任务在此期间无法保持其状态
		//			- 前一个checkPoint,但未触发失败,因为它们
		//			- 可以保持其状态并可以成功地将其保留在
		//			- 连续的checkPoint(此处通知的checkPoint)
		//
		//			- 在这两种情况下,先前的checkPoint都永远不会达到提交状态,但是
		//			- 始终希望此checkpoint包含先前的checkPoint并覆盖所有checkPoint
		//			- 自上次成功以来发生的变化。因此,我们需要承诺所有待处理的事务。
		//
		//	(3)多个事务正在挂起,但checkPoint已完成通知
		//		- 与最新无关。这是可能的,因为通知消息
		//		- 可以延迟(在极端情况下,直到下一个检查点到达为止已触发),因为可能存在并发的重叠checkPoint(在上一个完全完成之前开始一个新的)。
		//
		// ==>绝不应该有我们这里没有待准备事务的情况
		//
		// ==> There should never be a case where we have no pending transaction here
		//

		Iterator<Map.Entry<Long, TransactionHolder<TXN>>> pendingTransactionIterator = pendingCommitTransactions.entrySet().iterator();
		Throwable firstError = null;

		while (pendingTransactionIterator.hasNext()) {
			Map.Entry<Long, TransactionHolder<TXN>> entry = pendingTransactionIterator.next();
			Long pendingTransactionCheckpointId = entry.getKey();
			TransactionHolder<TXN> pendingTransaction = entry.getValue();
			if (pendingTransactionCheckpointId > checkpointId) {
				continue;
			}

			LOG.info("{} - checkpoint {} complete, committing transaction {} from checkpoint {}",
				name(), checkpointId, pendingTransaction, pendingTransactionCheckpointId);

			logWarningIfTimeoutAlmostReached(pendingTransaction);
			try {
				commit(pendingTransaction.handle);
			} catch (Throwable t) {
				if (firstError == null) {
					firstError = t;
				}
			}

			LOG.debug("{} - committed checkpoint transaction {}", name(), pendingTransaction);

			pendingTransactionIterator.remove();
		}

		if (firstError != null) {
			throw new FlinkRuntimeException("Committing one of transactions failed, logging first encountered failure",
				firstError);
		}
	}
    ....................................
 }

checkpoint一致性协议图

通过上述代码,注意到 TwoPhaseCommitSinkFunction实现二阶段提交,本质上是实现了CheckpointedFunction 以及CheckpointListener,下面为接口类的代码

@PublicEvolving
public interface CheckpointedFunction {

	/**
	 * 当请求检查点的快照时,将调用此方法。
	 * 确保通过以下方式通过{@link FunctionInitializationContext}先前提供的方式公开所有状态
	 * 函数已被{@link FunctionSnapshotContext}本身初始化或提供。
	 *
	 * @param context用于绘制算子快照的上下文
	 * @throws异常抛出,如果无法创建状态或无法恢复状态。
	 */
	void snapshotState(FunctionSnapshotContext context) throws Exception;

	/**
	 * 在分布式过程中创建并行函数实例时调用此方法
	 * 执行。函数通常使用此方法设置其状态存储数据结构。
	 *
	 * @param context用于初始化运算符的上下文
	 * @throws异常抛出,如果无法创建状态或无法恢复状态。
	 */
	void initializeState(FunctionInitializationContext context) throws Exception;
}


/**
 * 此接口必须由要接收的功能/操作来实现
 * 所有人都完全确认检查点后的提交通知
 * 参与者。
 */
@PublicEvolving
public interface CheckpointListener {

	/**
	 * 分布式检查点完成后,此方法称为通知。
	 *
	 * 请注意,此方法期间的任何异常都不会导致检查点
	 * 再失败一次。
	 *
	 * @param checkpointId已完成的检查点的ID。
	 * @throws异常
	 */
	void notifyCheckpointComplete(long checkpointId) throws Exception;
}

上图以及接口总结如下:

1. jobMaster 会周期性的发送执行checkpoint命令(start checkpoint);

2.当source端收到执行指令后会产生一条barrier消息插入到input消息队列中,当处理到barrier时会执行本地checkpoint, 并且会将barrier发送到下一个节点,当checkpoint完成之后会发送一条ack信息给jobMaster ;

3. 当DAG图中所有节点都完成checkpoint之后,jobMaster会收到来自所有节点的ack信息,那么就表示一次完整的checkpoint的完成;

4. JobMaster会给所有节点发送一条callback信息,表示通知checkpoint完成消息,这个过程是异步的,并非必须的,方便做一些其他的事情,例如kafka offset提交到kafka。

对比flink 整个checkpoint机制调用流程可以发现与2PC非常相似,JobMaster相当于master协调者,所有的处理节点相当于slave执行者,start-checkpoint消息相当于pre-commit消息,每个处理节点的checkpoint相当于pre-commit过程,checkpoint ack消息相当于执行者反馈信息,最后callback消息相当于commit消息,完成具体的提交动作。

根据flink 官方文档中保证端到端的一致性描述: flink.apache.org/features/20…

两阶段提交步骤解析(Two-Phase)

  • 开始预提交阶段: 所有算子开始进行pre-commit

  • 预提交状态阶段(仅提交内部状态):如果至少一个算子的pre-commit失败,则所有其他pre-commit都将中止,然后我们回滚到先前成功完成的checkpoint。

  • 预提交状态阶段(仅提交外部状态): 在成功进行pre-commit之后,必须保证提交最终成功,即整个算子链中的算子和我们的外部系统都需要做出此保证。如果commit失败(例如,由于间歇性的网络问题),则整个Flink应用程序都会失败,并根据用户的重新启动策略重新启动,并且还会进行另一次提交尝试。因为如果提交最终未能成功,则会发生数据丢失

  • 提交阶段: 我们可以确保所有算子都同意检查点的最终结果:所有操作员都同意数据已提交或提交被中止并回滚。

场景简述

假设现有Flink程序如下:

示例Flink应用程序中,具有以下角色:

  • Kafka读取的数据源(在Flink中,是KafkaConsumer
  • 窗口聚合
  • 将数据写回Kafka的数据接收器(在Flink中,是KafkaProducer) 为了使数据接收器提供一次准确的保证,它必须在事务范围内将所有数据写入Kafka。提交将两个检查点之间的所有写入捆绑在一起。

这样可以确保在失败的情况下回滚写操作。

但是,在具有多个并发运行的sink任务的分布式系统中,简单的提交或回滚是不够的,因为所有组件必须在提交或回滚时达成一致,以确保结果一致。Flink使用两阶段提交协议及其pre-commit阶段来应对这种场景

预提交阶段(仅提交内部状态)

检查点的开始表示我们的两阶段提交协议的pre-commit阶段。当检查点开始时,Flink JobManager将检查点屏障(将数据流中的记录分为进入当前检查点的集合与进入下一个检查点的集合)。

barrier在算子之间传递。对于每个算子,它都会触发算子的state后端以对其状态进行快照。

数据源存储其Kafka偏移量,完成此操作后,将checkpoint barrier传递给下一个运算符。

如果算子仅具有内部state,则此方法有效。内部state是由Flink的状态后端存储和管理的所有内容

例如,第二个运算符中的window算子。当进程仅具有内部state时,除了在检查点之前更新状态后端中的数据外,无需在预提交期间执行任何其他操作。在检查点成功的情况下,Flink会负责正确地提交那些状态,在失败的情况下,Flink会中止它们。

预提交阶段(仅提交外部状态)

但是,当进程具有外部state时,必须以不同的方式处理此状态。外部状态通常以写入外部系统(例如Kafka)的形式出现。在那种情况下,为了提供精确的一次保证,外部系统必须为与两阶段提交协议集成的事务提供支持。

我们知道示例中的Sink算子具有这种外部状态,因为它正在将数据写入Kafka。在这种情况下,在pre-commit阶段,Sink算子除了将其状态写入状态后端之外,还必须预提交其外部事务。

checkpoint barrier提交阶段结束。此时,检查点已成功完成,并且由整个应用程序的状态组成,包括预提交的外部状态。万一发生故障,我们将从该检查点重新初始化应用程序。

提交阶段

在提交阶段,通知所有算子产生checkpoint快照成功。这是两阶段提交协议的提交阶段,JobManager会为应用程序中的每个算子发出checkpoint完成的回调。数据源和window算子没有外部状态,因此在提交阶段,这些算子不必执行任何操作。但是,Sink算子确实具有外部state,并且使用外部写入来提交事务。

Flink两阶段提交的实现思路

由于网上两阶段提交示例比较全面(mysql,oracle),官方提供的FlinkKafkaProducer 源码注释也比较详细,这里就不深入展开了。 前面我们讨论kafka如何扩展TwoPhaseCommitSinkFunction的示例。总结如下:

我们只需要实现四种方法,并为精确一次(exactly-once) file sink:展开实现细节:

  • beginTransaction - 要开始事务,我们在目标文件系统上的临时目录中创建一个临时文件。随后,我们可以在处理文件时将数据写入该文件。
  • preCommit - 在预提交时,我们刷新文件,将其关闭,再也不会再次写入该文件。我们还将为属于下一个checkpoint的所有后续写入启动新事务。
  • commit - 在提交时,我们将预先提交的文件原子地移动到实际的目标目录。请注意,这会增加输出数据可见性的延迟。
  • abort - 中止时,我们将删除临时文件。
/**
	 * Write value within a transaction.
	 */
	protected abstract void invoke(TXN transaction, IN value, Context context) throws Exception;

	/**
	 * Method that starts a new transaction.
	 *
	 * @return newly created transaction.
	 */
	protected abstract TXN beginTransaction() throws Exception;

	/**
	 * Pre commit previously created transaction. Pre commit must make all of the necessary steps to prepare the
	 * transaction for a commit that might happen in the future. After this point the transaction might still be
	 * aborted, but underlying implementation must ensure that commit calls on already pre committed transactions
	 * will always succeed.
	 *
	 * <p>Usually implementation involves flushing the data.
	 */
	protected abstract void preCommit(TXN transaction) throws Exception;

	/**
	 * Commit a pre-committed transaction. If this method fail, Flink application will be
	 * restarted and {@link TwoPhaseCommitSinkFunction#recoverAndCommit(Object)} will be called again for the
	 * same transaction.
	 */
	protected abstract void commit(TXN transaction);

	/**
	 * Invoked on recovered transactions after a failure. User implementation must ensure that this call will eventually
	 * succeed. If it fails, Flink application will be restarted and it will be invoked again. If it does not succeed
	 * eventually, a data loss will occur. Transactions will be recovered in an order in which they were created.
	 */
	protected void recoverAndCommit(TXN transaction) {
		commit(transaction);
	}

	/**
	 * Abort a transaction.
	 */
	protected abstract void abort(TXN transaction);

下面是一个Flink 基于mysql通过两阶段提交,实现消费的exactly-once(精确一次)的示例

@Slf4j
public class MySqlTwoPhaseCommitSink extends TwoPhaseCommitSinkFunction<Tuple3<String, String, String>, MySqlTwoPhaseCommitSink.ConnectionState, Void> {


    public MySqlTwoPhaseCommitSink() {

        super(new KryoSerializer<>(MySqlTwoPhaseCommitSink.ConnectionState.class, new ExecutionConfig()), VoidSerializer.INSTANCE);

    }


    @Override
    protected void invoke(MySqlTwoPhaseCommitSink.ConnectionState connectionState, Tuple3<String, String, String> objectNode, Context context) throws Exception {
        System.err.println("start invoke.......");
        Connection connection = connectionState.connection;
        log.info("---------------------->", connection);
        String sql = "insert into t_word_counts values (?,?,?)";
        PreparedStatement ps = connection.prepareStatement(sql);
        log.info("sql--------------------->", sql);
        ps.setString(1, objectNode.f0);
        ps.setString(2, objectNode.f1);
        ps.setString(3, objectNode.f2);
        ps.executeUpdate();


    }

    /**
     * 获取连接,开启手动提交事物
     *
     * @return
     * @throws Exception
     */
    @Override
    protected MySqlTwoPhaseCommitSink.ConnectionState beginTransaction() throws Exception {

        Connection connection = HikariUtils.getConnection();

        log.info("start beginTransaction......." + connection);

        return new ConnectionState(connection);
    }

    /**
     * 预提交,这里预提交的逻辑在invoke方法中
     *
     * @param connectionState
     * @throws Exception
     */
    @Override
    protected void preCommit(MySqlTwoPhaseCommitSink.ConnectionState connectionState) throws Exception {
        log.info("start preCommit......." + connectionState);
    }

    /**
     * 如果invoke方法执行正常,则提交事务
     *
     * @param connectionState
     */
    @Override
    protected void commit(MySqlTwoPhaseCommitSink.ConnectionState connectionState) {
        log.info("start commit......." + connectionState);

        Connection connection = connectionState.connection;

        try {
            connection.commit();
            connection.close();
        } catch (SQLException e) {
            throw new RuntimeException("提交事物异常");
        }
    }

    /**
     * 如果invoke执行异常则回滚事物,下一次的checkpoint操作也不会执行
     *
     * @param connectionState
     */
    @Override
    protected void abort(MySqlTwoPhaseCommitSink.ConnectionState connectionState) {
        log.info("start abort rollback......." + connectionState);
        Connection connection = connectionState.connection;
        try {
            connection.rollback();
            connection.close();
        } catch (SQLException e) {
            throw new RuntimeException("回滚事物异常");
        }
    }

    static class ConnectionState {

        private final transient Connection connection;

        ConnectionState(Connection connection) {

            this.connection = connection;
        }

    }

}