03-Flink 流批一体 API开发2(仅供学习)

概述

第一部分:DataStream Operators

01-Operators【Physical Partitioning】

DataStream提供物理分区算子:[决定上游Operator中各个分区数据如何发送到下游Operator的各个分区中。]

在这里插入图片描述

​ 在DataStream函数中提供7种方式,常见的物理分区策略有**随机分配(Random)、轮询分配(Round-Robin)、重缩放(Rescale)和广播(Broadcast)**等。
在这里插入图片描述

  • 第一、物理分区算子【global

功能:将所有的数据都发送到下游的某个算子实例(subtask id = 0)。

在这里插入图片描述

  • 第二、物理分区算子【broadcast

功能:发送到下游所有的算子实例。

在这里插入图片描述

  • 第三、物理分区算子【forward

功能:发送到下游对应的第1个task,保证上下游算子并行度一致,即上游算子与下游算子是1:1关系

在这里插入图片描述

[在上下游的算子没有指定分区器的情况下,如果上下游的算子并行度一致,则使用ForwardPartitioner,否则使用RebalancePartitioner,对于ForwardPartitioner,必须保证上下游算子并行度一致,否则会抛出异常]

  • 第四、物理分区算子【shuffle

功能:随机选择一个下游算子实例进行发送

在这里插入图片描述

  • 第五、物理分区算子【rebalance

功能:通过循环的方式依次发送到下游的task。

在这里插入图片描述

[在Flink批处理中(离线数据分析中),如果数据倾斜,直接调用rebalance函数,将数据均衡分配。]

在这里插入图片描述

  • 第六、物理分区算子【rescale

功能:基于上下游Operator并行度,将记录以循环的方式输出到下游Operator每个实例。

在这里插入图片描述

  • 第七、物理分区算子【custom

当Flink 供的所有分区策略都不能满足用户的需求时,使用partitionCustom()方法来自定义分区策略。

在这里插入图片描述

案例代码演示:DataStream中各种数据分区函数使用

package cn.itqzd.flink.transformation;

import org.apache.flink.api.common.functions.Partitioner;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;

import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * Flink 流计算物理分区算子: 对流数据进行分区
 *      todo: global、broadcast、forward、shuffle、rebalance、rescale、partitionCustom
 * @author xuyuan
 */
public class TransformationPartitionDemo {

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(1) ;

		// 2. 数据源-source
		DataStreamSource<Tuple2<Integer, String>> dataStream = env.addSource(
			new RichParallelSourceFunction<Tuple2<Integer, String>>() {
				private boolean isRunning = true ;

				@Override
				public void run(SourceContext<Tuple2<Integer, String>> ctx) throws Exception {
					int index = 1 ;
					Random random = new Random();
					String[] chars = new String[]{
						"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
						"P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
					};
					while (isRunning){
						Tuple2<Integer, String> tuple = Tuple2.of(index, chars[random.nextInt(chars.length)]);
						ctx.collect(tuple);

						TimeUnit.SECONDS.sleep(2);
						index ++ ;
					}
				}

				@Override
				public void cancel() {
					isRunning = false ;
				}
			}
		);
		//dataStream.printToErr();

		// 3. 数据转换-transformation
		// todo 1. global 全局,将所有数据发往下游第1个subTask
		DataStream<Tuple2<Integer, String>> globalDataStream = dataStream.global();
		// globalDataStream.print().setParallelism(3) ;

		// todo 2. broadcast 广播,发送给下游所有SubTask
		DataStream<Tuple2<Integer, String>> broadcastDataStream = dataStream.broadcast();
		// broadcastDataStream.printToErr().setParallelism(3) ;

		// todo 3. forward 向前,上下游并行度相同时,一对一发送
		// DataStream<Tuple2<Integer, String>> forwardDataStream = dataStream.setParallelism(3).forward();
		// forwardDataStream.print().setParallelism(3);

		// todo 4. shuffle 随机,发送到下游时,随机选择
		DataStream<Tuple2<Integer, String>> shuffleDataStream = dataStream.shuffle();
		// shuffleDataStream.printToErr().setParallelism(3) ;

		// todo 5. rebalance 均衡,采用轮询机制发送到下游各个subTask任务
		DataStream<Tuple2<Integer, String>> rebalanceDataStream = dataStream.rebalance();
		// rebalanceDataStream.print().setParallelism(3) ;

		// todo 6. rescale 局部均衡,本地轮询机制
		// DataStream<Tuple2<Integer, String>> rescaleDataStream = dataStream.setParallelism(4).rescale();
		// rescaleDataStream.printToErr().setParallelism(2);

		// todo 7. partitionCustom 自定义分区规则
		/*
			step1、从数据流中每条数据获取key,
			step2、依据key决定如何进行物理分区
		 */
		DataStream<Tuple2<Integer, String>> customStream = dataStream.partitionCustom(
			// step2、依据key决定如何进行物理分区
			new Partitioner<Integer>() {
				@Override
				public int partition(Integer key, int numPartitions) {
					return key % 2; // 0 , 1  -> 下游算子并行度:2
				}
			},
			// step1、从数据流中每条数据获取key
			new KeySelector<Tuple2<Integer, String>, Integer>() {
				@Override
				public Integer getKey(Tuple2<Integer, String> value) throws Exception {
					return value.f0;
				}
			}
		);
		customStream.printToErr().setParallelism(2) ;

		// 4. 数据终端-sink

		// 5. 触发执行-execute
		env.execute("TransformationPartitionDemo");
	}

}  

02-Operators【RichFunction】


​ “富函数”是DataStream API提供的一个函数类的接口,所有Flink函数类都有其Rich版本。富函数类一般是以抽象类的形式出现的。例如:RichMapFunction、RichFilterFunction、RichReduceFunction 等。

在这里插入图片描述

富函数RichFunction,可以获取运行环境的上下文,并拥有一些生命周期方法,当处理数据时,如果涉及到数据库连接、网络连接以及文件描述符的创建和关闭,使用富函数。RichFunction有一个生命周期的概念,典型的生命周期方法有:openclose 方法。

在这里插入图片描述

  • open()方法:
    • rich function的初始化方法,当一个算子例如map或者filter被调用之前open()会被调用。
  • close()方法:
    • 生命周期中的最后一个调用的方法,做一些清理工作。
  • getRuntimeContext()方法:
    • 提供了函数的RuntimeContext的一些信息,例如函数执行的并行度,任务的名字,以及state状态。

案例代码演示:修改自定义数据源从MySQL数据库加载数据代码,加上打印语句,查看结果。

package cn.itqzd.flink.transformation;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.concurrent.TimeUnit;

/**
 * 富函数RichFunction,生命周期方法:open和close,当算子对数据库操作时,建议使用
 */
public class TransformationRichDemo {

