万字细说Flink中Window原理与使用

一、时间语义

学习flink的Window窗口,需要先了解下flink的三种时间语义。不同的时间语义对应不同的窗口函数。

1.Flink中的时间语义

  • 在Flink的流式处理中,会涉及到时间的不同概念,如下图所示:
    在这里插入图片描述

  • 1. Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的日志数据中,每一条日志都会记录自己的生成时间,Flink通过时间戳分配器访问事件时间戳。

  • 2. Ingestion Time:是数据进入Flink的时间。

  • 3. Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是Processing Time。

2. 设置时间语义

  • 在flink的流式处理中,绝大部分的业务都会使用eventTime,一般只在eventTime无法使用时,才会被迫使用ProcessingTime或者IngestionTime。

  • 如果要使用EventTime,那么需要引入EventTime的时间属性,引入方式如下所示:

    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment
    // 从调用时刻开始给env创建的每一个stream追加时间特征
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    

    TimeCharacteristic源码:

    public enum TimeCharacteristic {
    
    	/**
    	 * Processing time for operators means that the operator uses the system clock of the machine
    	 * to determine the current time of the data stream. Processing-time windows trigger based
    	 * on wall-clock time and include whatever elements happen to have arrived at the operator at
    	 * that point in time.
    	 */
    	ProcessingTime,
    
    	/**
    	 * Ingestion time means that the time of each individual element in the stream is determined
    	 * when the element enters the Flink streaming data flow. Operations like windows group the
    	 * elements based on that time, meaning that processing speed within the streaming dataflow
    	 * does not affect windowing, but only the speed at which sources receive elements.
    	
    	 */
    	IngestionTime,
    
    	/**
    	 * Event time means that the time of each individual element in the stream (also called event)
    	 * is determined by the event's individual custom timestamp. These timestamps either exist in
    	 * the elements from before they entered the Flink streaming dataflow, or are user-assigned at
    	 * the sources. The big implication of this is that it allows for elements to arrive in the
    	 * sources and in all operators out of order, meaning that elements with earlier timestamps may
    	 * arrive after elements with later timestamps.
    	 */
    	EventTime
    }
    

二、Window窗口

1.Window概述

  • streaming流式计算是一种被设计用于处理无限数据集的数据处理引擎,而无限数据集是指一种不断增长的本质上无限的数据集,而window是一种切割无限数据为有限块进行处理的手段。

  • Window是无限数据流处理的核心,Window将一个无限的stream拆分成有限大小的"buckets"桶,我们可以在这些桶上做计算操作。

2.Window简介

Window可以分成两类:

  • CountWindow(计数窗口):按照指定的数据条数生成一个Window,与时间无关。

  • TimeWindow(时间窗口):按照时间生成Window。

    • 对于TimeWindow,可以根据窗口实现原理的不同分成三类:滚动窗口(Tumbling Window)滑动窗口(Sliding Window)会话窗口(Session Window)

2.1 滚动窗口(Tumbling Windows)

  • 将数据依据固定的窗口长度对数据进行切片。

  • 特点:时间对齐,窗口长度固定,没有重叠。

  • 滚动窗口分配器将每个元素分配到一个指定窗口大小的窗口中,滚动窗口有一个固定的大小,并且不会出现重叠。例如:如果你指定了一个5分钟大小的滚动窗口,窗口的创建如下图所示

在这里插入图片描述

  • 适用场景:适合做BI统计等(做每个时间段的聚合计算)。

2.2 滑动窗口(Sliding Windows)

  • 滑动窗口是固定窗口的更广义的一种形式,滑动窗口由固定的窗口长度和滑动间隔组成。

  • 特点:时间对齐,窗口长度固定,可以有重叠。

  • 滑动窗口分配器将元素分配到固定长度的窗口中,与滚动窗口类似,窗口的大小由窗口大小参数来配置,另一个窗口滑动参数控制滑动窗口开始的频率。因此,滑动窗口如果滑动参数小于窗口大小的话,窗口是可以重叠的,在这种情况下元素会被分配到多个窗口中。

  • 例如,你有10分钟的窗口和5分钟的滑动,那么每个窗口中5分钟的窗口里包含着上个10分钟产生的数据,如下图所示:
    在这里插入图片描述

  • 适用场景:对最近一个时间段内的统计(求某接口最近5min的失败率来决定是否要报警)。

