Flink Watermark(水位线)机制详解

Watermark是Flink处理事件时间(Event Time)的核心机制,用于处理乱序数据触发窗口计算。让我全面深入地介绍:

一、核心概念

1. 什么是Watermark?

**Watermark(水位线)**是一个时间戳标记,表示:

  • "所有时间戳 ≤ Watermark 的数据都已到达"

  • "时间戳 > Watermark 的数据可能还在路上"

    Watermark(t) 的含义:
    时间戳 ≤ t 的数据已经全部到达(或大部分到达)

2. 为什么需要Watermark?

在分布式流处理中,数据可能会:

  • 乱序到达:网络延迟、多数据源等原因
  • 🐌 延迟到达:某些数据比其他数据晚很多
  • 何时触发计算:不知道数据是否都到齐了

Watermark解决的核心问题

在数据可能乱序的情况下,如何确定"某个时间窗口的数据已经全部到达,可以触发计算了"?

3. Watermark示意图

复制代码
数据流(事件时间):
时间戳: 1  3  2  5  4  8  7  10  9
       |  |  |  |  |  |  |   |  |
       ↓  ↓  ↓  ↓  ↓  ↓  ↓   ↓  ↓
      [==================数据流==================>

Watermark插入(假设允许2秒延迟):
数据: 1  3  2  W(1)  5  4  W(3)  8  7  W(6)  10  9  W(8)
                ↑              ↑          ↑            ↑
            Watermark(1)   Watermark(3) ...

Watermark(6)表示:
- 时间戳 ≤ 6 的数据已经全部到达
- 窗口[0,5)可以触发计算了
- 时间戳为3的延迟数据仍能被处理

二、Watermark的工作原理

1. Watermark与窗口触发

java 复制代码
窗口触发条件:
当 Watermark >= 窗口结束时间 时,窗口触发计算

示例:窗口 [0, 10)
- Watermark = 5  → 窗口不触发(还有数据可能到达)
- Watermark = 9  → 窗口不触发(还有数据可能到达)
- Watermark = 10 → 窗口触发!(时间戳<10的数据已全部到达)

2. 完整流程示例

复制代码
场景:5秒滚动窗口,允许3秒延迟

数据到达顺序(事件时间):
t=1s → 进入窗口[0,5)
t=3s → 进入窗口[0,5)
t=2s → 进入窗口[0,5)(乱序)
t=7s → 进入窗口[5,10),生成Watermark(4)
t=6s → 进入窗口[5,10)(乱序)
t=10s → 进入窗口[10,15),生成Watermark(7)
t=9s → 进入窗口[5,10)(乱序)

Watermark推进过程:
1. 收到t=7s,当前最大时间戳=7
   → Watermark = 7 - 3 = 4
   → 窗口[0,5)不触发(4 < 5)

2. 收到t=10s,当前最大时间戳=10
   → Watermark = 10 - 3 = 7
   → 窗口[5,10)不触发(7 < 10)

3. 收到t=13s,当前最大时间戳=13
   → Watermark = 13 - 3 = 10
   → 窗口[5,10)触发!(10 >= 10)
   → 输出窗口[5,10)的计算结果

三、Watermark生成策略

1. 周期性生成(Periodic Watermarks)

特点:按固定时间间隔生成Watermark

java 复制代码
// 方式1:有界乱序(最常用)
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
    .withTimestampAssigner((event, timestamp) -> event.getTimestamp());

// 原理:
// Watermark = 当前最大事件时间 - 允许的最大乱序时间
// 例如:最大事件时间=10s,允许乱序=3s → Watermark=7s
java 复制代码
// 方式2:单调递增(无乱序)
WatermarkStrategy.<Event>forMonotonousTimestamps()
    .withTimestampAssigner((event, timestamp) -> event.getTimestamp());

// 原理:
// Watermark = 当前最大事件时间
// 适用于数据严格按时间顺序到达的场景
java 复制代码
// 方式3:自定义周期性Watermark
WatermarkStrategy
    .forGenerator((context) -> new WatermarkGenerator<Event>() {
        private long maxTimestamp = Long.MIN_VALUE;
        private final long maxOutOfOrderness = 3000L; // 3秒
        
        @Override
        public void onEvent(Event event, long eventTimestamp, 
                           WatermarkOutput output) {
            // 每条数据到达时更新最大时间戳
            maxTimestamp = Math.max(maxTimestamp, event.getTimestamp());
        }
        
        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            // 周期性生成Watermark(默认200ms一次)
            output.emitWatermark(new Watermark(maxTimestamp - maxOutOfOrderness));
        }
    })
    .withTimestampAssigner((event, timestamp) -> event.getTimestamp());