	@Data
	@NoArgsConstructor
	@AllArgsConstructor
	public static class Student {
		private Integer id;
		private String name;
		private Integer age;
	}

	/**
	 * 自定义数据源,从MySQL表中加载数据,并且实现增量加载
	 */
	private static class MySQLSource extends RichParallelSourceFunction<Student> {
		// 定义变量,标识是否加载数据
		private boolean isRunning = true ;

		// 定义变量,open方法初始化,close方法关闭连接
		private Connection conn = null ;
		private PreparedStatement pstmt = null ;
		private ResultSet result = null ;

		// 初始化方法,在获取数据之前,准备工作
		@Override
		public void open(Configuration parameters) throws Exception {
			System.out.println("调用open()方法,初始化操作:创建数据库连接..........................");
			// step1、加载驱动
			Class.forName("com.mysql.jdbc.Driver") ;
			// step2、获取连接Connection
			conn = DriverManager.getConnection(
				"jdbc:mysql://node1:3306/?useSSL=false",
				"root",
				"123456"
			);
			// step3、创建Statement对象,设置语句(INSERT、SELECT)
			pstmt = conn.prepareStatement("SELECT id, name, age FROM db_flink.t_student") ;
		}

		@Override
		public void run(SourceContext<Student> ctx) throws Exception {
			while (isRunning){
				System.out.println("调用run()方法,查询数据库中数据..........................");
				// step4、执行操作,获取ResultSet对象
				result = pstmt.executeQuery();
				// step5、遍历获取数据
				while (result.next()){
					// 获取每个字段的值
					int stuId = result.getInt("id");
					String stuName = result.getString("name");
					int stuAge = result.getInt("age");
					// 构建实体类对象
					Student student = new Student(stuId, stuName, stuAge);
					// 发送数据
					ctx.collect(student);
				}

				// 每隔5秒加载一次数据库,获取数据
				TimeUnit.SECONDS.sleep(5);
			}
		}

		@Override
		public void cancel() {
			isRunning = false ;
		}

		// 收尾工作,当不再加载数据时,一些善后工作
		@Override
		public void close() throws Exception {
			System.out.println("调用close()方法,收尾工作,关闭数据库连接.....................");
			// step6、关闭连接
			if(null != result) result.close();
			if(null != pstmt) pstmt.close();
			if(null != conn) conn.close();
		}
	}

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration()) ;
		env.setParallelism(1);

		// 2. 数据源-source
		DataStreamSource<Student> studentDataStream = env.addSource(new MySQLSource());

		// 3. 数据转换-transformation
		// 4. 数据终端-sink
		studentDataStream.printToErr();

		// 5. 触发执行-execute
		env.execute("TransformationRichDemo") ;
	}
}

执行程序,结果如下截图:

在这里插入图片描述

03-Operators【ProcessFunction】


[Flink DataStream API中最底层API,提供process方法,其中需要实现ProcessFunction函数]
在这里插入图片描述

查看抽象类:ProcessFunction源码,最主要方法:processElement,对流中每条数据进行处理。

在这里插入图片描述

案例演示:使用process函数,代替filter函数,实现对数据过滤操作。

在这里插入图片描述

package cn.itqzd.flink.transformation;

import org.apache.flink.api.common.functions.FilterFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.util.Collector;

/**
 * 使用Flink 计算引擎实现流式数据处理:从Socket接收数据,对数据进行过滤【filter】和【process】
 */
public class TransformationProcessDemo {

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(1) ;

		// 2. 数据源-source
		DataStreamSource<String> inputStream = env.socketTextStream("node1", 9999);

		// 3. 数据转换-transformation
		// TODO: DataStream中filter属于高级API函数
		SingleOutputStreamOperator<String> filterStream = inputStream.filter(
			new FilterFunction<String>() {
				@Override
				public boolean filter(String line) throws Exception {
					return line.trim().length() > 0;
				}
			}
		);
		filterStream.printToErr("filter>");

		// TODO: 可以使用底层API方法 -> process
		SingleOutputStreamOperator<String> processStream = inputStream.process(
			new ProcessFunction<String, String>() {
				@Override
				public void processElement(String line, Context ctx, Collector<String> out) throws Exception {
					if(line.trim().length() > 0){
						out.collect(line);
					}
				}
			}
		);
		processStream.printToErr("process>");

		// 4. 数据接收器-sink
        
		// 5. 执行应用-execute
		env.execute("StreamProcessDemo");
	}

}

第二部分:DataStream Connector

​ 在Flink框架中,专门为一些存储系统提供Connector连接器,方便用户进行读取数据(Data Source 数据源)和保存数据(Data Sink数据接收器)。

https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/connectors/datastream/overview/

在这里插入图片描述

04-Connector之Kafka Connector


Kafka Connector连接器,既可以作为数据源Source加载数据,又可以作为数据接收器Sink保存数据。

在这里插入图片描述

使用Kafka Connector连接器,添加Maven 依赖:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka_2.11</artifactId>
    <version>1.13.1</version>
</dependency>

​ 在Kafka Connector连接器中提供Source数据源和Sink接收器类,在Flink 1.12版本中提供基于新的接口消费Kafka数据:KafkaSource

在这里插入图片描述

Flink 1.14版本中提供基于新的接口生产数据到Kafka接收器:KafkaSink

在这里插入图片描述

05-Connector之Kafka 数据源【FlinkKafkaConsumer】


当从Kafka消费数据时,工具类:FlinkKafkaConsumer ,相关说明如下:

https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/connectors/datastream/kafka/#kafka-sourcefunction

在这里插入图片描述

  1. 订阅的主题:topic,一个Topic名称或一个列表(多个Topic)
  2. 反序列化规则:deserialization
  3. 消费者属性-集群地址:bootstrap.servers
  4. 消费者属性-消费者组id(如果不设置,会有默认的,但是默认的不方便管理):group.id

当从Kafka消费数据时,需要指定反序列化实现类:将Kafka读取二进制数据,转换为String对象

在这里插入图片描述

Kafka Consumer消费数据,反序列化数据说明:

在这里插入图片描述

启动Zookeeper和Kafka集群,命令如下:

[root@node1 ~]# start-zk.sh
[root@node1 ~]# start-kafka.sh

[root@node1 ~]# /export/server/kafka/bin/kafka-topics.sh --list --bootstrap-server node1:9092

[root@node1 ~]# /export/server/kafka/bin/kafka-topics.sh --create --topic flink-topic --bootstrap-server node1:9092,node2:9092,node3:9092 --replication-factor 1 --partitions 3

[root@node1 ~]# /export/server/kafka/bin/kafka-console-producer.sh --topic flink-topic --broker-list node1:9092,node2:9092,node3:9092

编程实现从Kafka消费数据,演示代码如下:

package cn.itqzd.flink.connector;

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;

