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事件时间处理的核心,理解其原理对于开发高质量的实时应用至关重要!

相关推荐
徐同保6 小时前
tailwindcss使用@apply指令定义自己的样式
1024程序员节
让我们一起加油好吗6 小时前
【数论】欧拉定理 && 扩展欧拉定理
c++·算法·数论·1024程序员节·欧拉定理·欧拉降幂·扩展欧拉定理
SEO-狼术6 小时前
How Users Interact with PDFs
1024程序员节
Yupureki6 小时前
从零开始的C++学习生活 14:map/set的使用和封装
c语言·数据结构·c++·学习·visual studio·1024程序员节
盼小辉丶6 小时前
视觉Transformer实战 | Transformer详解与实现
pytorch·深度学习·transformer·1024程序员节
你的电影很有趣6 小时前
lesson77:Vue组件开发指南:从基础使用到高级通信
javascript·vue.js·1024程序员节
zhangzhangkeji6 小时前
UE5 蓝图-12:pawn蓝图,轴映射-鼠标右键,补充轴映射与操作映射的区别。相机的旋转俯仰逻辑,伸缩逻辑,浮点差值函数 FInterpTo;
ue5·1024程序员节
keineahnung23456 小时前
C++中的Aggregate initialization
c++·1024程序员节
zhangyifang_0097 小时前
【流程引擎】与【规则引擎】
1024程序员节