2. 标点式生成(Punctuated Watermarks)

特点:根据特定数据标记生成Watermark

java 复制代码
// 自定义标点式Watermark
WatermarkStrategy
    .forGenerator((context) -> new WatermarkGenerator<Event>() {
        @Override
        public void onEvent(Event event, long eventTimestamp, 
                           WatermarkOutput output) {
            // 遇到特殊标记数据时生成Watermark
            if (event.hasWatermarkMarker()) {
                output.emitWatermark(new Watermark(event.getTimestamp()));
            }
        }
        
        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            // 标点式不使用周期性生成
        }
    })
    .withTimestampAssigner((event, timestamp) -> event.getTimestamp());

// 应用场景:
// 1. 数据源自带Watermark标记
// 2. Kafka等消息队列的特殊控制消息
// 3. 需要精确控制Watermark生成时机

四、完整代码示例

示例1:基础Watermark使用

java 复制代码
public class BasicWatermarkExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 设置Watermark生成间隔(默认200ms)
        env.getConfig().setAutoWatermarkInterval(1000L); // 1秒
        
        // 模拟乱序数据
        DataStream<Event> events = env.fromElements(
            new Event("sensor1", 1000L, 25.5),   // 1秒
            new Event("sensor1", 3000L, 26.0),   // 3秒
            new Event("sensor1", 2000L, 25.8),   // 2秒(乱序)
            new Event("sensor1", 7000L, 27.0),   // 7秒
            new Event("sensor1", 5000L, 26.5),   // 5秒(乱序)
            new Event("sensor1", 11000L, 28.0)   // 11秒
        );
        
        // 分配时间戳和Watermark(允许3秒乱序)
        DataStream<Event> withWatermarks = events.assignTimestampsAndWatermarks(
            WatermarkStrategy
                .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
                .withTimestampAssigner((event, timestamp) -> event.timestamp)
        );
        
        // 5秒滚动窗口
        withWatermarks
            .keyBy(event -> event.sensorId)
            .window(TumblingEventTimeWindows.of(Time.seconds(5)))
            .process(new ProcessWindowFunction<Event, String, String, TimeWindow>() {
                @Override
                public void process(
                    String sensorId,
                    Context ctx,
                    Iterable<Event> events,
                    Collector<String> out
                ) {
                    double sum = 0;
                    int count = 0;
                    
                    for (Event event : events) {
                        sum += event.temperature;
                        count++;
                    }
                    
                    out.collect(String.format(
                        "Sensor: %s, Window: [%d-%d], Avg Temp: %.2f, Count: %d",
                        sensorId,
                        ctx.window().getStart() / 1000,
                        ctx.window().getEnd() / 1000,
                        sum / count,
                        count
                    ));
                }
            })
            .print();
        
        env.execute("Basic Watermark Example");
    }
    
    static class Event {
        String sensorId;
        Long timestamp;
        Double temperature;
        
        Event(String sensorId, Long timestamp, Double temperature) {
            this.sensorId = sensorId;
            this.timestamp = timestamp;
            this.temperature = temperature;
        }
    }
}

/* 执行过程分析:

数据到达:t=1s, 3s, 2s, 7s, 5s, 11s

Watermark推进:
1. t=1s  → maxTimestamp=1s  → Watermark=-2s(1-3)
2. t=3s  → maxTimestamp=3s  → Watermark=0s(3-3)
3. t=2s  → maxTimestamp=3s  → Watermark=0s(不变)
4. t=7s  → maxTimestamp=7s  → Watermark=4s(7-3)
5. t=5s  → maxTimestamp=7s  → Watermark=4s(不变)
6. t=11s → maxTimestamp=11s → Watermark=8s(11-3)
   → 窗口[0,5)触发!(8>=5)

窗口[0,5)包含的数据:
- t=1s ✅
- t=3s ✅
- t=2s ✅(乱序数据被正确处理)

输出:
Sensor: sensor1, Window: [0-5], Avg Temp: 25.77, Count: 3
*/