import java.util.Properties;

/**
 * Flink从Kafka消费数据,指定topic名称和反序列化类
 */
public class ConnectorFlinkKafkaConsumerDemo {

	public static void main(String[] args) throws Exception{
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(3);

		// 2. 数据源-source
		// 2-1. 创建消费Kafka数据时属性
		Properties props = new Properties();
		props.setProperty("bootstrap.servers", "node1:9092,node2:9092,node3:9092");
		props.setProperty("group.id", "test");
		// 2-2. 构建FlinkKafkaConsumer实例对象
		FlinkKafkaConsumer<String> kafkaConsumer = new FlinkKafkaConsumer<String>(
			"flink-topic", //
			new SimpleStringSchema(), //
			props
		);
		// 2-3. 添加Source
		DataStream<String> kafkaStream = env.addSource(kafkaConsumer);

		// 3. 数据转换-transformation
		// 4. 数据接收器-sink
		kafkaStream.printToErr();

		// 5. 触发执行-execute
		env.execute("ConnectorFlinkKafkaConsumerDemo") ;
	}

}

其中最核心的部分就是:创建FlinkKafkaConsumer对象,传递参数值

在这里插入图片描述

06-Connector之Kafka 数据源【Start Offset】


​ 从Kafka 消费数据时,可以设置从Kafka Topic中哪个偏移量位置开始消费数据,默认情况下,第一次运行,从Kafka Topic最大偏移量开始消费数据。

在这里插入图片描述

  • 第一、earliest:从最起始位置开始消费,当然不一定是从0开始,因为如果数据过期就清掉
    了,所以可以理解为从现存的数据里最小位置开始消费;
  • 第二、latest:从最末位置开始消费;
  • 第三、per-partition assignment:对每个分区都指定一个offset,再从offset位置开始消费

[在Kafka Consumer 参数中,有一个参数,可以进行设置,表示从哪里开始消费读取数据。]

在这里插入图片描述

​ Flink 提供KafkaConnector连接器中提供5种方式,指定从哪里开始消费数据。默认情况下,从Kafka消费数据时,采用的是:latest,最新偏移量开始消费数据

在这里插入图片描述

  1. setStartFromGroupOffsets
    • 从groupId上次消费数据记录开始消费,将上次消费偏移量存储在topic【__consumer_offsets】;
    • 如果消费组时第一次消费数据,从最大偏移量开始消费。
  2. setStartFromEarliest:从最小偏移量消费数据
  3. setStartFromLatest:从最大偏移量消费数据
  4. setStartFromTimestamp:消费每条数据时间戳大于指定时间戳
  5. setStartFromSpecificOffsets:从指定偏移量开始消费数据,偏移量值大于设置偏移量

在这里插入图片描述

在代码中设置消费数据起始位置相关API如下所示:

在这里插入图片描述

案例演示代码如下所示:

package cn.itqzd.flink.connector;

import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.internals.KafkaTopicPartition;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * Flink从Kafka消费数据,指定topic名称和反序列化类,可以指定消费数据开始偏移量
 */
public class ConnectorFlinkKafkaConsumerOffsetDemo {

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment() ;
		env.setParallelism(1) ;

		// 2. 数据源-source
		// 从Kafka消费数据时,设置参数值
		Properties props = new Properties() ;
		props.setProperty("bootstrap.servers", "node1:9092");
		props.setProperty("group.id", "test1");
		// 传递参数,创建FlinkKafkaConsumer实例对象
		FlinkKafkaConsumer<String> kafkaConsumer = new FlinkKafkaConsumer<String>(
			"flink-topic",
			new SimpleStringSchema(),
			props
		) ;
		// TODO: 1、Flink从topic中最初的数据开始消费
		//kafkaConsumer.setStartFromEarliest() ;

		// TODO: 2、Flink从topic中最新的数据开始消费
		//kafkaConsumer.setStartFromLatest();

		// TODO: 3、Flink从topic中指定的group上次消费的位置开始消费,所以必须配置group.id参数
		//kafkaConsumer.setStartFromGroupOffsets() ;

		// TODO: 4、Flink从topic中指定的offset开始,这个比较复杂,需要手动指定offset
		Map<KafkaTopicPartition, Long> offsets = new HashMap<>();
		offsets.put(new KafkaTopicPartition("flink-topic", 0), 100L);
		offsets.put(new KafkaTopicPartition("flink-topic", 1), 90L);
		offsets.put(new KafkaTopicPartition("flink-topic", 2), 110L);
		//kafkaConsumer.setStartFromSpecificOffsets(offsets);

		// TODO: 5、指定时间戳消费数据
		kafkaConsumer.setStartFromTimestamp(1644935966961L) ;

		// 从Kafka消费数据
		DataStreamSource<String> kafkaDataStream = env.addSource(kafkaConsumer);

		// 3. 数据转换-transformation
		// 4. 数据终端-sink
		kafkaDataStream.printToErr();

		// 5. 触发执行-execute
		env.execute("ConnectorFlinkKafkaConsumerOffsetDemo") ;
	}

}

[上面所设置消费偏移量位置,表示不考虑流式程序从Checkpoint检查点或保存点SavePoint恢复。]

在这里插入图片描述

07-Connector之Kafka 数据源【新增Topic和分区发现】


​ 有一个 Flink Job需要将五份数据聚合到一起,五份数据对应5个 kafka topic,随着业务增长,新增一类数据,同时新增1个 Kafka topic,如何在不重启作业的情况下作业自动感知新的 topic。

​ [新增TOPIC数据,如何Flink Job感知?]

在这里插入图片描述

https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/connectors/datastream/kafka/#topic-partition-subscription

Flink Kafka Source数据源,提供对应机制:topic 发现

  • 对于新增Topic来说,可以设置消费Topic名称时,采用正则表达式即可;
  • topic名称设置:java.util.regex.Pattern.compile("test-topic-[0-9]")

在这里插入图片描述

​ Flink Job从一个固定的Kafka topic 读数据,开始该 topic 有7 个 partition,但随着业务的增长数据量变大,需要对Kafka partition 个数进行扩容,由 7 个扩容到 14。该情况下如何在不重启作业情况下动态感知新扩容的 partition?

对Topic增加分区,如何Flink job感知

在这里插入图片描述

​ 构建 FlinkKafkaConsumer 时的 Properties 中设置flink.partition-discovery.interval-millis 参数为非负值,表示开启动态发现的开关,及设置的时间间隔,启动一个单独的线程定期去Kafka获取最新的meta信息。

在这里插入图片描述

08-Connector之Kafka 数据源【KafkaSource】


Flink 1.12 版本中,提供基于新API接口Data Source实现Kafka 数据源:KafkaSource,消费数据更加简单

https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/connectors/datastream/kafka/#kafka-source

