一步一个脚印,一天一道大数据面试题。
在流式大数据处理框架中,Exactly-Once 语义对于确保每条数据精确地只被消费一次(避免重复读取和丢失读取)非常重要。下面将介绍 Flink 是如何实现 Exactly-Once 语义的。
尽管在程序正常运行、资源充足的情况下实现 Exactly-Once 语义并不难,但实际生产环境中存在各种复杂情况和突发状况,因此为了可靠地实现 Exactly-Once,需要以下容错机制。
数据源(Source)
首先,数据源需要记录“偏移量”,即标记已读取的位置。这样,如果程序重启,可以准确地从未被消费的第一条数据开始读取,既不会多读也不会少读。
Flink 检查点(Checkpoint)
Flink 提供了检查点机制,能够在出现错误时准确恢复数据和操作符状态等。只有通过精确的容错恢复机制,才能实现可靠的 Exactly-Once 语义。
Flink 的检查点机制基于分布式快照技术,定期将作业的状态保存到持久存储中,例如分布式文件系统或远程数据库。当发生故障时,Flink 可以使用最近的检查点进行恢复,确保处理过程的准确性。
数据消费端(Sink)
最后,在数据消费端,需要确保消费者能够支持“事务性”提交,比如使用支持事务的数据库(如 MySQL)进行数据写入。这样,在发生故障时,Flink 可以回滚未完成的事务,并重新执行已提交的事务,从而保证数据的一致性和准确性。
如果无法使用事务性提交,另一种方式是通过幂等性操作来实现 Exactly-Once 语义。例如,可以多次将同一条数据放入一个 Set 集合中,依然保持与第一次放入集合时相同的结果。
代码样例
代码样例来自尚硅谷,Flink
EXACTLY_ONCE读写 Kafka
public class KafkaEOSDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 代码中用到hdfs,需要导入hadoop依赖、指定访问hdfs的用户名
System.setProperty("HADOOP_USER_NAME", "atguigu");
// TODO 1、启用检查点,设置为精准一次
env.enableCheckpointing(5000, CheckpointingMode.EXACTLY_ONCE);
CheckpointConfig checkpointConfig = env.getCheckpointConfig();
checkpointConfig.setCheckpointStorage("hdfs://hadoop102:8020/chk");
checkpointConfig.setExternalizedCheckpointCleanup(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
// TODO 2.读取kafka
KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
.setBootstrapServers("hadoop102:9092,hadoop103:9092,hadoop104:9092")
.setGroupId("atguigu")
.setTopics("topic_1")
.setValueOnlyDeserializer(new SimpleStringSchema())
.setStartingOffsets(OffsetsInitializer.latest())
.build();
DataStreamSource<String> kafkasource = env
.fromSource(kafkaSource, WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3)), "kafkasource");
/**
* TODO 3.写出到Kafka
* 精准一次 写入Kafka,需要满足以下条件,缺一不可
* 1、开启checkpoint
* 2、sink设置保证级别为 精准一次
* 3、sink设置事务前缀
* 4、sink设置事务超时时间: checkpoint间隔 < 事务超时时间 < max的15分钟
*/
KafkaSink<String> kafkaSink = KafkaSink.<String>builder()
// 指定 kafka 的地址和端口
.setBootstrapServers("hadoop102:9092,hadoop103:9092,hadoop104:9092")
// 指定序列化器:指定Topic名称、具体的序列化
.setRecordSerializer(
KafkaRecordSerializationSchema.<String>builder()
.setTopic("ws")
.setValueSerializationSchema(new SimpleStringSchema())
.build()
)
// TODO 3.1 精准一次,开启 2pc
.setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
// TODO 3.2 精准一次,必须设置 事务的前缀
.setTransactionalIdPrefix("atguigu-")
// TODO 3.3 精准一次,必须设置 事务超时时间: 大于checkpoint间隔,小于 max 15分钟
.setProperty(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 10*60*1000+"")
.build();
kafkasource.sinkTo(kafkaSink);
env.execute();
}
}