示例2:观察Watermark推进过程

java 复制代码
public class WatermarkObserverExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1); // 单并行度便于观察
        
        DataStream<Event> events = env.fromElements(
            new Event(1000L),
            new Event(3000L),
            new Event(2000L),  // 乱序
            new Event(5000L),
            new Event(4000L),  // 乱序
            new Event(8000L),
            new Event(11000L)
        );
        
        // 允许2秒乱序
        events
            .assignTimestampsAndWatermarks(
                WatermarkStrategy
                    .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                    .withTimestampAssigner((event, ts) -> event.timestamp)
            )
            .process(new ProcessFunction<Event, String>() {
                @Override
                public void processElement(
                    Event event,
                    Context ctx,
                    Collector<String> out
                ) {
                    long currentWatermark = ctx.timerService().currentWatermark();
                    
                    out.collect(String.format(
                        "Event: t=%ds, CurrentWatermark: %ds",
                        event.timestamp / 1000,
                        currentWatermark == Long.MIN_VALUE ? 
                            -999 : currentWatermark / 1000
                    ));
                }
            })
            .print();
        
        env.execute("Watermark Observer");
    }
    
    static class Event {
        Long timestamp;
        Event(Long timestamp) { this.timestamp = timestamp; }
    }
}

/* 输出:
Event: t=1s, CurrentWatermark: -999s  (初始值)
Event: t=3s, CurrentWatermark: 1s     (3-2=1)
Event: t=2s, CurrentWatermark: 1s     (maxTs仍是3)
Event: t=5s, CurrentWatermark: 3s     (5-2=3)
Event: t=4s, CurrentWatermark: 3s     (maxTs仍是5)
Event: t=8s, CurrentWatermark: 6s     (8-2=6)
Event: t=11s, CurrentWatermark: 9s    (11-2=9)

观察:
- Watermark单调递增,不会回退
- 乱序数据不影响Watermark(只看最大时间戳)
- Watermark = 最大事件时间 - 允许延迟
*/

示例3:多流Watermark对齐

java 复制代码
public class MultiStreamWatermarkExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 数据源1:快速流(低延迟)
        DataStream<Event> fastStream = env
            .fromElements(
                new Event("fast", 1000L),
                new Event("fast", 2000L),
                new Event("fast", 3000L),
                new Event("fast", 10000L)  // 快速推进到10秒
            )
            .assignTimestampsAndWatermarks(
                WatermarkStrategy.<Event>forMonotonousTimestamps()
                    .withTimestampAssigner((e, ts) -> e.timestamp)
            );
        
        // 数据源2:慢速流(高延迟)
        DataStream<Event> slowStream = env
            .fromElements(
                new Event("slow", 1000L),
                new Event("slow", 2000L),
                new Event("slow", 3000L)  // 只推进到3秒
            )
            .assignTimestampsAndWatermarks(
                WatermarkStrategy.<Event>forMonotonousTimestamps()
                    .withTimestampAssigner((e, ts) -> e.timestamp)
            );
        
        // 合流
        fastStream
            .union(slowStream)
            .keyBy(event -> "key")
            .window(TumblingEventTimeWindows.of(Time.seconds(5)))
            .process(new ProcessWindowFunction<Event, String, String, TimeWindow>() {
                public void process(String key, Context ctx,
                                   Iterable<Event> events, Collector<String> out) {
                    int count = 0;
                    for (Event e : events) count++;
                    
                    out.collect(String.format(
                        "Window [%d-%d]: %d events",
                        ctx.window().getStart() / 1000,
                        ctx.window().getEnd() / 1000,
                        count
                    ));
                }
            })
            .print();
        
        env.execute("Multi-Stream Watermark");
    }
    
    static class Event {
        String source;
        Long timestamp;
        Event(String source, Long timestamp) {
            this.source = source;
            this.timestamp = timestamp;
        }
    }
}