在这里插入图片描述

案例演示:从Kafka消费数据,进行实时流式处理。

package cn.itqzd.flink.connector;

import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

/**
 * Flink从Kafka消费数据,指定topic名称和反序列化类
 */
public class ConnectorKafkaSourceDemo {

	public static void main(String[] args) throws Exception{
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(3);

		// 2. 数据源-source
		// 2-1. 创建KafkaSource对象,设置属性
		KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
			.setBootstrapServers("node1:9092,node2:9092,node3:9092")
			.setTopics("flink-topic")
			.setGroupId("my-group")
			.setStartingOffsets(OffsetsInitializer.earliest())
			.setValueOnlyDeserializer(new SimpleStringSchema())
			.build();
		// 2-2. 添加数据源
		DataStream<String> kafkaStream = env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "KafkaSource");

		// 3. 数据转换-transformation
		// 4. 数据接收器-sink
		kafkaStream.printToErr();

		// 5. 触发执行-execute
		env.execute("StreamKafkaSourceDemo") ;
	}

}

09-Connector之FlinkKafkaProducer


​ Flink提供Connector连接器中支持Kafka的Source数据源数据终端SinkFlink Kafka Sink核心类:FlinkKafkaProducer

在这里插入图片描述

  1. topic 名称
  2. 序列化:将Java对象转byte[]
  3. Kafka Server地址信息
  4. 容错语义

在这里插入图片描述

准备工作:启动集群、创建Topic,命令如下

[root@node1 ~]# start-zk.sh
[root@node1 ~]# start-kafka.sh

[root@node1 ~]# /export/server/kafka/bin/kafka-topics.sh --list --bootstrap-server node1:9092

[root@node1 ~]# /export/server/kafka/bin/kafka-topics.sh --create --topic flink-topic --bootstrap-server node1:9092 --replication-factor 1 --partitions 3

[root@node1 ~]# /export/server/kafka/bin/kafka-console-consumer.sh --topic flink-topic --bootstrap-server node1:9092

案例演示:自定义Source数据源,产生交易订单数据,将其转换为JSON字符串,实时保存到Kafka topic

package cn.itqzd.flink.connector;

import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import org.apache.flink.streaming.connectors.kafka.KafkaSerializationSchema;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;

import javax.annotation.Nullable;
import java.util.Properties;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 将DataStream数据流中数据保存至Kafka Topic队列,使用FlinkKafkaProducer类完成
 * @author xuyuan
 */
public class ConnectorFlinkKafkaProducerDemo {

	@Data
	@NoArgsConstructor
	@AllArgsConstructor
	static class Order {
		private String id;
		private Integer userId;
		private Double money;
		private Long orderTime;
	}

	/**
	 * 自定义数据源:每隔1秒产生1条交易订单数据
	 */
	private static class OrderSource extends RichParallelSourceFunction<Order> {
		// 定义标识变量,表示是否产生数据
		private boolean isRunning = true;

		// 模拟产生交易订单数据
		@Override
		public void run(SourceContext<Order> ctx) throws Exception {
			Random random = new Random() ;
			while (isRunning){
				// 构建交易订单数据
				// 构建交易订单数据
				Order order = new Order(
					UUID.randomUUID().toString(), //
					random.nextInt(10) + 1 , //
					(double)random.nextInt(100) ,//
					System.currentTimeMillis()
				);

				// 将数据输出
				ctx.collect(order);

				// 每隔1秒产生1条数据,线程休眠
				TimeUnit.SECONDS.sleep(1);
			}
		}

		@Override
		public void cancel() {
			isRunning = false ;
		}
	}


	/**
	 * 创建子类,实现接口,对数据进行序列化操作
	 */
	private static class KafkaStringSchema implements KafkaSerializationSchema<String>{
		@Override
		public ProducerRecord<byte[], byte[]> serialize(String jsonStr, @Nullable Long timestamp) {
			return new ProducerRecord<>("flink-topic", jsonStr.getBytes());
		}
	}

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(3) ;

		// 2. 数据源-source
		DataStreamSource<Order> orderDataStream = env.addSource(new OrderSource());
		//orderDataStream.printToErr();

		// 3. 数据转换-transformation

		// 将订单数据Order对象,转换为JSON字符串,存储到Kafka Topic队列
		SingleOutputStreamOperator<String> jsonDataStream = orderDataStream.map(new MapFunction<Order, String>() {
			@Override
			public String map(Order order) throws Exception {
				// 阿里巴巴库:fastJson,转换对象为json字符串
				return JSON.toJSONString(order);
			}
		});
		//jsonDataStream.printToErr();

		// 4. 数据终端-sink
		// 4-1. 写入数据时序列化
		KafkaSerializationSchema<String> serializationSchema = new KafkaStringSchema() ;
		// 4-2. 生成者属性设置
		Properties props = new Properties() ;
		props.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "node1:9092,node2:9092,node3:9092");
		// 4-3. 构建实例对象
		FlinkKafkaProducer<String> kafkaProducer = new FlinkKafkaProducer<String>(
			"flink-topic",
				serializationSchema,
				props,
				FlinkKafkaProducer.Semantic.EXACTLY_ONCE
		);
		// 4-4. 添加接收器
		jsonDataStream.addSink(kafkaProducer) ;

		// 5. 触发执行-execute
		env.execute("ConnectorFlinkKafkaProducerDemo");
	}

}  
基于flink构建实时数仓项目,按照数仓分层管理数据
    ODS层 ->  DWD 层 -> DWS层 ->  ADS 层
        |       |       |
            Kafka Topic

10-Connector之JdbcSink


Flink 1.11版本开始,增加JDBC Connector连接器,可以将DataStream数据直接保存RDBMS表中。

https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/connectors/datastream/jdbc/

  • 1)、添加Maven依赖
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-jdbc_2.11</artifactId>
    <version>1.13.1</version>
</dependency>
  • 2)、API使用说明:JdbcSink.sink

在这里插入图片描述

  • 3)、官方提供案例代码

在这里插入图片描述

需求:将Flink集合中的数据集DataStream,通过JDBCSink保存到MySQL。

package cn.itqzd.flink.connector;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.flink.connector.jdbc.JdbcConnectionOptions;
import org.apache.flink.connector.jdbc.JdbcExecutionOptions;
import org.apache.flink.connector.jdbc.JdbcSink;
import org.apache.flink.connector.jdbc.JdbcStatementBuilder;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;

import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 * 案例演示:使用JDBC Sink 连接器,将数据保存至MySQL表中,继承RichSinkFunction
 */
public class ConnectorJdbcSinkDemo {

	@Data
	@NoArgsConstructor
	@AllArgsConstructor
	private static class Student{
		private Integer id ;
		private String name ;
		private Integer age ;
	}

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(1) ;

