要想Flink实现ExactlyOnce需要Source能够记录偏移量,Sink支持开启事务
一、Source
1、使用KafkaSource需要调用addSource方法,传入一个FlinkKafkaConsumer的实例
2、FlinkKafkaConsumer类实现了FlinkKafkaConsumerBase,点到FlinkKafkaConsumerBase里面我们看到他有一个成员变量,这个成员变量就是用来记录Kafka的某个topic的某个分区的偏移量,因为可能会有一个台机器读多个kafka分区的情况,所以用的ListState
二、Sink
1、调用addSink方法,传入一个FlinkKafkaProducer的一个实例,点入到FlinkKafkaProducer里面,看到这个类继承了TwoPhaseCommitSinkFunction,这个类叫做两阶段提交接收函数,后面我们能看到什么是两阶段提交
2、可以看到TwoPhaseCommitSinkFunction这个方法继承了RichSinkFunction,并实现了CheckpointedFunction, CheckpointListener这两个接口
3、RichSinkFunction是自定义Sink基本都要实现;CheckpointedFunction提供了initializeState和snapshotState这两个方法,initializeState方法是subTask的生命周期方法,snapshotState方法是在每次Checkpoint的时候,调用一次这个方法;继承CheckpointListener需要实现notifyCheckpointComplete方法和notifyCheckpointAborted方法
这里我们需要看一下Flink的Checkpoint的步骤,下图(这是实现两阶段提交的关键!!)
从下图,我们可以看到(④)当JobManager集齐了这次Checkpoint所有subtask的应答后,会想实现了CheckpointListener接口的subtask发送这次Checkpoint成功的消息;(⑤)所有实现了CheckpointListener方法的Task的subtask会调用notifyCheckpointComplete方法
4、(①)我们看FlinkKafkaProducer的initializeState方法调用了父TwoPhaseCommitSinkFunction的initializeState方法
(②)TwoPhaseCommitSinkFunction的initializeState方法首先恢复了用来保存数据的状态
(③)TwoPhaseCommitSinkFunction的initializeState方法开启了一个事务
5、FlinkKafkaProducer的invoke方法,每有一个条数据进入到Sink就调用一次这个方法
它将topic、分区号、时间戳、序列化的key和value封装到ProducerRecord的实例中
再使用KafkaProducer的连接将变量recordsend(数据没有立即写入到Kafka的Broker中,而是缓存到支持事务的客户端),再有数据进入到Sink中,再调用invoke方法,再将数据使用Kafka的Producer发送(和上面一个意思),只要没有flush或数据达到一定的大小,数据就会缓存在客户端
6、(①)随着时间的推移,在Checkpoint时,会调用FlinkKafkaProducer的snapshotState方法,这个方法首先调用了父类的snapshotState方法,我们看父类的snapshotState
(②) 在该方法中调用了preCommit方法,preCommit方法大概就干了一件事,将客户端的数据flush到Kafka的Broker中,但是!!注意,这时候并没有提交事务(写入到Kafka Broker中的数据是uncommited状态),然后将缓存在客户端的数据持久化到statebacked中。
7、当所有的subtask都完成checkpoint之后,也就是JobManager集齐了所有subtask的Ack应答之后,就会向所有实现了CheckpointListener接口的Task的subtask发送Chenkpoint成功的消息,之后在subtask中调用重写的notifyCheckpointComplete方法
8、TwoPhaseCommitSinkFunction重写的notifyCheckpointComplete方法中调用了commit方法
,这个方法会提交事务,并将客户端的数据flush到Kafka的Broker----这样写入到Kafka中的数据才算真正写入成功!
在写入的时候,可能会出现错误
(一) 如果Checkpoint失败,所有subtask重启,然后从上一次的状态恢复数据,会接着之前的偏移量继续读,继续处理
(二) 如果Checkpoint成功了,提交事务失败了,所有的subtask重启,然后在initializeState方法中恢复状态装的数据,再将数据写入到Kafka的Broker中,并提交事务
现在看一下这个两阶段提交接是哪两个阶段
preCommit,即在Checkpoint时,将客户端缓存的数据flush到Broker中(但没有提交事务)
commit在整个这次Checkpoint成功后,JobManager通知实现了CheckpointListener接口的subtask(Sink),提交事务