/* Watermark对齐原理:

合流后的Watermark = min(所有上游的Watermark)

fastStream Watermark: 1s → 2s → 3s → 10s
slowStream Watermark: 1s → 2s → 3s

合流后Watermark:     1s → 2s → 3s → 3s(被慢流拖住)

影响:
- 即使fastStream推进到10s,窗口[0,5)仍不触发
- 因为合流后Watermark只有3s < 5s
- slowStream成为瓶颈(数据倾斜问题)
*/

五、延迟数据处理

1. 什么是延迟数据?

复制代码
延迟数据:事件时间 < 当前Watermark 的数据

示例:
当前Watermark = 10s
收到一条t=7s的数据 → 延迟数据(7 < 10)

窗口[5,10)已经在Watermark=10s时触发计算
t=7s的数据到达时窗口已关闭 → 默认被丢弃!

2. 延迟数据处理策略

策略1:设置允许的延迟时间(Allowed Lateness)
java 复制代码
dataStream
    .keyBy(...)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .allowedLateness(Time.seconds(2))  // 允许窗口关闭后2秒内的延迟数据
    .sum(1);

/* 工作原理:

窗口[0,5):
1. Watermark=5s → 窗口首次触发,输出结果1
2. 收到t=3s的延迟数据 → 重新计算,输出结果2(更新)
3. 收到t=4s的延迟数据 → 重新计算,输出结果3(更新)
4. Watermark=7s → 窗口彻底关闭(5+2=7)
5. 之后的延迟数据被丢弃

优点:
- 容忍一定程度的延迟
- 结果更准确

缺点:
- 需要保持窗口状态更长时间
- 可能产生多次输出
*/
策略2:侧输出流(Side Output)收集延迟数据
java 复制代码
// 定义延迟数据标签
OutputTag<Event> lateDataTag = new OutputTag<Event>("late-data"){};

SingleOutputStreamOperator<String> result = dataStream
    .keyBy(...)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .sideOutputLateData(lateDataTag)  // 延迟数据输出到侧输出流
    .sum(1);

// 获取延迟数据
DataStream<Event> lateData = result.getSideOutput(lateDataTag);

// 处理延迟数据
lateData.print("Late Data");  // 可以单独处理或记录日志

/* 优点:
- 不丢失任何数据
- 可以单独分析延迟数据
- 用于监控和告警

应用场景:
- 数据质量监控
- 延迟数据统计
- 后续补偿处理
*/
策略3:组合使用
java 复制代码
OutputTag<Event> lateDataTag = new OutputTag<Event>("late-data"){};

SingleOutputStreamOperator<String> result = dataStream
    .keyBy(event -> event.sensorId)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .allowedLateness(Time.seconds(2))      // 允许2秒延迟
    .sideOutputLateData(lateDataTag)       // 超过2秒的进侧输出
    .aggregate(new MyAggregateFunction());

// 主流:正常和轻度延迟的数据
result.print("Main Output");

// 侧输出流:严重延迟的数据
result.getSideOutput(lateDataTag).print("Severe Late Data");

/* 数据分类:

1. 正常数据:t <= Watermark
   → 正常进入窗口

2. 轻度延迟:Watermark < t < Watermark+AllowedLateness
   → 进入窗口,触发重新计算

3. 严重延迟:t >= Watermark+AllowedLateness
   → 输出到侧输出流

时间线示例(窗口[0,5),允许延迟2秒):
Watermark=3s: t=2s → 正常数据
Watermark=5s: 窗口触发,输出结果
Watermark=6s: t=4s → 轻度延迟,重新计算
Watermark=7s: 窗口彻底关闭
Watermark=8s: t=3s → 严重延迟,进侧输出流
*/

完整示例:延迟数据处理

