Flink DatastreamAPI详解(三)
·
Sliding Windows
1. 事件时间滑动窗口
input
.keyBy(<key selector>)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
// ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
// 窗口大小(size) 滑动步长(slide)
.<windowed transformation>(<window function>);
2. 处理时间滑动窗口
input
.keyBy(<key selector>)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.<windowed transformation>(<window function>);
3. 带偏移量的处理时间滑动窗口
input
.keyBy(<key selector>)
.window(SlidingProcessingTimeWindows.of(
Time.hours(12), // 窗口大小:12小时
Time.hours(1), // 滑动步长:1小时
Time.hours(-8) // 偏移量:-8小时(时区调整)
))
.<windowed transformation>(<window function>);
核心概念:滑动窗口(Sliding Window)
关键参数
SlidingEventTimeWindows.of(size, slide)
// ^^^^ ^^^^^
// 窗口大小 滑动步长
- size(窗口大小):每个窗口的时间跨度
- slide(滑动步长):窗口滑动的间隔
滑动窗口 vs 滚动窗口
| 特性 | 滚动窗口 | 滑动窗口 |
|---|---|---|
| 重叠 | ❌ 不重叠 | ✅ 可重叠 |
| 数据归属 | 每条数据属于1个窗口 | 每条数据可属于多个窗口 |
| 窗口间隔 | 窗口大小 = 滑动步长 | 窗口大小 > 滑动步长(通常) |
| 使用场景 | 独立统计 | 平滑趋势、移动平均 |
窗口划分示意图
情况1:窗口大小 = 10秒,滑动步长 = 5秒(窗口重叠)
时间轴:
0 5 10 15 20 25 30
|----|----|----|----|----|----|
窗口1: [0, 10) |==========|
窗口2: [5, 15) |==========|
窗口3: [10, 20) |==========|
窗口4: [15, 25) |==========|
窗口5: [20, 30) |==========|
特点:
- 窗口重叠50%
- 每5秒产生一个新窗口
- 一条数据可能属于2个窗口
数据分配示例:
时间戳为8秒的数据:
- 属于窗口1 [0, 10) ✅
- 属于窗口2 [5, 15) ✅
- 属于窗口3 [10, 20) ❌
时间戳为12秒的数据:
- 属于窗口2 [5, 15) ✅
- 属于窗口3 [10, 20) ✅
- 属于窗口4 [15, 25) ❌
情况2:窗口大小 = 10秒,滑动步长 = 10秒(等同于滚动窗口)
窗口1: [0, 10) |==========|
窗口2: [10, 20) |==========|
窗口3: [20, 30) |==========|
特点:
- 窗口不重叠
- 退化为滚动窗口
情况3:窗口大小 = 10秒,滑动步长 = 2秒(高度重叠)
窗口1: [0, 10) |==========|
窗口2: [2, 12) |==========|
窗口3: [4, 14) |==========|
窗口4: [6, 16) |==========|
窗口5: [8, 18) |==========|
特点:
- 每2秒产生一个新窗口
- 一条数据属于5个窗口(10/2=5)
- 更平滑的趋势分析
详细示例解析
示例1:事件时间滑动窗口(10秒窗口,5秒滑动)
public class SlidingEventTimeWindowExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// 模拟数据:用户点击事件
DataStream<Event> events = env.fromElements(
new Event("user1", 1, 1000L), // 1秒
new Event("user1", 1, 3000L), // 3秒
new Event("user1", 1, 7000L), // 7秒
new Event("user1", 1, 11000L), // 11秒
new Event("user1", 1, 13000L) // 13秒
);
// 分配时间戳和Watermark
DataStream<Event> withTimestamps = events.assignTimestampsAndWatermarks(
WatermarkStrategy.<Event>forMonotonousTimestamps()
.withTimestampAssigner((event, ts) -> event.timestamp)
);
// 滑动窗口:窗口大小10秒,每5秒滑动一次
withTimestamps
.keyBy(event -> event.userId)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.process(new ProcessWindowFunction<Event, String, String, TimeWindow>() {
@Override
public void process(
String userId,
Context ctx,
Iterable<Event> elements,
Collector<String> out
) {
int count = 0;
for (Event e : elements) count++;
out.collect(String.format(
"User: %s, Window: [%d, %d), Count: %d",
userId,
ctx.window().getStart() / 1000,
ctx.window().getEnd() / 1000,
count
));
}
})
.print();
env.execute("Sliding Event-Time Window");
}
static class Event {
String userId;
int clickCount;
Long timestamp;
Event(String userId, int clickCount, Long timestamp) {
this.userId = userId;
this.clickCount = clickCount;
this.timestamp = timestamp;
}
}
}
/* 输出分析:
事件时间戳:1s, 3s, 7s, 11s, 13s
窗口[0, 10): 包含 1s, 3s, 7s → Count: 3
窗口[5, 15): 包含 7s, 11s, 13s → Count: 3
窗口[10, 20): 包含 11s, 13s → Count: 2
注意:
- 7s的数据同时属于窗口[0,10)和窗口[5,15)
- 11s的数据同时属于窗口[5,15)和窗口[10,20)
*/
示例2:处理时间滑动窗口(实时统计)
public class SlidingProcessingTimeWindowExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// 模拟实时数据流
DataStream<Tuple2<String, Integer>> stream = env
.socketTextStream("localhost", 9999)
.map(line -> {
String[] parts = line.split(",");
return new Tuple2<>(parts[0], Integer.parseInt(parts[1]));
});
// 滑动窗口:10秒窗口,每5秒统计一次
stream
.keyBy(tuple -> tuple.f0)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.sum(1)
.print();
/* 应用场景:
实时监控最近10秒的数据,每5秒更新一次统计结果
时间线(处理时间):
00:00:00-00:00:10: 窗口1统计
00:00:05-00:00:15: 窗口2统计(与窗口1重叠50%)
00:00:10-00:00:20: 窗口3统计
优点:更平滑的趋势展示
*/
env.execute("Sliding Processing-Time Window");
}
}
示例3:带偏移量的滑动窗口(跨时区统计)
public class SlidingWindowWithOffset {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// 模拟订单数据
DataStream<Order> orders = env.fromElements(
new Order("product1", 100.0),
new Order("product1", 200.0),
new Order("product2", 300.0)
);
// 滑动窗口:12小时窗口,每1小时滑动,按北京时间(UTC-8)
orders
.keyBy(order -> order.productId)
.window(SlidingProcessingTimeWindows.of(
Time.hours(12), // 窗口大小:12小时
Time.hours(1), // 滑动步长:每小时统计一次
Time.hours(-8) // 偏移量:调整到北京时间
))
.aggregate(new AggregateFunction<Order, Tuple2<Double, Integer>, Double>() {
public Tuple2<Double, Integer> createAccumulator() {
return new Tuple2<>(0.0, 0);
}
public Tuple2<Double, Integer> add(Order order, Tuple2<Double, Integer> acc) {
return new Tuple2<>(acc.f0 + order.amount, acc.f1 + 1);
}
public Double getResult(Tuple2<Double, Integer> acc) {
return acc.f1 == 0 ? 0.0 : acc.f0 / acc.f1;
}
public Tuple2<Double, Integer> merge(Tuple2<Double, Integer> a,
Tuple2<Double, Integer> b) {
return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
}
})
.print();
/* 应用场景:
每小时统计最近12小时的平均订单金额
窗口示例(北京时间):
窗口1: [2025-01-01 00:00, 2025-01-01 12:00)
窗口2: [2025-01-01 01:00, 2025-01-01 13:00)
窗口3: [2025-01-01 02:00, 2025-01-01 14:00)
...
每小时更新一次,展示12小时移动平均
*/
env.execute("Sliding Window with Offset");
}
static class Order {
String productId;
Double amount;
Order(String productId, Double amount) {
this.productId = productId;
this.amount = amount;
}
}
}
常见应用场景
场景1:移动平均值(Moving Average)
// 计算最近10秒的平均温度,每2秒更新一次
temperatures
.keyBy(sensor -> sensor.sensorId)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(2)))
.aggregate(new AggregateFunction<Temperature, Tuple2<Double, Integer>, Double>() {
public Tuple2<Double, Integer> createAccumulator() {
return new Tuple2<>(0.0, 0);
}
public Tuple2<Double, Integer> add(Temperature temp, Tuple2<Double, Integer> acc) {
return new Tuple2<>(acc.f0 + temp.value, acc.f1 + 1);
}
public Double getResult(Tuple2<Double, Integer> acc) {
return acc.f1 == 0 ? 0.0 : acc.f0 / acc.f1; // 平均值
}
public Tuple2<Double, Integer> merge(Tuple2<Double, Integer> a,
Tuple2<Double, Integer> b) {
return new Tuple2<>(a.f0 + b.f0, a.f1 + b.f1);
}
});
/* 优点:
1. 更平滑的趋势展示
2. 减少噪声影响
3. 更频繁的更新
*/
场景2:实时TopN(每分钟统计最近5分钟的热门商品)
// 每分钟更新一次最近5分钟的热门商品Top 10
clicks
.keyBy(click -> click.productId)
.window(SlidingEventTimeWindows.of(Time.minutes(5), Time.minutes(1)))
.aggregate(
// 增量统计点击数
new AggregateFunction<Click, Long, Tuple2<String, Long>>() {
public Long createAccumulator() { return 0L; }
public Long add(Click click, Long acc) { return acc + 1; }
public Tuple2<String, Long> getResult(Long count) {
return new Tuple2<>(getCurrentProductId(), count);
}
public Long merge(Long a, Long b) { return a + b; }
}
)
.windowAll(TumblingProcessingTimeWindows.of(Time.minutes(1)))
.process(new ProcessAllWindowFunction<Tuple2<String, Long>, String, TimeWindow>() {
public void process(Context ctx,
Iterable<Tuple2<String, Long>> elements,
Collector<String> out) {
// 排序并输出Top 10
List<Tuple2<String, Long>> list = new ArrayList<>();
elements.forEach(list::add);
list.sort(Comparator.comparing((Tuple2<String, Long> t) -> t.f1).reversed());
list.stream()
.limit(10)
.forEach(product ->
out.collect("Product: " + product.f0 + ", Clicks: " + product.f1)
);
}
});
场景3:实时告警(异常检测)
// 最近1分钟内错误率超过10%则告警,每10秒检查一次
logs
.keyBy(log -> log.serviceId)
.window(SlidingEventTimeWindows.of(Time.minutes(1), Time.seconds(10)))
.process(new ProcessWindowFunction<Log, Alert, String, TimeWindow>() {
public void process(
String serviceId,
Context ctx,
Iterable<Log> logs,
Collector<Alert> out
) {
int totalCount = 0;
int errorCount = 0;
for (Log log : logs) {
totalCount++;
if (log.level.equals("ERROR")) {
errorCount++;
}
}
double errorRate = totalCount == 0 ? 0 : (double) errorCount / totalCount;
if (errorRate > 0.1) { // 错误率超过10%
out.collect(new Alert(
serviceId,
errorRate,
"High error rate: " + String.format("%.2f%%", errorRate * 100)
));
}
}
});
场景4:流量监控(QPS统计)
// 统计最近10秒的QPS,每秒更新一次
requests
.keyBy(req -> req.apiPath)
.window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(1)))
.aggregate(
new AggregateFunction<Request, Long, Double>() {
public Long createAccumulator() { return 0L; }
public Long add(Request req, Long count) { return count + 1; }
public Double getResult(Long count) { return count / 10.0; } // QPS
public Long merge(Long a, Long b) { return a + b; }
}
)
.map(qps -> "QPS: " + String.format("%.2f", qps))
.print();
/* 效果:
每秒输出最近10秒的平均QPS
更平滑的QPS曲线
*/
滑动步长的选择策略
不同步长的效果对比
// 窗口大小固定为60秒
// 步长60秒(滚动窗口)
.window(SlidingEventTimeWindows.of(Time.seconds(60), Time.seconds(60)))
// 特点:无重叠,每分钟统计一次
// 步长30秒(50%重叠)
.window(SlidingEventTimeWindows.of(Time.seconds(60), Time.seconds(30)))
// 特点:每30秒更新,数据属于2个窗口
// 步长10秒(高频更新)
.window(SlidingEventTimeWindows.of(Time.seconds(60), Time.seconds(10)))
// 特点:每10秒更新,数据属于6个窗口
// 步长1秒(极高频)
.window(SlidingEventTimeWindows.of(Time.seconds(60), Time.seconds(1)))
// 特点:每秒更新,数据属于60个窗口(计算开销大)
选择建议
| 步长选择 | 更新频率 | 计算开销 | 适用场景 |
|---|---|---|---|
| 大步长(接近窗口大小) | 低 | 低 | 离线分析 |
| 中等步长(窗口大小的1/2) | 中 | 中 | 实时报表 |
| 小步长(窗口大小的1/10) | 高 | 高 | 实时监控 |
| 极小步长(秒级) | 极高 | 极高 | 高精度告警 |
性能优化考虑
1. 数据重复计算
// 滑动窗口的计算开销
// 滚动窗口(无重叠)
.window(TumblingEventTimeWindows.of(Time.seconds(10)))
// 每条数据计算1次
// 滑动窗口(50%重叠)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
// 每条数据计算2次(属于2个窗口)
// 滑动窗口(90%重叠)
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(1)))
// 每条数据计算10次(属于10个窗口)
// ⚠️ 计算开销 = 窗口大小 / 滑动步长
2. 状态存储开销
// 使用增量聚合减少状态存储
// ❌ 全量窗口函数:需要缓存所有数据
.window(SlidingEventTimeWindows.of(Time.minutes(10), Time.seconds(10)))
.process(new ProcessWindowFunction<...>() {
// 需要缓存10分钟的数据
// 状态大小:O(窗口内元素数量 × 窗口数量)
});
// ✅ 增量聚合:只保存累积状态
.window(SlidingEventTimeWindows.of(Time.minutes(10), Time.seconds(10)))
.reduce((v1, v2) -> v1 + v2);
// 状态大小:O(窗口数量) - 每个窗口只保存一个累积值
3. 增量聚合 + 全量窗口函数结合
// 最佳实践:既高效又灵活
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.aggregate(
// 增量聚合(内存高效)
new AggregateFunction<Event, Long, Long>() {
public Long createAccumulator() { return 0L; }
public Long add(Event event, Long acc) { return acc + 1; }
public Long getResult(Long acc) { return acc; }
public Long merge(Long a, Long b) { return a + b; }
},
// 全量窗口函数(获取窗口信息)
new ProcessWindowFunction<Long, String, String, TimeWindow>() {
public void process(String key, Context ctx,
Iterable<Long> counts, Collector<String> out) {
Long count = counts.iterator().next();
out.collect(String.format(
"Key: %s, Window: [%d-%d), Count: %d",
key, ctx.window().getStart(), ctx.window().getEnd(), count
));
}
}
);
完整对比总结
| 窗口类型 | 窗口大小 | 滑动步长 | 重叠度 | 数据计算次数 | 适用场景 |
|---|---|---|---|---|---|
| 滚动窗口 | 10s | 10s | 0% | 1次 | 独立统计 |
| 滑动窗口(50%重叠) | 10s | 5s | 50% | 2次 | 移动平均 |
| 滑动窗口(80%重叠) | 10s | 2s | 80% | 5次 | 实时监控 |
| 滑动窗口(90%重叠) | 10s | 1s | 90% | 10次 | 高频告警 |
关键要点总结
- ✅ 滑动窗口特点:窗口可重叠,数据可属于多个窗口
- ✅ 两个参数:窗口大小(size)和滑动步长(slide)
- ✅ 重叠计算:slide < size时窗口重叠,数据被重复计算
- ✅ 事件时间vs处理时间:与滚动窗口相同的区别
- ✅ 偏移量:用于时区调整,实现跨时区统计
- ⚠️ 性能开销:计算次数 = size / slide
- ⚠️ 使用场景:移动平均、实时监控、趋势分析
- ⚠️ 优化建议:使用增量聚合函数减少状态存储
更多推荐
所有评论(0)