		// 2. 数据源-source
		DataStreamSource<Student> inputDataStream = env.fromElements(
			new Student(23, "zhaoqi", 40),
			new Student(34, "zhaoliu", 19),
			new Student(35, "wangwu", 20),
			new Student(36, "zhaoliu", 19)
		);

		// 3. 数据转换-transformation
		// 4. 数据终端-sink
		// 4-1. 创建JdbcSink实例对象,传递参数信息
		SinkFunction<Student> jdbcSink = JdbcSink.sink(
			// a. 插入语句
			"REPLACE INTO db_flink.t_student (id, name, age) VALUES (?, ?, ?)", //
			// b. 构建Statement实例对象
			new JdbcStatementBuilder<Student>() {
				@Override
				public void accept(PreparedStatement pstmt, Student student) throws SQLException {
					pstmt.setInt(1, student.id);
					pstmt.setString(2, student.name);
					pstmt.setInt(3, student.age);
				}
			},
			// c. 设置执行插入参数
			JdbcExecutionOptions.builder()
				.withBatchSize(1000)
				.withBatchIntervalMs(200)
				.withMaxRetries(5)
				.build(),
			// d. 设置连接MySQL数据库信息
			new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
				.withDriverName("com.mysql.jdbc.Driver")
				.withUrl("jdbc:mysql://node1:3306/?useUnicode=true&characterEncoding=utf-8&useSSL=false")
				.withUsername("root")
				.withPassword("123456")
				.build()
		);
		// 4-2. 为数据流DataStream添加Sink
		inputDataStream.addSink(jdbcSink) ;

		// 5. 触发执行-execute
		env.execute("StreamJdbcSinkDemo") ;
	}
}

11-Connector之Streaming File Sink


​ 大数据业务场景中,经常有一种场景:[外部数据发送到kafka中,Flink作为中间件消费Kafka数据并进行业务处理,处理完成之后的数据可能还需要写入到数据库或者文件系统中,如写入HDFS中]。

在这里插入图片描述

StreamingFileSinkFlink1.7中推出的新特性,可以用来将分区文件写入到支持 Flink FileSystem 接口的文件系统中,支持Exactly-Once语义

https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/connectors/datastream/streamfile_sink/

​ Streaming File Sink 会将数据写入到桶Bucket(认可Hive中分区目录)中,可以设置存储目录名称、文件大小和文件名称

  • 由于输入流可能是无界的,因此每个桶中的数据被划分为多个有限大小的文件。
  • 如何分桶是可以配置的,默认使用基于时间的分桶策略,这种策略每个小时创建一个新的桶,桶中包含的文件将记录所有该小时内从流中接收到的数据。

在这里插入图片描述

​ 桶目录中的实际输出数据会被划分为多个部分文件(part file),每一个接收桶数据的 Sink Subtask ,至少包含一个部分文件(part file)。

  • 额外的部分文件(part file)将根据滚动策略创建,滚动策略是可以配置的。
  • 默认的策略是根据文件大小和超时时间来滚动文件。
  • 超时时间指打开文件的最长持续时间,以及文件关闭前的最长非活动时间。

​ [使用 StreamingFileSink 时需要启用 Checkpoint ,每次做 Checkpoint 时写入完成。如果 Checkpoint 被禁用,部分文件(part file)将永远处于 ‘in-progress’ 或 ‘pending’ 状态,下游系统无法安全地读取。]

案例演示:编写Flink程序,自定义数据源产生交易订单数据,接收后的数据流式方式存储到本地文件系统

# 1. 执行环境-env
	1-1.设置并行度为:3
	1-2.设置Checkpoint检查点,如果不设置,数据不会写入文件
	
# 2. 数据源-source
	自定义数据源,产生交易订单数据
	数据格式:e7057860-e88,u-14395,94.24,1630142114565
	
# 4. 数据接收器-sink
	4-1. 设置数据存储文件格式
	4-2. 设置输出文件大小滚动策略,什么时候产生新文件
	4-3. 设置文件的名称
	4-4. 添加Sink,设置并行度为:1
	
# 5.触发执行-execute

具体代码如下所示:

package cn.itqzd.flink.connector;

import org.apache.commons.lang3.time.FastDateFormat;
import org.apache.flink.api.common.serialization.SimpleStringEncoder;
import org.apache.flink.core.fs.Path;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.filesystem.OutputFileConfig;
import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink;
import org.apache.flink.streaming.api.functions.sink.filesystem.bucketassigners.DateTimeBucketAssigner;
import org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.DefaultRollingPolicy;
import org.apache.flink.streaming.api.functions.source.ParallelSourceFunction;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * Flink Stream 流计算,将DataStream 保存至文件系统,使用FileSystem Connector
 */
public class ConnectorStreamingFileSinkDemo {

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(3);
		// TODO: 设置检查点
		env.enableCheckpointing(5000) ;

		// 2. 数据源-source
		DataStreamSource<String> orderDataStream = env.addSource(new OrderSource());
		//orderDataStream.print();

		// 3. 数据转换-transformation
		// 4. 数据终端-sink
		StreamingFileSink<String> fileSink = StreamingFileSink
			// 4-1. 设置存储文件格式,Row行存储
			.forRowFormat(
				new Path("datas/file-sink"), new SimpleStringEncoder<String>()
			)
			// 4-2. 设置桶分配政策,默认基于时间的分配器,每小时产生一个桶,格式如下yyyy-MM-dd--HH
			.withBucketAssigner(new DateTimeBucketAssigner<>())
			// 4-3. 设置数据文件滚动策略
			.withRollingPolicy(
				DefaultRollingPolicy.builder()
					.withRolloverInterval(TimeUnit.SECONDS.toMillis(5))
					.withInactivityInterval(TimeUnit.SECONDS.toMillis(10))
					.withMaxPartSize(2 * 1024 * 1024)
					.build()
			)
			// 4-4. 设置文件名称
			.withOutputFileConfig(
				OutputFileConfig.builder()
					.withPartPrefix("itqzd")
					.withPartSuffix(".log")
					.build()
			)
			.build();
		// 4-4. 数据流DataStream添加Sink
		orderDataStream.addSink(fileSink).setParallelism(1);

		// 5. 触发执行
		env.execute("ConnectorStreamingFileSinkDemo");
	}

	/**
	 * 自定义数据源,实时产生交易订单数据
	 */
	private static class OrderSource implements ParallelSourceFunction<String> {
		private boolean isRunning = true ;
		private FastDateFormat format = FastDateFormat.getInstance("yyyyMMddHHmmssSSS");
		@Override
		public void run(SourceContext<String> ctx) throws Exception {
			Random random = new Random();
			while (isRunning){
				// 交易订单
				long timeMillis = System.currentTimeMillis();
				String orderId = format.format(timeMillis) + (10000 + random.nextInt(10000))  ;
				String userId = "u_" + (10000 + random.nextInt(10000)) ;
				double orderMoney = new BigDecimal(random.nextDouble() * 100).setScale(2, RoundingMode.HALF_UP).doubleValue() ;
				String output = orderId + "," + userId + "," + orderMoney + "," + timeMillis ;
				System.out.println(output);
				// 输出
				ctx.collect(output);
				TimeUnit.MILLISECONDS.sleep(100);
			}
		}

		@Override
		public void cancel() {
			isRunning = false ;
		}
	}
}