2.3 会话窗口(Session Windows)

  • 由一系列事件组合一个指定时间长度的timeout间隙组成,类似于web应用的session,也就是一段时间没有接收到新数据就会生成新的窗口。

  • 特点:时间无对齐。

  • session窗口分配器通过session活动来对元素进行分组,session窗口跟滚动窗口和滑动窗口相比,不会有重叠和固定的开始时间和结束时间的情况,相反,当它在一个固定的时间周期内不再收到元素,即非活动间隔产生,那个这个窗口就会关闭。一个session窗口通过一个session间隔来配置,这个session间隔定义了非活跃周期的长度,当这个非活跃周期产生,那么当前的session将关闭并且后续的元素将被分配到新的session窗口中去。

在这里插入图片描述

3.Window API

3.1 TimeWindow

  • TimeWindow是将指定时间范围内的所有数据组成一个window,一次对一个window里面的所有数据进行计算。
  • flink有两种开窗的方式:
    • 1.直接调用timeWindow。传入一个参数就是滚动窗口,传入两个参数就是滑动窗口。timeWindow源码如下:

      如果传入一个参数(窗口大小),就是滚动窗口,本质是根据时间语义分别调用了TumblingProcessingTimeWindowsTumblingEventTimeWindows

      /**
      * Windows this {@code KeyedStream} into tumbling time windows.
      *
      * <p>This is a shortcut for either {@code .window(TumblingEventTimeWindows.of(size))} or
      * {@code .window(TumblingProcessingTimeWindows.of(size))} depending on the time characteristic
      * set using
      * {@link org.apache.flink.streaming.api.environment.StreamExecutionEnvironment#setStreamTimeCharacteristic(org.apache.flink.streaming.api.TimeCharacteristic)}
      *
      * @param size The size of the window.
      */
      public WindowedStream<T, KEY, TimeWindow> timeWindow(Time size) {
      	if (environment.getStreamTimeCharacteristic() == TimeCharacteristic.ProcessingTime) {
      	return window(TumblingProcessingTimeWindows.of(size));
      	} else {
      	return window(TumblingEventTimeWindows.of(size));
      	}
      }
      

      如果传入两个参数(窗口大小,和滑动步长),就是滑动窗口,也是根据时间语义实现了SlidingProcessingTimeWindowsSlidingEventTimeWindows

      /**
       * Windows this {@code KeyedStream} into sliding time windows.
       *
       * <p>This is a shortcut for either {@code .window(SlidingEventTimeWindows.of(size, slide))} or
       * {@code .window(SlidingProcessingTimeWindows.of(size, slide))} depending on the time
       * characteristic set using
       * {@link org.apache.flink.streaming.api.environment.StreamExecutionEnvironment#setStreamTimeCharacteristic(org.apache.flink.streaming.api.TimeCharacteristic)}
       *
       * @param size The size of the window.
       */
      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));
      	}
      }
      
    • 2 调用window方法,传入一个WindowAssigner窗口分配器

      	/**
      	 * Windows this data stream to a {@code WindowedStream}, which evaluates windows
      	 * over a key grouped stream. Elements are put into windows by a {@link WindowAssigner}. The
      	 * grouping of elements is done both by key and by window.
      	 
      	 * @param assigner The {@code WindowAssigner} that assigns elements to windows.
      	 * @return The trigger windows data stream.
      	 */
      	@PublicEvolving
      	public <W extends Window> WindowedStream<T, KEY, W> window(WindowAssigner<? super T, W> assigner) {
      		return new WindowedStream<>(this, assigner);
      	}
      

      flink实现了如下的WindowAssigner窗口分配器
      在这里插入图片描述
      第一种方式timeWindow()实现了两种最常见的滚动窗口和滑动窗口,使用起来更加的方便。第二种方式window()需要自己传入对应的窗口分配器,但是可以有更多的开窗选择。同时可以传入参数去控制offsetoffset可以应用在数据夸时区的场景。

      TumblingProcessingTimeWindows为例。 传入一个参数即窗口大小,第二个参数即offset延迟开窗的时间

      	/**
      	 * Creates a new {@code TumblingProcessingTimeWindows} {@link WindowAssigner} that assigns
      	 * elements to time windows based on the element timestamp.
      	 *
      	 * @param size The size of the generated windows.
      	 * @return The time policy.
      	 */
      	public static TumblingProcessingTimeWindows of(Time size) {
      		return new TumblingProcessingTimeWindows(size.toMilliseconds(), 0);
      	}
      
      /**
       * Creates a new {@code TumblingProcessingTimeWindows} {@link WindowAssigner} that assigns
       * elements to time windows based on the element timestamp and offset.
       *
       * <p>For example, if you want window a stream by hour,but window begins at the 15th minutes
       * of each hour, you can use {@code of(Time.hours(1),Time.minutes(15))},then you will get
       * time windows start at 0:15:00,1:15:00,2:15:00,etc.
       *
       * <p>Rather than that,if you are living in somewhere which is not using UTC±00:00 time,
       * such as China which is using UTC+08:00,and you want a time window with size of one day,
       * and window begins at every 00:00:00 of local time,you may use {@code of(Time.days(1),Time.hours(-8))}.
       * The parameter of offset is {@code Time.hours(-8))} since UTC+08:00 is 8 hours earlier than UTC time.
       *
       * @param size The size of the generated windows.
       * @param offset The offset which window start would be shifted by.
       * @return The time policy.
       */
      public static TumblingProcessingTimeWindows of(Time size, Time offset) {
      	return new TumblingProcessingTimeWindows(size.toMilliseconds(), offset.toMilliseconds());
      }
      