java 复制代码
public class LateDataHandlingExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        
        // 定义延迟数据标签
        OutputTag<Event> lateDataTag = new OutputTag<Event>("late-data"){};
        
        DataStream<Event> events = env.fromElements(
            new Event("sensor1", 1000L, 25.0),
            new Event("sensor1", 2000L, 26.0),
            new Event("sensor1", 3000L, 27.0),
            new Event("sensor1", 8000L, 28.0),   // 推进Watermark到6s
            new Event("sensor1", 4000L, 26.5),   // 延迟数据1(在允许范围内)
            new Event("sensor1", 10000L, 29.0),  // 推进Watermark到8s
            new Event("sensor1", 2500L, 25.5)    // 延迟数据2(超过允许延迟)
        );
        
        DataStream<Event> withWatermarks = events.assignTimestampsAndWatermarks(
            WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                .withTimestampAssigner((event, ts) -> event.timestamp)
        );
        
        SingleOutputStreamOperator<String> result = withWatermarks
            .keyBy(event -> event.sensorId)
            .window(TumblingEventTimeWindows.of(Time.seconds(5)))
            .allowedLateness(Time.seconds(1))     // 允许1秒延迟
            .sideOutputLateData(lateDataTag)      // 严重延迟数据输出到侧输出
            .process(new ProcessWindowFunction<Event, String, String, TimeWindow>() {
                @Override
                public void process(
                    String key,
                    Context ctx,
                    Iterable<Event> events,
                    Collector<String> out
                ) {
                    double sum = 0;
                    int count = 0;
                    
                    for (Event event : events) {
                        sum += event.temperature;
                        count++;
                    }
                    
                    out.collect(String.format(
                        "[%s] Window [%d-%d]: Avg=%.2f, Count=%d, Watermark=%d",
                        ctx.currentProcessingTime(),
                        ctx.window().getStart() / 1000,
                        ctx.window().getEnd() / 1000,
                        sum / count,
                        count,
                        ctx.currentWatermark() / 1000
                    ));
                }
            });
        
        // 主输出流
        result.print("Main");
        
        // 延迟数据流
        result.getSideOutput(lateDataTag)
            .map(event -> String.format(
                "Late Data: t=%ds, temp=%.1f",
                event.timestamp / 1000,
                event.temperature
            ))
            .print("Late");
        
        env.execute("Late Data Handling");
    }
    
    static class Event {
        String sensorId;
        Long timestamp;
        Double temperature;
        
        Event(String sensorId, Long timestamp, Double temperature) {
            this.sensorId = sensorId;
            this.timestamp = timestamp;
            this.temperature = temperature;
        }
    }
}

/* 输出:

Main> Window [0-5]: Avg=26.00, Count=3, Watermark=6
      ↑ Watermark=6s时触发,包含t=1s,2s,3s

Main> Window [0-5]: Avg=26.13, Count=4, Watermark=6
      ↑ t=4s的延迟数据到达,重新计算(4s在允许延迟内)

Late> Late Data: t=2s, temp=25.5
      ↑ t=2.5s的数据超过允许延迟,进入侧输出流

观察:
1. 窗口首次触发:Watermark=6s (8-2=6 >= 5)
2. t=4s延迟数据触发重算:4s在[5-1, 5+1]范围内
3. t=2.5s严重延迟:Watermark已=8s,窗口在6s彻底关闭
*/

六、Watermark传播机制

1. 单流传播

复制代码
Source → Map → KeyBy → Window
  ↓      ↓      ↓       ↓
  W1  →  W1  →  W1  →   W1

Watermark在算子间传播:
- 每个算子收到Watermark后向下游转发
- 保持单调递增

2. 多流合并

java 复制代码
Stream1 (W1=10s)  ┐
                  ├→ Union → (Watermark = min(10,5) = 5s)
Stream2 (W2=5s)   ┘

规则:合流后的Watermark取所有上游的最小值

原因:保守策略,确保不会漏掉任何数据

3. 分流广播

复制代码
        ┌→ Stream1 (W=10s)
Source ─┤
        └→ Stream2 (W=10s)

规则:所有分支获得相同的Watermark

七、Watermark最佳实践

1. 选择合适的乱序时间

java 复制代码
// ❌ 太小:丢失延迟数据
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(1))
// 1秒延迟可能不够

// ✅ 适中:平衡准确性和延迟
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3))