12-Connector之FileSink


Flink 1.12 中,提供流批统一的 FileSink connector,以替换现有的 StreamingFileSink connector (FLINK-19758),允许为 BATCH 和 STREAMING 两种执行模式,实现不同的运行时策略,以达到仅使用一种 sink 实现。

https://nightlies.apache.org/flink/flink-docs-release-1.13/docs/connectors/datastream/file_sink/

在这里插入图片描述

  • 1)、添加Maven依赖
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-files</artifactId>
    <version>1.13.1</version>
</dependency>
  • 2)、API使用:FileSink

在这里插入图片描述

  • 3)、案例演示:自定义数据源产生交易订单数据,写入本地文件系统文件中
package cn.itqzd.flink.connector;

import org.apache.commons.lang3.time.FastDateFormat;
import org.apache.flink.api.common.serialization.SimpleStringEncoder;
import org.apache.flink.connector.file.sink.FileSink;
import org.apache.flink.core.fs.Path;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.filesystem.OutputFileConfig;
import org.apache.flink.streaming.api.functions.sink.filesystem.bucketassigners.DateTimeBucketAssigner;
import org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.DefaultRollingPolicy;
import org.apache.flink.streaming.api.functions.source.ParallelSourceFunction;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * Flink Stream 流计算,将DataStream 保存至文件系统,使用FileSystem Connector
 */
public class _11StreamBatchFileSinkDemo {

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(3);
		// TODO: 设置检查点
		env.enableCheckpointing(1000) ;

		// 2. 数据源-source
		DataStreamSource<String> orderDataStream = env.addSource(new OrderSource());
		//orderDataStream.print();

		// 3. 数据转换-transformation
		// 4. 数据终端-sink
		FileSink<String> fileSink = FileSink
			// 4-1. 设置存储文件格式,Row行存储
			.forRowFormat(
				new Path("datas/file-sink"), new SimpleStringEncoder<String>("UTF-8")
			)
			// 4-2. 设置桶分配政策,默认基于时间的分配器,每小时产生一个桶,格式如下yyyy-MM-dd--HH
			.withBucketAssigner(new DateTimeBucketAssigner<>())
			// 4-3. 设置数据文件滚动策略
			.withRollingPolicy(
				DefaultRollingPolicy.builder()
					.withRolloverInterval(TimeUnit.SECONDS.toMillis(5))
					.withInactivityInterval(TimeUnit.SECONDS.toMillis(10))
					.withMaxPartSize(2 * 1024 * 1024)
					.build()
			)
			// 4-4. 设置文件名称
			.withOutputFileConfig(
				OutputFileConfig.builder()
					.withPartPrefix("itqzd")
					.withPartSuffix(".log")
					.build()
			)
			.build();
		// 4-4. 数据流DataStream添加Sink
		orderDataStream.sinkTo(fileSink).setParallelism(1) ;

		// 5. 触发执行
		env.execute("StreamFileSinkDemo");
	}

	/**
	 * 自定义数据源,实时产生交易订单数据
	 */
	private static class OrderSource implements ParallelSourceFunction<String> {
		private boolean isRunning = true ;
		private FastDateFormat format = FastDateFormat.getInstance("yyyyMMddHHmmssSSS");
		@Override
		public void run(SourceContext<String> ctx) throws Exception {
			Random random = new Random();
			while (isRunning){
				// 交易订单
				long timeMillis = System.currentTimeMillis();
				String orderId = format.format(timeMillis) + (10000 + random.nextInt(10000))  ;
				String userId = "u_" + (10000 + random.nextInt(10000)) ;
				double orderMoney = new BigDecimal(random.nextDouble() * 100).setScale(2, RoundingMode.HALF_UP).doubleValue() ;
				String output = orderId + "," + userId + "," + orderMoney + "," + timeMillis ;
				System.out.println(output);
				// 输出
				ctx.collect(output);
				TimeUnit.MILLISECONDS.sleep(100);
			}
		}

		@Override
		public void cancel() {
			isRunning = false ;
		}
	}
}

​ 对比StreamingFileSink接收器和FileSink接收器,发现文件名称稍有不同,一个使用subTask任务编号,一个使用subTask任务TaskId。

在这里插入图片描述

13-Connector之RedisSink


Flink中提供Connector:RedisSink,将DataStream可以保存到Redis数据库中。

https://bahir.apache.org/docs/flink/current/flink-streaming-redis/

  • 1)、添加Maven 依赖
<dependency>
  <groupId>org.apache.bahir</groupId>
  <artifactId>flink-connector-redis_2.11</artifactId>
  <version>1.0</version>
</dependency>
  • 2)、核心类:RedisSink,创建对象,传递RedisMapper实例;此外,底层采用Jedis Client 第三方库写入Redis数据库。

在这里插入图片描述

  • 3)、RedisMapper映射接口方法:

在这里插入图片描述

  • 4)、官方实例代码:

在这里插入图片描述

案例演示:将Flink集合中的数据通过RedisSink进行保存

package cn.itqzd.flink.connector;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.redis.RedisSink;
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisPoolConfig;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisCommand;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisCommandDescription;
import org.apache.flink.streaming.connectors.redis.common.mapper.RedisMapper;
import org.apache.flink.util.Collector;

/**
 * 案例演示:将数据保存至Redis中,直接使用官方提供Connector
 *      https://bahir.apache.org/docs/flink/current/flink-streaming-redis/
 */
public class ConnectorRedisSinkDemo {

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(1) ;

		// 2. 数据源-source
		DataStreamSource<String> inputDataStream = env.socketTextStream("node1", 9999);