(1)滚动窗口

  • Flink默认的时间窗口根据Processing Time 进行窗口的划分,将Flink获取到的数据根据进入Flink的时间划分到不同的窗口中。

    DataStream<Tuple2<String, Double>> minTempPerWindowStream = dataStream
       .map(new MapFunction<SensorReading, Tuple2<String, Double>>() {
           @Override
           public Tuple2<String, Double> map(SensorReading value) throws Exception {
               return new Tuple2<>(value.getId(), value.getTemperature());
           }
       })
       .keyBy(data -> data.f0) 
       .timeWindow( Time.seconds(15) )
       //调用window(),传入一个WindowAssigner,实现开滚动窗口
       //.window(TumblingProcessingTimeWindows.of(Time.seconds(15)))
       //设置offset开窗延迟大小
       //.window(TumblingProcessingTimeWindows.of(Time.seconds(15), Time.seconds(0)))
       .minBy(1);
    
  • 时间间隔可以通过Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等其中的一个来指定。

(2)滑动窗口

  • 滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size窗口大小,一个是sliding_size滑动步长,如果需要还可以传入三个参数offset_size开窗延迟大小。

  • 下面代码中的sliding_size设置为了5s,也就是说,每5s就计算输出结果一次,每一次计算的window范围是15s内的所有元素。

    DataStream<SensorReading> minTempPerWindowStream = dataStream
        .keyBy(SensorReading::getId) 
        .timeWindow( Time.seconds(15), Time.seconds(5))
        //调用window(),传入一个WindowAssigner,实现开滑动窗口
        //.window(SlidingProcessingTimeWindows.of(Time.seconds(15),Time.seconds(5))) 
        //设置offset开窗延迟大小	
        //.window(SlidingProcessingTimeWindows.of(Time.seconds(15), Time.seconds(5), Time.seconds(0)))
        .minBy("temperature");
    
  • 时间间隔可以通过Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等其中的一个来指定。