// ❌ 太大:结果延迟高
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofMinutes(10))
// 10分钟太保守,实时性差

2. 监控Watermark延迟

java 复制代码
dataStream
    .process(new ProcessFunction<Event, Event>() {
        @Override
        public void processElement(Event event, Context ctx, Collector<Event> out) {
            long watermark = ctx.timerService().currentWatermark();
            long eventTime = event.timestamp;
            long lag = eventTime - watermark;  // Watermark延迟
            
            if (lag > 60000) {  // 延迟超过1分钟
                // 记录日志或发送告警
                System.err.println("High watermark lag: " + lag + "ms");
            }
            
            out.collect(event);
        }
    });

3. 处理空闲数据源

java 复制代码
// 问题:某个分区长时间无数据,Watermark不推进

// 解决:设置空闲超时
WatermarkStrategy
    .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
    .withIdleness(Duration.ofMinutes(1))  // 1分钟无数据则视为空闲
    .withTimestampAssigner((event, ts) -> event.timestamp);

/* 效果:
- 分区空闲1分钟后,不再影响全局Watermark
- 其他活跃分区的Watermark可以正常推进
*/

4. Kafka数据源的Watermark

java 复制代码
// Kafka分区独立生成Watermark
FlinkKafkaConsumer<Event> consumer = new FlinkKafkaConsumer<>(...);

DataStream<Event> stream = env
    .addSource(consumer)
    .assignTimestampsAndWatermarks(
        WatermarkStrategy
            .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(3))
            .withIdleness(Duration.ofMinutes(1))  // 重要!处理空闲分区
            .withTimestampAssigner((event, ts) -> event.timestamp)
    );

/* 注意事项:
1. Kafka每个分区独立生成Watermark
2. 全局Watermark = min(所有分区的Watermark)
3. 某个分区空闲会拖慢全局Watermark
4. 必须设置withIdleness处理空闲分区
*/

八、关键要点总结

核心概念

  1. Watermark定义:时间戳标记,表示"≤该时间戳的数据已全部到达"
  2. 触发条件:Watermark >= 窗口结束时间时触发窗口计算
  3. 单调递增:Watermark只能前进,不能后退
  4. 乱序处理:通过设置允许的乱序时间容忍延迟数据

生成策略

  1. 有界乱序:Watermark = 最大事件时间 - 允许延迟(最常用)
  2. 单调递增:Watermark = 最大事件时间(无乱序场景)
  3. 自定义生成:根据业务需求定制Watermark逻辑

延迟数据

  1. ⚠️ allowedLateness:允许窗口关闭后继续接收延迟数据
  2. ⚠️ sideOutputLateData:将严重延迟数据输出到侧输出流
  3. ⚠️ 多次输出:延迟数据可能导致窗口重复计算并输出

多流场景

  1. ⚠️ Watermark对齐:多流合并取最小Watermark
  2. ⚠️ 空闲数据源:使用withIdleness避免空闲分区拖慢Watermark
  3. ⚠️ 数据倾斜:慢速分区会成为Watermark瓶颈

最佳实践

  1. ✅ 根据业务容忍度选择合适的乱序时间
  2. ✅ 监控Watermark延迟,及时发现数据源问题
  3. ✅ 使用侧输出流记录延迟数据,便于分析和告警
  4. ✅ Kafka等多分区数据源必须设置空闲超时

Watermark是Flink事件时间处理的核心,理解其原理对于开发高质量的实时应用至关重要!

相关推荐
u0103055274 天前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u0103055275 天前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝6 天前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u0103055276 天前
Java图像处理实战指南
人工智能·1024程序员节
u0103055277 天前
Java AWT鼠标事件全解析
人工智能·1024程序员节
u0103055279 天前
Java实用类应用精讲
人工智能·1024程序员节
u01030552710 天前
Java I/O核心操作实战
人工智能·1024程序员节
u01030552711 天前
ArrayList操作详解与实战应用
人工智能·1024程序员节
u01030552711 天前
Java模块化编程实战指南
人工智能·笔记·1024程序员节
u01030552712 天前
使用BufferedReader读取控制台输入
人工智能·1024程序员节