		// 3. 数据转换-transformation
		SingleOutputStreamOperator<Tuple2<String, Integer>> resultDataStream = inputDataStream
			// a. 过滤数据
			.filter(line -> null != line && line.trim().length() > 0)
			// b. 分割单词
			.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
				@Override
				public void flatMap(String line, Collector<Tuple2<String, Integer>> out) throws Exception {
					String[] words = line.trim().split("\\W+");
					for (String word : words) {
						out.collect(Tuple2.of(word, 1));
					}
				}
			})
			// c. 按照单词分组及对组内聚合操作
			.keyBy(tuple -> tuple.f0).sum("f1");
		//resultDataStream.printToErr();

		// 4. 数据终端-sink
		/*
			spark -> 15
			flink -> 20
			hive -> 10
			--------------------------------------------
			Redis 数据结构:哈希Hash
				Key:
					flink:word:count
				Value: 哈希
					field  value
					spark  15
					flink  20
					hive   10
			命令:
				HSET flink:word:count spark 15
		 */
		// 4-1. 构建Redis Server配置
		FlinkJedisPoolConfig config = new FlinkJedisPoolConfig.Builder()
			.setHost("node1")
			.setPort(6379)
			.setDatabase(0)
			.setMinIdle(3)
			.setMaxIdle(8)
			.setMaxTotal(8)
			.build();
		// 4-2. 构建RedisMapper实例
		RedisMapper<Tuple2<String, Integer>> redisMapper = new RedisMapper<Tuple2<String, Integer>>() {
			// HSET flink:word:count spark 15 -> 拆分到如下三个方法中
			@Override
			public RedisCommandDescription getCommandDescription() {
				return new RedisCommandDescription(RedisCommand.HSET, "flink:word:count");
			}

			@Override
			public String getKeyFromData(Tuple2<String, Integer> data) {
				return data.f0;
			}

			@Override
			public String getValueFromData(Tuple2<String, Integer> data) {
				return data.f1 + "";
			}
		};
		// 4-3. 创建RedisSink对象
		RedisSink<Tuple2<String, Integer>> redisSink = new RedisSink<Tuple2<String, Integer>>(
			config, redisMapper
		);
		// 4-4. 添加Sink
		resultDataStream.addSink(redisSink) ;

		// 5. 触发执行-execute
		env.execute("ConnectorRedisSinkDemo") ;
	}

}

运行流式程序,输入测试数据,查看Redis中结果数据,截图如下所示:

在这里插入图片描述

第三部分:批处理高级特性


14-批处理特性之累加器Accamulator


​ Flink中的累加器Accumulator ,与Mapreduce counter的应用场景类似,可以很好地观察task在运行期间的数据变化,如在Flink job任务中的算子函数中操作累加器,在任务执行结束之后才能获得累加器的最终结果。

在这里插入图片描述

Flink有以下内置累加器,每个累加器都实现了Accumulator接口:

在这里插入图片描述

使用累加器进行累加统计操作时,步骤如下:

1.创建累加器
	private IntCounter numLines = new IntCounter();
2.注册累加器
	getRuntimeContext().addAccumulator("num-lines", this.numLines);
3.使用累加器
	this.numLines.add(1);
4.获取累加器的结果
	myJobExecutionResult.getAccumulatorResult("num-lines")

编写程序:对数据源读取的数据进行计数Counter,最终输出所出来的数据条目数。

在这里插入图片描述

package cn.itqzd.flink.batch;

import org.apache.flink.api.common.JobExecutionResult;
import org.apache.flink.api.common.accumulators.IntCounter;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.operators.DataSource;
import org.apache.flink.api.java.operators.MapOperator;
import org.apache.flink.configuration.Configuration;

/**
 * 案例压实:Flink中累加器Accumulator使用,统计处理数据条目数
 * @author xuyuan
 */
public class BatchAccumulatorDemo {

	/**
	 * 定义子类,实现函数接口,重写抽象方法map:实现数据处理和计数器累加功能
	 */
	private static class CounterMapFunction extends RichMapFunction<String, String> {
		// todo step1. 定义累加器
		private IntCounter counter = new IntCounter();

		@Override
		public void open(Configuration parameters) throws Exception {
			// todo step2. 注册累加器
			getRuntimeContext().addAccumulator("counter", counter);
		}

		@Override
		public String map(String value) throws Exception {
			// todo step3. 使用累加器进行计数
			counter.add(1);

			return value;
		}
	}

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(1);

		// 2. 数据源-source
		DataSource<String> dataset = env.readTextFile("datas/click.log");

		// 3. 数据转换-transformation
		MapOperator<String, String> mapDataSet = dataset.map(new CounterMapFunction());

		// 4. 数据终端-sink
		mapDataSet.writeAsText("datas/click.txt");

		// 5. 触发执行-execute
		JobExecutionResult jobResult = env.execute("BatchAccumulatorDemo");

		// todo step4. 获取累加器值
		Object counter = jobResult.getAccumulatorResult("counter");
		System.out.println("counter = " + counter);
	}

}  

15-批处理特性之广播变量Broadcast


​ Flink支持广播,可以将数据广播到TaskManager上就可以供TaskManager中的SubTask/task去使用,数据存储到内存中。

在这里插入图片描述

  1. 可以理解广播就是一个公共的共享变量
  2. 将一个数据集广播后,不同的Task都可以在节点上获取到
  3. 每个节点(TaskManager)只存一份
  4. 如果不使用广播,每一个Task都会拷贝一份数据集,造成内存资源浪费
  • 第一、广播变量是要把dataset广播到内存中,所以广播的数据量不能太大,否则会出现OOM
  • 第二、广播变量的值不可修改,这样才能确保每个节点获取到的值都是一致的

使用广播变量步骤:

1:广播数据
	.withBroadcastSet(DataSet, "name");
	
2:获取广播的数据
	Collection<> broadcastSet = getRuntimeContext().getBroadcastVariable("name");
	
3:使用广播数据

​ 将studentDS(学号,姓名)集合广播出去(广播到各个TaskManager内存中),然后使用scoreDS(学号,学科,成绩)和广播数据(学号,姓名)进行关联,得到这样格式的数据:(姓名,学科,成绩)

		// 大表数据
		DataSource<Tuple3<Integer, String, Integer>> scoreDataSet = env.fromCollection(
			Arrays.asList(
				Tuple3.of(1, "语文", 50),
				Tuple3.of(1, "数学", 70),
				Tuple3.of(1, "英语", 86),
				Tuple3.of(2, "语文", 80),
				Tuple3.of(2, "数学", 86),
				Tuple3.of(2, "英语", 96),
				Tuple3.of(3, "语文", 90),
				Tuple3.of(3, "数学", 68),
				Tuple3.of(3, "英语", 92)
			)
		);
		// 小表数据
		DataSource<Tuple2<Integer, String>> studentDataSet = env.fromCollection(
			Arrays.asList(
				Tuple2.of(1, "张三"),
				Tuple2.of(2, "李四"),
				Tuple2.of(3, "王五")
			)
		);

在这里插入图片描述

编写代码,完成小数据集DataSet广播,与大数据集进行关联操作,代码如下所示:

package cn.itqzd.flink.batch;

import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.operators.DataSource;
import org.apache.flink.api.java.operators.MapOperator;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.configuration.Configuration;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Flink 批处理中广播变量:将小数据集广播到TaskManager内存中,便于Slot中运行SubTask任务使用
 * @author xuyuan
 */
