super(environment, new SourceTransformation<>(sourceName, operator, outTypeInfo, environment.getParallelism()));
this.isParallel = isParallel;
if (!isParallel) {
setParallelism(1);
}
}
protected SingleOutputStreamOperator(StreamExecutionEnvironment environment, StreamTransformation transformation) {
super(environment, transformation);
}
public DataStream(StreamExecutionEnvironment environment, StreamTransformation transformation) {
this.environment = Preconditions.checkNotNull(environment, “Execution Environment must not be null.”);
this.transformation = Preconditions.checkNotNull(transformation, “Stream Transformation must not be null.”);
}
可见构建过程就是初始化了DataStream中的`environment`和`transformation`这两个属性。
其中 `transformation` 赋值的是 `SourceTranformation` 的一个实例,`SourceTransformation`是 `StreamTransformation` 的子类,而`StreamTransformation`则描述了创建一个`DataStream`的操作。对于每个DataStream,其底层都是有一个StreamTransformation的具体实例的,所以在DataStream在构造初始时会为其属性transformation设置一个具体的实例。并且DataStream的很多接口的调用都是直接调用的StreamTransformation的相应接口,如并行度、id、输出数据类型信息、资源描述等。
通过上述过程,根据指定的hostname和port进行数据产生的数据源就构造完成了,获得的是一个`DataStreamSource`的实例,描述的是一个输出数据类型是String的数据流的源。
在上述的数据源的构建过程中,出现 **Function(SourceFunction)、StreamOperator、StreamTransformation、DataStream** 这四个接口:
* **Function**接口:用户通过继承该接口的不同子类来实现用户自己的数据处理逻辑,如上述中实现了SourceFunction这个子类,来实现从指定hostname和port来接收数据,并转发字符串的逻辑;
* **StreamOperator**接口:数据流操作符的基础接口,该接口的具体实现子类中,会有保存用户自定义数据处理逻辑的函数的属性,负责对userFunction的调用,以及调用时传入所需参数,比如在StreamSource这个类中,在调用SourceFunction的run方法时,会构建一个SourceContext的具体实例,作为入参,用于run方法中,进行数据的转发;
* **StreamTransformation**接口:该接口描述了构建一个DataStream的操作,以及该操作的并行度、输出数据类型等信息,并有一个属性,用来持有StreamOperator的一个具体实例;
* **DataStream**:描述的是一个具有相同数据类型的数据流,底层是通过具体的StreamTransformation来实现,其负责提供各种对流上的数据进行操作转换的API接口。
通过上述的关系,最终用户自定义数据处理逻辑的函数,以及并行度、输出数据类型等就都包含在了DataStream中,而DataStream也就可以很好的描述一个具体的数据流了。
上述四个接口的包含关系是这样的:`Function –> StreamOperator –> StreamTransformation –> DataStream`。
通过数据源的构造,理清Flink数据流中的几个接口的关系后,接下来再来看如何在数据源上进行各种操作,达到最终的数据统计分析的目的。
### 四、操作数据流
进行具体的转换操作:
DataStream windowCounts = text
.flatMap(new FlatMapFunction<String, WordWithCount>() {
@Override
public void flatMap(String value, Collector out) {
for (String word : value.split(“\s”)) {
out.collect(new WordWithCount(word, 1L));
}
}
})
.keyBy(“word”)
.timeWindow(Time.seconds(5), Time.seconds(1))
.reduce(new ReduceFunction() {
@Override
public WordWithCount reduce(WordWithCount a, WordWithCount b) {
return new WordWithCount(a.word, a.count + b.count);
}
});
**这段逻辑中,对数据流做了四次操作,分别是flatMap、keyBy、timeWindow、reduce,接下来分别介绍每个转换都做了些什么操作。**
#### 4.1 flatMap 转换
`flatMap`的入参是一个`FlatMapFunction`的具体实现,功能就是将接收到的字符串,按空格切割成不同单词,然后每个单词构建一个WordWithCount实例,然后向下游转发,用于后续的数据统计。然后调用DataStream的flatMap方法,进行数据流的转换,如下:
public SingleOutputStreamOperator flatMap(FlatMapFunction<T, R> flatMapper) {
TypeInformation outType = TypeExtractor.getFlatMapReturnTypes(clean(flatMapper),
getType(), Utils.getCallLocationName(), true);
/** 根据传入的flatMapper这个Function,构建StreamFlatMap这个StreamOperator的具体子类实例 */
return transform(“Flat Map”, outType, new StreamFlatMap<>(clean(flatMapper)));
}
public SingleOutputStreamOperator transform(String operatorName, TypeInformation outTypeInfo, OneInputStreamOperator<T, R> operator) {
/** 读取输入转换的输出类型, 如果是MissingTypeInfo, 则及时抛出异常, 终止操作 */
transformation.getOutputType();
OneInputTransformation<T, R> resultTransform = new OneInputTransformation<>(
this.transformation,
operatorName,
operator,
outTypeInfo,
environment.getParallelism());
@SuppressWarnings({ “unchecked”, “rawtypes” })
SingleOutputStreamOperator returnStream = new SingleOutputStreamOperator(environment, resultTransform);
getExecutionEnvironment().addOperator(resultTransform);
return returnStream;
}
整个构建过程,与构建数据源的过程相似。
>
> a、先根据传入的flatMapper这个Function构建一个StreamOperator的具体子类StreamFlatMap的实例;
> b、根据a中构建的StreamFlatMap的实例,构建出OneInputTransFormation这个StreamTransformation的子类的实例;
> c、再构建出DataStream的子类SingleOutputStreamOperator的实例。
>
>
>
除了构建出了 `SingleOutputStreamOperator` 这个实例为并返回外,还有代码:
getExecutionEnvironment().addOperator(resultTransform);
public void addOperator(StreamTransformation<?> transformation) {
Preconditions.checkNotNull(transformation, “transformation must not be null.”);
this.transformations.add(transformation);
}
就是将上述构建的`OneInputTransFormation`的实例,添加到了`StreamExecutionEnvironment`的属性`transformations`这个类型为`List`。
#### 4.2 keyBy 转换
这里的keyBy转换,入参是一个字符串”word”,意思是按照WordWithCount中的word字段进行分区操作。
public KeyedStream<T, Tuple> keyBy(String… fields) {
return keyBy(new Keys.ExpressionKeys<>(fields, getType()));
}
先根据传入的字段名数组,以及数据流的输出数据类型信息,构建出用来描述key的ExpressionKeys的实例,ExpressionKeys有两个属性:
/** key字段的列表, FlatFieldDescriptor 描述了每个key, 在所在类型中的位置以及key自身的数据类信息 */
private List keyFields;
/** 包含key的数据类型的类型信息, 与构造函数入参中的字段顺序一一对应 */
private TypeInformation<?>[] originalKeyTypes;
在获取key的描述之后,继续调用keyBy的重载方法:
private KeyedStream<T, Tuple> keyBy(Keys keys) {
return new KeyedStream<>(this, clean(KeySelectorUtil.getSelectorForKeys(keys,
getType(), getExecutionConfig())));
}
这里首先构建了一个KeySelector的子类ComparableKeySelector的实例,作用就是从具体的输入实例中,提取出key字段对应的值(可能是多个key字段)组成的元组(Tuple)。
对于这里的例子,就是从每个WordWithCount实例中,提取出word字段的值。
然后构建一个KeyedStream的实例,KeyedStream也是DataStream的子类。构建过程如下:
public KeyedStream(DataStream dataStream, KeySelector<T, KEY> keySelector) {
this(dataStream, keySelector, TypeExtractor.getKeySelectorTypes(keySelector, dataStream.getType()));
}
public KeyedStream(DataStream dataStream, KeySelector<T, KEY> keySelector, TypeInformation keyType) {
super(
dataStream.getExecutionEnvironment(),
new PartitionTransformation<>(
dataStream.getTransformation(),
new KeyGroupStreamPartitioner<>(keySelector, StreamGraphGenerator.DEFAULT_LOWER_BOUND_MAX_PARALLELISM)));
this.keySelector = keySelector;
this.keyType = validateKeyType(keyType);
}
在进行父类构造函数调用之前,先基于keySelector构造了一个KeyGroupStreamPartitioner的实例,再进一步构造了一个PartitionTransformation实例。
这里与flatMap的转换略有不同:
>
> a、flatMap中,根据传入的flatMapper这个Function构建的是StreamOperator这个接口的子类的实例,而keyBy中,则是根据keySelector构建了ChannelSelector接口的子类实例;
> b、keyBy中构建的StreamTransformation实例,并没有添加到StreamExecutionEnvironment的属性transformations这个列表中。
>
>
>
ChannelSelector只有一个接口,根据传入的数据流中的具体数据记录,以及下个节点的并行度来决定该条记录需要转发到哪个通道。
public interface ChannelSelector {
int[] selectChannels(T record, int numChannels);
}
KeyGroupStreamPartitioner中该方法的实现如下:
public int[] selectChannels(
SerializationDelegate<StreamRecord> record,
int numberOfOutputChannels) {
K key;
try {
/** 通过keySelector从传入的record中提取出对应的key */
key = keySelector.getKey(record.getInstance().getValue());
} catch (Exception e) {
throw new RuntimeException("Could not extract key from " + record.getInstance().getValue(), e);
}
/** 根据提取的key,最大并行度,以及输出通道数,决定出record要转发到的通道编号 */
returnArray[0] = KeyGroupRangeAssignment.assignKeyToParallelOperator(key, maxParallelism, numberOfOutputChannels);
return returnArray;
}
再进一步看一下KeyGroupRangerAssignment的assignKeyToParallelOperator方法的实现逻辑。
public static int assignKeyToParallelOperator(Object key, int maxParallelism, int parallelism) {
return computeOperatorIndexForKeyGroup(maxParallelism, parallelism, assignToKeyGroup(key, maxParallelism));
}
public static int assignToKeyGroup(Object key, int maxParallelism) {
return computeKeyGroupForKeyHash(key.hashCode(), maxParallelism);
}
public static int computeKeyGroupForKeyHash(int keyHash, int maxParallelism) {
return MathUtils.murmurHash(keyHash) % maxParallelism;
}
public static int computeOperatorIndexForKeyGroup(int maxParallelism, int parallelism, int keyGroupId) {
return keyGroupId * parallelism / maxParallelism;
}
>
> a、先通过key的hashCode,算出maxParallelism的余数,也就是可以得到一个[0, maxParallelism)的整数;
> b、在通过公式 keyGroupId \* parallelism / maxParallelism ,计算出一个[0, parallelism)区间的整数,从而实现分区功能。
>
>
>
#### 4.3 timeWindow 转换
这里timeWindow转换的入参是两个时间,第一个参数表示窗口长度,第二个参数表示窗口滑动的时间间隔。
public WindowedStream<T, KEY, TimeWindow> timeWindow(Time size, Time slide) {
if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
return window(SlidingProcessingTimeWindows.of(size, slide));
} else {
return window(SlidingEventTimeWindows.of(size, slide));
}
}
根据环境配置的数据流处理时间特征构建不同的WindowAssigner的具体实例。
WindowAssigner的功能就是对于给定的数据流中的记录,决定出该记录应该放入哪些窗口中,并提供触发器等供。默认的时间特征是ProcessingTime,所以这里会构建一个SlidingProcessingTimeWindow实例,来看下SlidingProcessingTimeWindow类的assignWindows方法的实现逻辑。
public Collection assignWindows(Object element, long timestamp, WindowAssignerContext context) {
/** 根据传入的WindowAssignerContext获取当前处理时间 */
timestamp = context.getCurrentProcessingTime();
List windows = new ArrayList<>((int) (size / slide));
/** 获取最近一次的窗口的开始时间 */
long lastStart = TimeWindow.getWindowStartWithOffset(timestamp, offset, slide);
/** 循环找出满足条件的所有窗口 */
for (long start = lastStart;
start > timestamp - size;
start -= slide) {
windows.add(new TimeWindow(start, start + size));
}
return windows;
}
看一下根据给定时间戳获取最近一次的窗口的开始时间的实现逻辑。
public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
return timestamp - (timestamp - offset + windowSize) % windowSize;
}
通过一个例子来解释上述代码的逻辑。比如:
>
> a、timestamp = 1520406257000 // 2018-03-07 15:04:17
> b、offset = 0
> c、windowSize = 60000
> d、(timestamp - offset + windowSize) % windowSize = 17000
> e、说明在时间戳 1520406257000 之前最近的窗口是在 17000 毫秒的地方
> f、timestamp - (timestamp - offset + windowSize) % windowSize = 1520406240000 // 2018-03-07 15:04:00
> g、这样就可以保证每个时间窗口都是从整点开始, 而offset则是由于时区等原因需要时间调整而设置。
>
>
>
通过上述获取WindowAssigner的子类实例后,调用window方法:
public WindowedStream<T, KEY, W> window(WindowAssigner<? super T, W> assigner) {
return new WindowedStream<>(this, assigner);
}
比keyBy转换的逻辑还简单,就是构建了一个WindowedStream实例,然后返回,就结束了。而WindowedStream是一个新的数据流,不是DataStream的子类。
WindowedStream描述一个数据流中的元素会基于key进行分组,并且对于每个key,对应的元素会被划分到多个时间窗口内。然后窗口会基于触发器,将对应窗口中的数据转发到下游节点。
#### 4.4 reduce 转换
reduce转换的入参是一个ReduceFunction的具体实现,这里的逻辑就是对收到的WordWithCount实例集合,将其中word字段相同的实际的count值累加。
public SingleOutputStreamOperator reduce(ReduceFunction function) {
if (function instanceof RichFunction) {
throw new UnsupportedOperationException("ReduceFunction of reduce can not be a RichFunction. " +
“Please use reduce(ReduceFunction, WindowFunction) instead.”);
}
/** 闭包清理 */
function = input.getExecutionEnvironment().clean(function);
return reduce(function, new PassThroughWindowFunction<K, W, T>());
}
public SingleOutputStreamOperator reduce(
ReduceFunction reduceFunction,
WindowFunction<T, R, K, W> function) {
TypeInformation inType = input.getType();
TypeInformation resultType = getWindowFunctionReturnType(function, inType);
return reduce(reduceFunction, function, resultType);
}
public SingleOutputStreamOperator reduce(ReduceFunction reduceFunction, ProcessWindowFunction<T, R, K, W> function, TypeInformation resultType) {
if (reduceFunction instanceof RichFunction) {
throw new UnsupportedOperationException(“ReduceFunction of apply can not be a RichFunction.”);
}
function = input.getExecutionEnvironment().clean(function);
reduceFunction = input.getExecutionEnvironment().clean(reduceFunction);
String callLocation = Utils.getCallLocationName();
String udfName = “WindowedStream.” + callLocation;
String opName;
KeySelector<T, K> keySel = input.getKeySelector();
OneInputStreamOperator<T, R> operator;
if (evictor != null) {
@SuppressWarnings({“unchecked”, “rawtypes”})
TypeSerializer<StreamRecord> streamRecordSerializer =
(TypeSerializer<StreamRecord>) new StreamElementSerializer(input.getType().createSerializer(getExecutionEnvironment().getConfig()));
ListStateDescriptor<StreamRecord> stateDesc =
new ListStateDescriptor<>(“window-contents”, streamRecordSerializer);
opName = “TriggerWindow(” + windowAssigner + ", " + stateDesc + ", " + trigger + ", " + evictor + ", " + udfName + “)”;
operator =
new EvictingWindowOperator<>(windowAssigner,
windowAssigner.getWindowSerializer(getExecutionEnvironment().getConfig()),
keySel,
input.getKeyType().createSerializer(getExecutionEnvironment().getConfig()),
stateDesc,
new InternalIterableProcessWindowFunction<>(new ReduceApplyProcessWindowFunction<>(reduceFunction, function)),
trigger,
evictor,
allowedLateness,
lateDataOutputTag);
} else {
ReducingStateDescriptor stateDesc = new ReducingStateDescriptor<>(“window-contents”,
reduceFunction,
input.getType().createSerializer(getExecutionEnvironment().getConfig()));
opName = “TriggerWindow(” + windowAssigner + ", " + stateDesc + ", " + trigger + ", " + udfName + “)”;
operator =
new WindowOperator<>(windowAssigner,
windowAssigner.getWindowSerializer(getExecutionEnvironment().getConfig()),
keySel,
input.getKeyType().createSerializer(getExecutionEnvironment().getConfig()),
stateDesc,
new InternalSingleValueProcessWindowFunction<>(function),
trigger,
allowedLateness,
lateDataOutputTag);
}
return input.transform(opName, resultType, operator);
}
通过对reduce重载方法的逐步调用,会走到上述代码的else逻辑中,这里也是先构建了StreamOperator的具体子类实例。
public SingleOutputStreamOperator transform(String operatorName,
TypeInformation outTypeInfo, OneInputStreamOperator<T, R> operator) {
SingleOutputStreamOperator returnStream = super.transform(operatorName, outTypeInfo, operator);
OneInputTransformation<T, R> transform = (OneInputTransformation<T, R>) returnStream.getTransformation();
transform.setStateKeySelector(keySelector);
transform.setStateKeyType(keyType);
return returnStream;
}
父类的transform中的逻辑如下:
public SingleOutputStreamOperator transform(String operatorName, TypeInformation outTypeInfo, OneInputStreamOperator<T, R> operator) {
/** 读取输入转换的输出类型, 如果是MissingTypeInfo, 则及时抛出异常, 终止操作 */
transformation.getOutputType();
OneInputTransformation<T, R> resultTransform = new OneInputTransformation<>(
this.transformation,
operatorName,
operator,
outTypeInfo,
environment.getParallelism());
@SuppressWarnings({ “unchecked”, “rawtypes” })
SingleOutputStreamOperator returnStream = new SingleOutputStreamOperator(environment, resultTransform);
getExecutionEnvironment().addOperator(resultTransform);
return returnStream;
}
逻辑与flatMap相似,也是基于StreamOperator构建了一个StreamTransformation的子类OneInputTransformation的实例,然后构建了DataStream的子类SingleOutputStreamOperator的实例,最后也将构建的StreamTransformation的子类实例添加到了StreamExecutionEnvironment的属性transformations这个列表中。
经过上述操作,对数据流中的数据进行分组聚合的操作就完成了。
### 五、输出统计结果
统计结果的输出如下:
windowCounts.print();
print方法就是在数据流的最后添加了一个Sink,用于承接统计结果。
public DataStreamSink print() {
PrintSinkFunction printFunction = new PrintSinkFunction<>();
return addSink(printFunction);
}
其中PrintSinkFunction的类继承图如下所示:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200712141001241.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JlaWlzQmVp,size_1,color_FFFFFF,t_70)
作为一个SinkFunction接口的实现,看下其对invoke方法的实现:
public void invoke(IN record) {
if (prefix != null) {
stream.println(prefix + record.toString());
}
else {
stream.println(record.toString());
}
}
实现逻辑很清晰,就是将记录输出打印。继续看addSink方法:
public DataStreamSink addSink(SinkFunction sinkFunction) {
transformation.getOutputType();
if (sinkFunction instanceof InputTypeConfigurable) {
((InputTypeConfigurable) sinkFunction).setInputType(getType(), getExecutionConfig());
}
StreamSink sinkOperator = new StreamSink<>(clean(sinkFunction));
DataStreamSink sink = new DataStreamSink<>(this, sinkOperator);
getExecutionEnvironment().addOperator(sink.getTransformation());
return sink;
}
实现逻辑与数据源是相似的,先构建StreamOperator,再构建DataStreamSink,在DataStreamSink的构建中,会构造出StreamTransformation实例,最后会将这个StreamTransformation实例添加到StreamExecutionEnvironment的属性transformations这个列表中。
经过上述步骤,就完成了数据流的源构造、数据流的转换操作、数据流的Sink构造,在这个过程中,每次转换都会产生一个新的数据流,而每个数据流下几乎都有一个StreamTransformation的子类实例,对于像flatMap、reduce这些转换得到的数据流里的StreamTransformation会被添加到StreamExecutionEnvironment的属性transformations这个列表中,这个属性在后续构建StreamGraph时会使用到。
另外在这个数据流的构建与转换过程中,每个DataStream中的StreamTransformation的具体子类中都有一个input属性,该属性会记录该StreamTransformation的上游的DataStream的StreamTransformation引用,从而使得整个数据流中的StreamTransformation构成了一个隐式的链表,由于一个数据流可能会转换成多个输出数据流,以及多个输入数据流又有可能会合并成一个输出数据流,确定的说,不是隐式列表,而是一张隐式的图。
述数据转换完成后,就会进行任务的执行,就是执行如下代码:
env.execute(“Socket Window WordCount”);
这里就会根据上述的转换过程,先生成StreamGraph,再根据StreamGraph生成JobGraph,然后通过客户端提交到集群进行调度执行。
![img](https://img-blog.csdnimg.cn/img_convert/511ae529bab63e6d3bea42d29a01d830.png)
![img](https://img-blog.csdnimg.cn/img_convert/bbdc035dec8b5a0ffa2e7fd65fde23ad.png)
![img](https://img-blog.csdnimg.cn/img_convert/9ab25742620024b5835e09ed24f56d48.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
可能会转换成多个输出数据流,以及多个输入数据流又有可能会合并成一个输出数据流,确定的说,不是隐式列表,而是一张隐式的图。
述数据转换完成后,就会进行任务的执行,就是执行如下代码:
env.execute(“Socket Window WordCount”);
这里就会根据上述的转换过程,先生成StreamGraph,再根据StreamGraph生成JobGraph,然后通过客户端提交到集群进行调度执行。
[外链图片转存中...(img-hSApCZCM-1714771145286)]
[外链图片转存中...(img-jCZbxxaV-1714771145286)]
[外链图片转存中...(img-k4zNv1QA-1714771145287)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**