(3)会话窗口

  • 会话窗口,采用会话持续时长作为窗口处理依据。设置指定的会话持续时长时间,在这段时间中不再出现会话则认为超出会话时长。

  • 下面代码中的会话时间设置为了10分钟,也就是说,每10分钟就计算输出结果一次

    DataStream<SensorReading> minTempPerWindowStream = dataStream
        .keyBy(SensorReading::getId) 
    	.window(ProcessingTimeSessionWindows.withDynamicGap(Time.minutes(10))
        .minBy("temperature");
    

3.3 CountWindow

  • CountWindow根据窗口中相同key元素的数量来触发执行,执行时只计算元素数量达到窗口大小的key对应的结果。

  • 注意:CountWindow的window_size指的是相同Key的元素的个数,不是输入的所有元素的总数。

(1)计数滚动窗口

  • 默认的CountWindow是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行。

    DataStream<SensorReading> minTempPerWindowStream = dataStream
    	.keyBy(SensorReading::getId) 
    	.countWindow( 5 )
    	.minBy("temperature");
    

(2) 计数滑动窗口

  • 滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是window_size,一个是sliding_size

  • 下面代码中的sliding_size设置为了2,也就是说,每收到两个相同key的数据就计算一次,每一次计算的window范围是10个元素。

    DataStream<SensorReading> minTempPerWindowStream = dataStream
    	.keyBy(SensorReading::getId) 
    	.countWindow( 10, 2 )
    	.minBy("temperature");
    

3.4 Time Window Assigner

flink实现了多个WindowAssigner,我们以TumblingEventTimeWindows为例讨论。

  • TumblingProcessingTimeWindows构造方法初始化size和offset两个属性。

    //TumblingProcessingTimeWindows构造器
    private TumblingProcessingTimeWindows(long size, long offset) {
    	if (Math.abs(offset) >= size) {
    		throw new IllegalArgumentException("TumblingProcessingTimeWindows parameters must satisfy abs(offset) < size");
    	}
    
    	this.size = size;
    	this.offset = offset;
    }
    
  • assignWindows:重写父类WindowAssignerassignWindows方法用来给数据分配窗口

    @Override
    public Collection<TimeWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
    	//1.首先获得系统当前时间戳
    	final long now = context.getCurrentProcessingTime();
    	//2.计算窗口的起始时间
    	long start = TimeWindow.getWindowStartWithOffset(now, offset, size);
    	//3.根据起始时间和结束时间创建一个新的窗口new TimeWindow(start, start + size)并返回。
    	return Collections.singletonList(new TimeWindow(start, start + size));
    }
    

    其中第二步计算窗口起始时间的计算逻辑getWindowStartWithOffset如下:

    public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
    //timestamp是数据中时间 offeset是窗口延迟大小   windowSize是窗口大小
    return timestamp - (timestamp - offset + windowSize) % windowSize;
    }
    

    offset默认为0的情况下, 我们假设 数据进入flink 的时间为 2021-01-01 20:00:08,则timestamp = 1609502408(s) = 1609502408000(ms)
    窗口大小为 windowSize = 10s = 10000ms 。在计算的时候要统一单位,这里我们统一使用秒即可,对结果无影响。

    1. timestamp - offset + windowSiz = 1609502408000 - 0 + 10000 = 1609502418000
    2. (timestamp - offset + windowSize) % windowSize = 1609502418000 % 10000 = 8000
    3. timestamp - (timestamp - offset + windowSize) % windowSize  = 1609502408000 - 8000 = 1609502400000
    

    对应的起始区间为1609502400000 => 2021-01-01 20:00:00
    所以第一条数据在2021-01-01 20:00:08时刻被flink处理,对应的起始窗口为:[2021-01-01 20:00:00, 2021-01-01 20:10:00)左闭右开。以后每10s生成一个窗口,数据根据时间落入对应的窗口内。

    举几个数据例子
    1.offset = 0ms 窗口大小windowSize = 10000ms,第一条数据:1609502419900 => 2021-01-01 20:00:19
    则起始窗口为[2021-01-01 20:00:10, 2021-01-01 20:00:20)
    2.offset = 0ms 窗口大小windowSize = 5000ms,第一条数据:1609502419900 => 2021-01-01 20:00:19
    则起始窗口为[2021-01-01 20:00:15, 2021-01-01 20:00:20)
    3.offset = 0ms 窗口大小windowSize = 3000ms,第一条数据:1609502419900 => 2021-01-01 20:00:19
    则起始窗口为[2021-01-01 20:00:18, 2021-01-01 20:00:21)
    4.offset = 0ms 窗口大小windowSize = 5000ms,第一条数据:11547718199000 =>2019-01-17 17:43:19
    则起始窗口为[2019-01-17 17:43:15, 2019-01-17 17:43:20)

    通过上述几个例子 我们可以更加方便的理解,起始窗口位置的计算逻辑timestamp - (timestamp - offset + windowSize) % windowSize: 可以简单的理解为,窗口的起始时间 一定是windowSize的倍数,而且一定是离第一条数据的timestamp最近的一个时间。