public class BatchBroadcastDemo {

	/**
	 * 定义子类,实现函数接口MapFunction,重写方法map:实现数据处理,使用广播变量
	 */
	private static class BroadcastMapFunction
			extends RichMapFunction<Tuple3<Integer, String, Integer>, String> {
		// 定义Map集合,存储广播数据
		private Map<Integer, String> stuMap = new HashMap<>() ;

		@Override
		public void open(Configuration parameters) throws Exception {
			// todo step2. 获取广播的数据集
			List<Tuple2<Integer, String>> list = getRuntimeContext().getBroadcastVariable("students");
			// todo step3. 将广播变量数据放到map聚合中,当处理大表数据时,依据key获取小表中value值
			for (Tuple2<Integer, String> tuple2 : list) {
				stuMap.put(tuple2.f0, tuple2.f1) ;
			}
		}

		@Override
		public String map(Tuple3<Integer, String, Integer> value) throws Exception {
			// value:  Tuple3.of(1, "语文", 50)
			Integer studentId = value.f0;
			String subjectName = value.f1;
			Integer studentScore = value.f2;
			// 依据学生ID获取学生名称
			String studentName = stuMap.getOrDefault(studentId, "未知");
			// 拼凑字符串并返回
			return studentName + ", " + subjectName + ", " + studentScore;
		}

	}

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment() ;
		env.setParallelism(1);

		// 2. 数据源-source,todo:从本地集合创建2个DataSet
		// 大表数据
		DataSource<Tuple3<Integer, String, Integer>> scoreDataSet = env.fromCollection(
			Arrays.asList(
				Tuple3.of(1, "语文", 50),
				Tuple3.of(1, "数学", 70),
				Tuple3.of(1, "英语", 86),
				Tuple3.of(2, "语文", 80),
				Tuple3.of(2, "数学", 86),
				Tuple3.of(2, "英语", 96),
				Tuple3.of(3, "语文", 90),
				Tuple3.of(3, "数学", 68),
				Tuple3.of(3, "英语", 92)
			)
		);
		// 小表数据
		DataSource<Tuple2<Integer, String>> studentDataSet = env.fromCollection(
			Arrays.asList(
				Tuple2.of(1, "张三"),
				Tuple2.of(2, "李四"),
				Tuple2.of(3, "王五")
			)
		);

		// 3. 数据转换-transformation: 使用map算子,对大表中每条数据处理,处理时使用小表数据
		/*
				Tuple3.of(1, "语文", 50)    --小表-->   张三", "语文", 50
		 */
		MapOperator<Tuple3<Integer, String, Integer>, String> resultDataSet = scoreDataSet
			.map(new BroadcastMapFunction())
			// todo step1. 将小表数据集广播出去,哪个算子使用小表,就在算子后面进行广播
			.withBroadcastSet(studentDataSet, "students");

		// 4. 数据接收器-sink
		resultDataSet.printToErr();
	}

}

16-批处理特性之分布式缓存Cache


Flink提供了一个类似于Hadoop的分布式缓存,让并行运行实例的函数可以在本地访问。

在这里插入图片描述

  • 广播变量:使用数据为数据集DataSet,将其广播到TaskManager内存中,被Task使用;
  • 分布式缓存:缓存数据文件数据,数据放在文件中;

[广播变量是将变量(DataSet)分发到各个TaskManager节点的内存上,分布式缓存是将文件缓存到各个TaskManager节点上]

编码步骤:

在这里插入图片描述

修改上述使用广播变量,进行大表与小表数据案例,将小表数据放入文件中,采用分布式缓存,然后关联。

在这里插入图片描述

package cn.itqzd.flink.batch;

import org.apache.commons.io.FileUtils;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.operators.DataSource;
import org.apache.flink.api.java.operators.MapOperator;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.configuration.Configuration;

import java.io.File;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Flink 批处理中分布式缓存:将小文件数据进行缓存到TaskManager中,被Slot中运行subTask任务使用
 * @author xuyuan
 */
public class BatchDistributedCacheDemo {

	/**
	 * 定义子类,实现函数接口MapFunction,重写方法map:实现数据处理,获取分布式缓存数据,存储到map集合中
	 */
	private static class CacheMapFunction
			extends RichMapFunction<Tuple3<Integer, String, Integer>, String> {
		// 定义Map集合,存储广播数据
		private Map<Integer, String> stuMap = new HashMap<>() ;

		@Override
		public void open(Configuration parameters) throws Exception {
			// todo step2. 获取分布式缓存的数据
			File file = getRuntimeContext().getDistributedCache().getFile("cache_students");

			// todo step3. 读取缓存文件数据   // 1,张三
			List<String> list = FileUtils.readLines(file, Charset.defaultCharset()); 
			for (String item : list) {
				String[] array = item.split(",");
				stuMap.put(Integer.parseInt(array[0]), array[1]) ;
			}
			System.out.println(stuMap);
		}

		@Override
		public String map(Tuple3<Integer, String, Integer> value) throws Exception {
			// value:  Tuple3.of(1, "语文", 50)
			Integer studentId = value.f0;
			String subjectName = value.f1;
			Integer studentScore = value.f2;
			// 依据学生ID获取学生名称
			String studentName = stuMap.getOrDefault(studentId, "未知");
			// 拼凑字符串并返回
			return studentName + ", " + subjectName + ", " + studentScore;
		}

	}

	public static void main(String[] args) throws Exception {
		// 1. 执行环境-env
		ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment() ;
		env.setParallelism(1);

		// todo step1. 将数据文件进行缓存,不能太大,属于小文件数据
		env.registerCachedFile("datas/distribute_cache_student", "cache_students");

		// 2. 数据源-source,todo:从本地集合创建1个DataSet
		// 大表数据
		DataSource<Tuple3<Integer, String, Integer>> scoreDataSet = env.fromCollection(
			Arrays.asList(
				Tuple3.of(1, "语文", 50),
				Tuple3.of(1, "数学", 70),
				Tuple3.of(1, "英语", 86),
				Tuple3.of(2, "语文", 80),
				Tuple3.of(2, "数学", 86),
				Tuple3.of(2, "英语", 96),
				Tuple3.of(3, "语文", 90),
				Tuple3.of(3, "数学", 68),
				Tuple3.of(3, "英语", 92)
			)
		);

		// 3. 数据转换-transformation: 使用map算子,对大表中每条数据处理,处理时使用小表数据
		/*
				Tuple3.of(1, "语文", 50)    --小表-->   张三", "语文", 50
		 */
		MapOperator<Tuple3<Integer, String, Integer>, String> resultDataSet = scoreDataSet
				.map(new CacheMapFunction());

		// 4. 数据接收器-sink
		resultDataSet.printToErr();
	}

}

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值