三、Window Function 窗口函数

window function 定义了要对窗口中收集的数据做的计算操作,flink开窗之后,需要对窗口做计算,这时就要用到窗口函数,主要可以分为两类:

  • 1.增量聚合函数(incremental aggregation functions)

每条数据到来就进行计算,保持一个简单的状态。典型的增量聚合函数有ReduceFunction, AggregateFunction。

  • 2.全窗口函数(full window functions)

先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。ProcessWindowFunction就是一个全窗口函数。

TODO

https://ci.apache.org/projects/flink/flink-docs-release-1.13/docs/dev/datastream/operators/windows/

四、其它可选API

  • trigger()—— 触发器

    • 定义 window 什么时候关闭,触发计算并输出结果。 窗口的计算触发依赖于窗口触发器,每种类型的窗口都有对应的窗口触发机制,都有一个默认的窗口触发器,触发器的作用就是去控制什么时候来触发我们的聚合方法。
    • EventTimeTrigger:通过对比Watermark和窗口的Endtime确定是否触发窗口计算,如果Watermark大于Window - EndTime则触发,否则不触发,窗口将继续等待。
    • ProcessTimeTrigger:通过对比ProcessTime和窗口EndTme确定是否触发窗口,如果ProcessTime大于EndTime则触发计算,否则窗口继续等待。
    • ContinuousEventTimeTrigger:根据间隔时间周期性触发窗口或者Window的结束时间小于当前EndTime触发窗口计算。
    • ContinuousProcessingTimeTrigger:根据间隔时间周期性触发窗口或者Window的结束时间小于当前ProcessTime触发窗口计算。
    • CountTrigger:根据接入数据量是否超过设定的阙值判断是否触发窗口计算。
    • DeltaTrigger:根据接入数据计算出来的Delta指标是否超过指定的Threshold去判断是否触发窗口计算。
    • PurgingTrigger:可以将任意触发器作为参数转换为Purge类型的触发器,计算完成后数据将被清理。
  • evitor()—— 移除器

    定义移除某些数据的逻辑

  • allowedLateness()—— 允许处理迟到的数据

  • sideOutputLateData()—— 将迟到的数据放入侧输出流

  • getSideOutput()—— 获取侧输出流

  • windows操作汇总

Keyed Windows

stream
       .keyBy(...)               <-  keyed versus non-keyed windows
       .window(...)              <-  required: "assigner"
      [.trigger(...)]            <-  optional: "trigger" (else default trigger)
      [.evictor(...)]            <-  optional: "evictor" (else no evictor)
      [.allowedLateness(...)]    <-  optional: "lateness" (else zero)
      [.sideOutputLateData(...)] <-  optional: "output tag" (else no side output for late data)
       .reduce/aggregate/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"

Non-Keyed Windows

stream
       .windowAll(...)           <-  required: "assigner"
      [.trigger(...)]            <-  optional: "trigger" (else default trigger)
      [.evictor(...)]            <-  optional: "evictor" (else no evictor)
      [.allowedLateness(...)]    <-  optional: "lateness" (else zero)
      [.sideOutputLateData(...)] <-  optional: "output tag" (else no side output for late data)
       .reduce/aggregate/apply()      <-  required: "function"
      [.getSideOutput(...)]      <-  optional: "output tag"

参考链接:

相关推荐
<p style="color:#333333;"> 大数据发展史:<br /> <img src="https://10.url.cn/qqke_course_info/ajNVdqHZLLAicLicp7c0pgCrMWa38xMj5YF2TuawK4VBCIdAxbILlmVpDel0nsSEiadHjZ4gTOWWHI/" alt="" /> </p> <p style="color:#333333;"> Flink和storm sparkstreaming对比<br /> <img src="https://10.url.cn/qqke_course_info/ajNVdqHZLLAG1PPfA0fFu9z8BssmHc1Via0kl4hxUDXlSLs6PH1bjmcRgOhkIZsdsSYVmbGIssYY/" alt="" /><br /> <img src="https://10.url.cn/qqke_course_info/ajNVdqHZLLBCwdvdAam64sN1X04EFKLHW4HTGl1c3lvZUtSpK495ZCQcckNcz6wdIjmATYPjSh0/" alt="" /> </p> <p style="color:#333333;"> <strong>实时框架如何选择</strong><br /> 1:需要关注流数据是否需要进行状态管理 <br /> 2:At-least-once或者Exectly-once消息投递模式是否有特殊要求 <br /> 3:对于小型独立的项目,并且需要低延迟的场景,建议使用storm <br /> 4:如果你的项目已经使用了spark,并且秒级别的实时处理可以满足需求的话,建议使用sparkStreaming<br /> 5:要求消息投递语义为 Exactly Once 的场景;数据量较大,要求高吞吐低延迟的场景;需要进行状态管理或窗口统计的场景,建议使用flink </p> <p style="color:#333333;"> 针对以上知识我们通过flink读取kafka保存到redis方式快速让大家学习flink如何使用,以及我们如果搭建高性能的flink应用,这个课程属于快速实战篇。 </p> <p style="color:#333333;"> Flink + kafka + redis 实时计算 </p> <p style="color:#333333;"> <br /> </p>
<span style="color:#404040;">如今的大数据技术应用场景,对实时性的要求已经越来越高。作为新一代大数据流处理框架,由于非常好的实时性,Flink独树一帜,在近些年引起了业内极大的兴趣和关注。Flink能够提供毫秒级别的延迟,同时保证了数据处理的低延迟、高吞吐和结果的正确性,还提供了丰富的时间类型和窗口计算、Exactly-once 语义支持,另外还可以进行状态管理,并提供了CEP(复杂事件处理)的支持。Flink在实时分析领域的优势,使得越来越多的公司开始将实时项目向Flink迁移,其社区也在快速发展壮大。</span><br /> <br /> <span style="color:#404040;">目前,Flink已经成为各大公司实时领域的发力重点,特别是国内以阿里为代表的一众大厂,都在全力投入,不少公司为Flink社区贡献了大量源码。如今Flink已被很多人认为是大数据实时处理的方向和未来,很多公司也都在招聘和储备了解掌握Flink的人才。</span><br /> <br /> <span style="color:#404040;">本教程将Flink理论与电商数据分析项目实战并重,对Flink基础理论知识做了系统的梳理和阐述,并通过电商用户行为分析的具体项目用多个指标进行了实战演练。为有志于增加大数据项目经验、扩展流式处理框架知识的工程师提供了学习方式。</span><br /> <br /> <span style="color:#404040;">二、教程内容和目标</span><br /> <span style="color:#404040;">本教程主要分为两部分:</span><br /> <span style="color:#404040;">第一部分,主要是Flink基础理论的讲解,涉及到各种重要概念、原理和API的用法,并且会有大量的示例代码实现;</span><br /> <span style="color:#404040;">第二部分,以电商作为业务应用场景,以Flink作为分析框架,介绍一个电商用户行为分析项目的开发实战。</span><br /> <span style="color:#404040;">通过理论和实际的紧密结合,可以使学员对Flink有充分的认识和理解,在项目实战Flink和流式处理应用的场景、以及电商分析业务领域有更深刻的认识;并且通过对流处理原理的学习和与批处理架构的对比,可以对大数据处理架构有更全面的了解,为日后成长为架构师打下基础。</span><br /> <br /> <span style="color:#404040;">三、谁适合学</span><br /> <span style="color:#404040;">1、有一定的 Java、Scala 基础,希望了解新的大数据方向的编程人员</span><br /> <span style="color:#404040;">2、有 Java、Scala 开发经验,了解大数据相关知识,希望增加项目经验的开发人员</span><br /> <span style="color:#404040;">3、有较好的大数据基础,希望掌握Flink及流式处理框架的求职人员</span>
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页