Reduce
java
windowedStream.reduce(new ReduceFunction<Tuple2<String,Integer>>() {
public Tuple2<String, Integer> reduce(
Tuple2<String, Integer> value1,
Tuple2<String, Integer> value2
) throws Exception {
return new Tuple2<String,Integer>(value1.f0, value1.f1 + value2.f1);
}
});
1. 数据类型
Tuple2<String, Integer>: 二元组,包含两个字段f0: String类型(通常是key,如用户ID、商品ID等)f1: Integer类型(通常是value,如数量、金额等)
2. ReduceFunction工作原理
reduce函数是一个增量聚合函数,它会:
- 接收同一个窗口内的两条记录
- 将它们合并成一条记录
- 不断重复这个过程,直到窗口内只剩一条记录
3. 执行逻辑
value1.f0 // 保留第一个元素的key
value1.f1 + value2.f1 // 将两个元素的value相加
效果:保持key不变,累加value值
执行示例
假设窗口内有以下数据:
("user1", 10)
("user1", 20)
("user1", 30)
执行过程:
第1次reduce: ("user1", 10) + ("user1", 20) = ("user1", 30)
第2次reduce: ("user1", 30) + ("user1", 30) = ("user1", 60)
最终结果: ("user1", 60)
典型应用场景
java
// 完整示例:统计每个用户5分钟窗口内的点击次数
DataStream<Tuple2<String, Integer>> input = ...;
input
.keyBy(value -> value.f0) // 按用户ID分组
.timeWindow(Time.minutes(5)) // 5分钟滚动窗口
.reduce(new ReduceFunction<Tuple2<String,Integer>>() {
public Tuple2<String, Integer> reduce(
Tuple2<String, Integer> value1,
Tuple2<String, Integer> value2
) throws Exception {
return new Tuple2<>(value1.f0, value1.f1 + value2.f1);
}
});
与其他聚合函数的对比
| 函数 | 特点 | 适用场景 |
|---|---|---|
| reduce | 增量计算,内存高效 | 求和、求最大值、累加 |
| aggregate | 更灵活,可以改变数据类型 | 复杂计算、平均值 |
| process | 最灵活,可访问窗口元数据 | 需要窗口时间、自定义逻辑 |
Lambda简化写法
使用Java 8 Lambda可以简化为:
java
windowedStream.reduce(
(value1, value2) -> new Tuple2<>(value1.f0, value1.f1 + value2.f1)
);
关键要点
- ✅ 增量计算:不需要缓存窗口内所有数据,节省内存
- ✅ 输入输出类型相同:reduce函数的输入和输出必须是同一类型
- ✅ 结合律:reduce操作应满足结合律(即计算顺序不影响结果)
- ⚠️ 需要先keyBy:reduce通常在keyBy之后使用
这段代码是Flink中典型的事件时间滚动窗口配置,用于按key分组并进行时间窗口聚合。让我详细解释:
keyBy
代码解析
java
// 方式1:从POJO对象中提取key
dataStream.keyBy(value -> value.getSomeKey());
// 方式2:从Tuple中提取key(使用字段索引)
dataStream.keyBy(value -> value.f0);
核心概念:keyBy的作用
1. 什么是keyBy?
keyBy 是Flink中的分区算子,它会:
- 根据指定的key将数据重新分配到不同的并行任务
- 相同key的数据一定会被路由到同一个任务实例
- 为后续的状态计算 和窗口聚合提供基础
2. 数据流转示意图
keyBy之前(数据随机分布):
Task-1: user1, user3, user2
Task-2: user1, user4, user3
Task-3: user2, user1, user4
keyBy(userId)后(相同key在同一Task):
Task-1: user1, user1, user1 ← 所有user1
Task-2: user2, user2 ← 所有user2
Task-3: user3, user3 ← 所有user3
Task-4: user4, user4 ← 所有user4
方式1:从POJO对象提取key
java
dataStream.keyBy(value -> value.getSomeKey());
适用场景:自定义Java对象(POJO)
java
// 定义一个POJO类
class Event {
private String userId;
private String eventType;
private Long timestamp;
public String getUserId() { return userId; }
public String getEventType() { return eventType; }
// ... 其他getter/setter
}
DataStream<Event> events = ...;
// 按userId分组
events.keyBy(event -> event.getUserId());
// 按eventType分组
events.keyBy(event -> event.getEventType());
// 按复合key分组(多字段组合)
events.keyBy(event -> event.getUserId() + "_" + event.getEventType());
使用示例
java
DataStream<Event> clickStream = ...;
// 按用户ID分组,统计每个用户的点击次数
clickStream
.keyBy(event -> event.getUserId())
.map(new MapFunction<Event, Tuple2<String, Integer>>() {
public Tuple2<String, Integer> map(Event event) {
return new Tuple2<>(event.getUserId(), 1);
}
})
.keyBy(tuple -> tuple.f0)
.sum(1); // 每个用户的点击数累加
// 输出:
// ("user1", 5)
// ("user2", 3)
// ("user1", 6) ← user1又有新点击
方式2:从Tuple提取key
java
dataStream.keyBy(value -> value.f0);
适用场景:Flink内置的Tuple类型
Flink提供了Tuple0到Tuple25,字段通过f0, f1, f2...访问:
java
// Tuple2<String, Integer> 示例
DataStream<Tuple2<String, Integer>> tupleStream = ...;
// 按第一个字段(f0)分组
tupleStream.keyBy(tuple -> tuple.f0);
// 按第二个字段(f1)分组
tupleStream.keyBy(tuple -> tuple.f1);
// Tuple3<用户ID, 商品ID, 金额>
DataStream<Tuple3<String, String, Double>> orders = ...;
// 按用户ID分组
orders.keyBy(tuple -> tuple.f0);
// 按商品ID分组
orders.keyBy(tuple -> tuple.f1);
字段位置索引简化写法
java
// 使用Lambda表达式
tupleStream.keyBy(value -> value.f0);
// 或使用字段位置(旧版本API,不推荐)
tupleStream.keyBy(0); // 已过时,建议用Lambda
// POJO字段名(旧版本API,不推荐)
pojoStream.keyBy("userId"); // 已过时
完整使用示例
示例1:WordCount(Tuple方式)
java
DataStream<String> text = env.fromElements(
"hello world",
"hello flink",
"flink streaming"
);
DataStream<Tuple2<String, Integer>> wordCounts = text
// 拆分并转换为Tuple2
.flatMap((String line, Collector<Tuple2<String, Integer>> out) -> {
for (String word : line.split(" ")) {
out.collect(new Tuple2<>(word, 1));
}
}).returns(Types.TUPLE(Types.STRING, Types.INT))
// 按单词(f0)分组
.keyBy(tuple -> tuple.f0)
// 累加计数(f1)
.sum(1);
// 输出:
// (hello, 1)
// (world, 1)
// (hello, 2) ← hello的计数更新
// (flink, 1)
// (flink, 2) ← flink的计数更新
// (streaming, 1)
示例2:用户行为统计(POJO方式)
java
class UserAction {
String userId;
String action;
Long timestamp;
public String getUserId() { return userId; }
// ... getters
}
DataStream<UserAction> actions = ...;
// 统计每个用户的行为次数
actions
.keyBy(action -> action.getUserId())
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.process(new ProcessWindowFunction<UserAction, String, String, TimeWindow>() {
public void process(String userId, Context ctx,
Iterable<UserAction> elements,
Collector<String> out) {
int count = 0;
for (UserAction action : elements) {
count++;
}
out.collect("User " + userId + ": " + count + " actions");
}
});
示例3:复合Key(多字段组合)
java
class Order {
String userId;
String productId;
Double amount;
// ... getters
}
DataStream<Order> orders = ...;
// 方式1:字符串拼接(简单但有哈希冲突风险)
orders.keyBy(order -> order.getUserId() + "_" + order.getProductId());
// 方式2:使用Tuple作为Key(推荐)
orders.keyBy(order -> Tuple2.of(order.getUserId(), order.getProductId()));
// 方式3:自定义Key类(最规范)
class OrderKey {
String userId;
String productId;
@Override
public int hashCode() {
return Objects.hash(userId, productId);
}
@Override
public boolean equals(Object obj) {
// ... 实现equals
}
}
orders.keyBy(order -> new OrderKey(order.getUserId(), order.getProductId()));
keyBy的重要特性
1. 数据重分布(Shuffle)
java
Source(并行度=4)
↓ ↓ ↓ ↓
→ keyBy ← (根据key的hash重新分配)
↓ ↓ ↓ ↓
Operator(并行度=4)
// 相同key一定路由到同一个subtask
key.hashCode() % parallelism = subtask_index
2. 状态隔离
java
dataStream
.keyBy(event -> event.getUserId())
.map(new RichMapFunction<Event, String>() {
private ValueState<Integer> countState;
@Override
public void open(Configuration parameters) {
countState = getRuntimeContext().getState(
new ValueStateDescriptor<>("count", Integer.class)
);
}
@Override
public String map(Event event) throws Exception {
// 每个key有独立的状态
Integer count = countState.value();
if (count == null) count = 0;
count++;
countState.update(count);
return event.getUserId() + ": " + count;
}
});
// user1的状态和user2的状态是完全隔离的
3. 允许链式keyBy
java
// 先按用户分组统计,再按地区汇总
dataStream
.keyBy(event -> event.getUserId())
.sum(1)
.keyBy(tuple -> tuple.getRegion()) // 重新按地区分组
.sum(1);
keyBy后可以使用的操作
java
KeyedStream<Event, String> keyedStream =
dataStream.keyBy(event -> event.getUserId());
// 1. 聚合操作
keyedStream.sum("amount");
keyedStream.min("timestamp");
keyedStream.max("price");
keyedStream.reduce((v1, v2) -> ...);
// 2. 窗口操作
keyedStream.window(TumblingEventTimeWindows.of(Time.minutes(5)));
keyedStream.timeWindow(Time.minutes(5));
// 3. 状态操作
keyedStream.map(new RichMapFunction<>() { /* 使用状态 */ });
keyedStream.flatMap(new RichFlatMapFunction<>() { /* 使用状态 */ });
// 4. Process函数(最灵活)
keyedStream.process(new KeyedProcessFunction<>() { /* 自定义逻辑 */ });
常见问题和注意事项
❌ 错误用法
java
// 错误1:key类型必须实现hashCode和equals
class BadKey {
String field;
// 没有实现hashCode和equals - 会导致问题!
}
dataStream.keyBy(value -> new BadKey(value.getField()));
// 错误2:使用可变对象作为key
dataStream.keyBy(value -> new ArrayList<>(value.getList())); // 危险!
// 错误3:返回null作为key
dataStream.keyBy(value -> value.getUserId()); // 如果getUserId()返回null会报错
✅ 正确用法
java
// 正确1:使用不可变类型
dataStream.keyBy(value -> value.getUserId()); // String是不可变的
dataStream.keyBy(value -> value.getCount()); // Integer是不可变的
// 正确2:使用Tuple
dataStream.keyBy(value -> Tuple2.of(value.f0, value.f1));
// 正确3:处理null值
dataStream.keyBy(value ->
value.getUserId() != null ? value.getUserId() : "UNKNOWN"
);
// 正确4:自定义Key类并正确实现hashCode/equals
class ProperKey {
private final String userId;
private final String productId;
// 构造函数、getter省略
@Override
public int hashCode() {
return Objects.hash(userId, productId);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof ProperKey)) return false;
ProperKey other = (ProperKey) obj;
return Objects.equals(userId, other.userId) &&
Objects.equals(productId, other.productId);
}
}
Key选择器的性能考虑
java
// ✅ 好:简单高效
dataStream.keyBy(value -> value.f0);
// ⚠️ 一般:字符串拼接有开销
dataStream.keyBy(value -> value.f0 + "_" + value.f1);
// ❌ 差:每次都创建新对象
dataStream.keyBy(value -> new ComplexKey(value.f0, value.f1));
// ✅ 更好:使用Tuple(Flink内部优化)
dataStream.keyBy(value -> Tuple2.of(value.f0, value.f1));
关键要点总结
- ✅ 数据分区:keyBy根据key的hash值将数据路由到不同任务
- ✅ 相同key保证:相同key的所有数据一定在同一个并行实例
- ✅ 状态基础:keyBy是使用Keyed State的前提
- ✅ POJO vs Tuple:POJO用getter,Tuple用f0/f1/f2...
- ⚠️ 实现hashCode/equals:自定义Key类必须正确实现这两个方法
- ⚠️ 避免null:key不能为null
- ⚠️ 避免数据倾斜:确保key分布均匀,避免热点key
这两行代码的核心作用是:将数据流按照指定的key进行分区,为后续的分组聚合、窗口计算和状态管理奠定基础。这是Flink流处理中最重要的算子之一。
## Map
这是Flink中最基础的**转换算子**之一 ------ `map`操作,用于对数据流中的每个元素进行一对一转换。让我详细解释:
## 代码解析
```java
DataStream<Integer> dataStream = //...
dataStream.map(new MapFunction<Integer, Integer>() {
@Override
public Integer map(Integer value) throws Exception {
return 2 * value; // 将每个元素乘以2
}
});
核心概念
1. map算子特点
- 一对一转换:输入一个元素,输出一个元素
- 无状态操作:每个元素的处理互不影响
- 可以改变数据类型:输入和输出类型可以不同
2. MapFunction接口
java
MapFunction<输入类型, 输出类型>
泛型参数:
- 第一个
Integer:输入类型 - 第二个
Integer:输出类型
3. 执行逻辑
这段代码的作用:将数据流中的每个整数乘以2
执行示例
java
输入数据流:
1, 2, 3, 4, 5
map操作(每个元素 × 2):
1 → 2
2 → 4
3 → 6
4 → 8
5 → 10
输出数据流:
2, 4, 6, 8, 10
Lambda简化写法
Java 8可以使用Lambda表达式简化:
java
// 原写法
dataStream.map(new MapFunction<Integer, Integer>() {
@Override
public Integer map(Integer value) throws Exception {
return 2 * value;
}
});
// Lambda写法
dataStream.map(value -> 2 * value);
// 或者方法引用(如果有对应方法)
dataStream.map(MyClass::doubleValue);
类型转换示例
map可以改变数据类型:
java
// 示例1:Integer → String
DataStream<Integer> intStream = ...;
DataStream<String> stringStream = intStream.map(
value -> "Number: " + value
);
// 输入: 1, 2, 3
// 输出: "Number: 1", "Number: 2", "Number: 3"
// 示例2:String → Tuple2
DataStream<String> input = ...;
DataStream<Tuple2<String, Integer>> output = input.map(
new MapFunction<String, Tuple2<String, Integer>>() {
public Tuple2<String, Integer> map(String value) {
return new Tuple2<>(value, value.length());
}
}
);
// 输入: "hello", "world"
// 输出: ("hello", 5), ("world", 5)
// 示例3:对象转换
DataStream<User> users = ...;
DataStream<UserDTO> dtos = users.map(user -> {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setName(user.getName());
return dto;
});
常见使用场景
1. 数据字段提取
java
DataStream<Tuple3<String, Integer, Double>> input = ...;
// 只提取第一个字段
DataStream<String> names = input.map(tuple -> tuple.f0);
2. 数据清洗/转换
java
DataStream<String> logs = ...;
// 转大写
DataStream<String> upperLogs = logs.map(String::toUpperCase);
// 去除前后空格
DataStream<String> trimmedLogs = logs.map(String::trim);
3. 数值计算
java
DataStream<Double> prices = ...;
// 价格打8折
DataStream<Double> discountPrices = prices.map(price -> price * 0.8);
// 温度转换(摄氏度→华氏度)
DataStream<Double> celsius = ...;
DataStream<Double> fahrenheit = celsius.map(c -> c * 9/5 + 32);
4. JSON解析
java
DataStream<String> jsonStrings = ...;
DataStream<User> users = jsonStrings.map(json -> {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, User.class);
});
5. 富函数(RichMapFunction)访问上下文
java
dataStream.map(new RichMapFunction<Integer, Integer>() {
private int multiplier;
@Override
public void open(Configuration parameters) {
// 初始化,获取配置
multiplier = getRuntimeContext()
.getExecutionConfig()
.getGlobalJobParameters()
.getInteger("multiplier", 2);
}
@Override
public Integer map(Integer value) {
return value * multiplier;
}
});
与其他算子对比
| 算子 | 输入→输出 | 特点 | 使用场景 |
|---|---|---|---|
| map | 1 → 1 | 一对一转换 | 字段转换、数据清洗 |
| flatMap | 1 → 0/1/n | 一对多转换 | 拆分、过滤+转换 |
| filter | 1 → 0/1 | 过滤数据 | 条件筛选 |
| mapPartition | n → m | 批量处理 | 批量数据库操作 |
示例对比
java
DataStream<String> input = env.fromElements("hello world", "flink kafka");
// map: 1→1 (转大写)
input.map(String::toUpperCase);
// 输出: "HELLO WORLD", "FLINK KAFKA"
// flatMap: 1→n (拆分单词)
input.flatMap((String line, Collector<String> out) -> {
for (String word : line.split(" ")) {
out.collect(word);
}
});
// 输出: "hello", "world", "flink", "kafka"
// filter: 1→0/1 (过滤长度>5)
input.filter(line -> line.length() > 5);
// 输出: "hello world", "flink kafka"
完整示例代码
java
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class MapExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// 创建数据源
DataStream<Integer> dataStream = env.fromElements(1, 2, 3, 4, 5);
// map转换:每个元素乘以2
DataStream<Integer> result = dataStream.map(
new MapFunction<Integer, Integer>() {
@Override
public Integer map(Integer value) throws Exception {
return 2 * value;
}
}
);
// Lambda简化写法
DataStream<Integer> result2 = dataStream.map(value -> 2 * value);
// 输出结果
result.print();
// 执行
env.execute("Map Example");
}
}
关键要点
- ✅ 无状态操作:map不维护状态,元素间相互独立
- ✅ 并行执行:可以并行处理,性能高
- ✅ 类型灵活:可以改变输出类型
- ⚠️ 不能过滤 :必须输出一个元素(想过滤用
filter) - ⚠️ 不能拆分 :不能输出多个元素(想拆分用
flatMap)
FlatMap
这是Flink中的flatMap算子 ,用于将一个元素转换为零个、一个或多个元素。这是一个经典的单词拆分示例。让我详细解释:
代码解析
java
dataStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
for(String word: value.split(" ")){
out.collect(word); // 可以输出多个元素
}
}
});
核心特点
flatMap vs map 的关键区别
| 特性 | map | flatMap |
|---|---|---|
| 输入→输出 | 1 → 1(必须输出1个) | 1 → 0/1/n(可输出任意个) |
| 返回方式 | return单个值 | 用Collector收集多个值 |
| 典型场景 | 数据转换 | 拆分、过滤、膨胀 |
FlatMapFunction接口
java
FlatMapFunction<输入类型, 输出类型>
void flatMap(输入值, Collector<输出类型> out)
参数说明:
value:输入的元素(这里是String,一行文本)out:输出收集器,可以调用out.collect()多次
执行示例
示例1:单词拆分(代码中的例子)
java
输入数据流:
"hello world"
"apache flink"
"streaming processing"
flatMap处理(按空格拆分):
"hello world" → out.collect("hello"), out.collect("world")
"apache flink" → out.collect("apache"), out.collect("flink")
"streaming processing" → out.collect("streaming"), out.collect("processing")
输出数据流:
"hello"
"world"
"apache"
"flink"
"streaming"
"processing"
流程图
输入流(3条记录):
┌─────────────────────┐
│ "hello world" │ ───┐
├─────────────────────┤ │
│ "apache flink" │ ───┼─→ flatMap拆分
├─────────────────────┤ │
│ "streaming process" │ ───┘
└─────────────────────┘
输出流(6条记录):
┌──────────┐
│ "hello" │
├──────────┤
│ "world" │
├──────────┤
│ "apache" │
├──────────┤
│ "flink" │
├──────────┤
│ "streaming" │
├──────────┤
│ "process"│
└──────────┘
Lambda简化写法
java
// 原写法
dataStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
for(String word: value.split(" ")){
out.collect(word);
}
}
});
// Lambda写法
dataStream.flatMap(
(String value, Collector<String> out) -> {
for(String word : value.split(" ")) {
out.collect(word);
}
}
).returns(Types.STRING); // 需要显式声明返回类型
// Java 8 Stream风格(更简洁)
dataStream.flatMap(
(String value, Collector<String> out) ->
Arrays.stream(value.split(" ")).forEach(out::collect)
).returns(Types.STRING);
常见使用场景
1. 数据拆分(最常见)
java
// 场景:日志拆分成多条记录
DataStream<String> logs = ...; // "user1,user2,user3"
DataStream<String> users = logs.flatMap(
(String log, Collector<String> out) -> {
for (String user : log.split(",")) {
out.collect(user.trim());
}
}
).returns(Types.STRING);
// 输入: "user1,user2,user3"
// 输出: "user1", "user2", "user3"
2. 过滤 + 转换(输出0或1个)
java
// 场景:过滤掉非法数据并转换
DataStream<String> input = ...;
DataStream<Integer> validNumbers = input.flatMap(
(String value, Collector<Integer> out) -> {
try {
int num = Integer.parseInt(value);
if (num > 0) { // 只保留正数
out.collect(num);
}
// 负数或0不输出(相当于过滤)
} catch (NumberFormatException e) {
// 非法数据不输出
}
}
).returns(Types.INT);
// 输入: "10", "abc", "-5", "20"
// 输出: 10, 20
3. 数据膨胀(一条变多条)
java
// 场景:将订单拆分成多个商品记录
class Order {
String orderId;
List<String> products;
}
DataStream<Order> orders = ...;
DataStream<Tuple2<String, String>> orderProducts = orders.flatMap(
(Order order, Collector<Tuple2<String, String>> out) -> {
for (String product : order.products) {
out.collect(new Tuple2<>(order.orderId, product));
}
}
).returns(Types.TUPLE(Types.STRING, Types.STRING));
// 输入: Order("order1", ["apple", "banana", "orange"])
// 输出: ("order1", "apple")
// ("order1", "banana")
// ("order1", "orange")
4. JSON解析 + 拆分
java
// 场景:解析JSON数组,输出多条记录
DataStream<String> jsonStream = ...; // "[{...}, {...}, {...}]"
DataStream<User> users = jsonStream.flatMap(
(String json, Collector<User> out) -> {
JSONArray array = new JSONArray(json);
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
User user = new User(
obj.getString("id"),
obj.getString("name")
);
out.collect(user);
}
}
).returns(User.class);
5. 嵌套结构扁平化
java
// 场景:将嵌套的数据结构展平
class Department {
String deptName;
List<Employee> employees;
}
DataStream<Department> departments = ...;
DataStream<Employee> allEmployees = departments.flatMap(
(Department dept, Collector<Employee> out) -> {
dept.employees.forEach(out::collect);
}
).returns(Employee.class);
完整WordCount示例
这是Flink最经典的示例:
java
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class WordCount {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> text = env.fromElements(
"hello world",
"hello flink",
"flink streaming"
);
DataStream<Tuple2<String, Integer>> wordCounts = text
// 步骤1:拆分单词
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) {
for (String word : value.split(" ")) {
out.collect(new Tuple2<>(word, 1));
}
}
})
// 步骤2:按单词分组
.keyBy(tuple -> tuple.f0)
// 步骤3:计数
.sum(1);
wordCounts.print();
env.execute("Word Count Example");
}
}
/* 输出:
(hello, 2)
(world, 1)
(flink, 2)
(streaming, 1)
*/
与其他算子的组合使用
java
DataStream<String> input = ...;
// flatMap + filter + map 组合
DataStream<Integer> result = input
.flatMap((String line, Collector<String> out) -> {
for (String word : line.split(" ")) {
out.collect(word);
}
}).returns(Types.STRING)
.filter(word -> word.length() > 3) // 过滤长度<=3的单词
.map(String::length); // 转换为单词长度
// 输入: "hello world a flink"
// flatMap后: "hello", "world", "a", "flink"
// filter后: "hello", "world", "flink"
// map后: 5, 5, 5
输出模式对比
java
// 输出0个元素(过滤)
flatMap((String value, Collector<String> out) -> {
if (value.startsWith("ERROR")) {
out.collect(value); // 只输出ERROR日志
}
// 其他日志不输出(相当于过滤掉)
})
// 输出1个元素(类似map)
flatMap((String value, Collector<String> out) -> {
out.collect(value.toUpperCase()); // 只输出一个
})
// 输出多个元素(拆分/膨胀)
flatMap((String value, Collector<String> out) -> {
for (String word : value.split(" ")) {
out.collect(word); // 输出多个
}
})
使用RichFlatMapFunction访问状态
java
dataStream.flatMap(new RichFlatMapFunction<String, String>() {
private transient ListState<String> bufferState;
@Override
public void open(Configuration parameters) {
// 初始化状态
ListStateDescriptor<String> descriptor =
new ListStateDescriptor<>("buffer", String.class);
bufferState = getRuntimeContext().getListState(descriptor);
}
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
// 可以访问状态、度量指标等
for (String word : value.split(" ")) {
out.collect(word);
bufferState.add(word); // 保存到状态
}
}
});
关键要点
- ✅ 一对多转换:一个输入可以产生0到N个输出
- ✅ 使用Collector :通过
out.collect()输出元素,可以调用多次 - ✅ 灵活性高:可以实现拆分、过滤、膨胀等多种操作
- ✅ 常用于预处理:如日志解析、数据拆分、结构展平
- ⚠️ 需要类型推断 :Lambda表达式需要
.returns()显式声明类型
WindowFunction & AllWindowFunction
1 WindowFunction(分区窗口)*
java
windowedStream.apply(new WindowFunction<Tuple2<String,Integer>, Integer, Tuple, Window>() {
public void apply(
Tuple tuple, // 窗口的key
Window window, // 窗口对象
Iterable<Tuple2<String, Integer>> values, // 窗口内所有元素
Collector<Integer> out // 输出收集器
) throws Exception {
int sum = 0;
for (Tuple2<String, Integer> t: values) { // 注意:原代码有误
sum += t.f1; // 累加第二个字段
}
out.collect(new Integer(sum)); // 输出窗口总和
}
});
2 AllWindowFunction(全局窗口)
java
allWindowedStream.apply(new AllWindowFunction<Tuple2<String,Integer>, Integer, Window>() {
public void apply(
Window window, // 窗口对象(没有key参数)
Iterable<Tuple2<String, Integer>> values, // 窗口内所有元素
Collector<Integer> out // 输出收集器
) throws Exception {
int sum = 0;
for (Tuple2<String, Integer> t: values) {
sum += t.f1;
}
out.collect(new Integer(sum));
}
});
核心区别对比
WindowFunction vs AllWindowFunction
| 特性 | WindowFunction | AllWindowFunction |
|---|---|---|
| 使用位置 | KeyedStream窗口 | 非分区窗口(windowAll) |
| 参数个数 | 4个(包含key) | 3个(无key参数) |
| 并行度 | 可并行(按key分区) | 强制为1(全局) |
| 处理范围 | 每个key的窗口数据 | 全局所有数据 |
| 第一个参数 | Tuple key | Window window |
参数详解
WindowFunction的参数
java
public void apply(
Tuple tuple, // 1. 窗口的key(当前处理的是哪个key的窗口)
Window window, // 2. 窗口元信息(开始时间、结束时间等)
Iterable<...> values, // 3. 窗口内的所有元素(缓存的数据)
Collector<...> out // 4. 输出收集器
)
AllWindowFunction的参数
java
public void apply(
Window window, // 1. 窗口元信息(没有key概念)
Iterable<...> values, // 2. 窗口内的所有元素
Collector<...> out // 3. 输出收集器
)
完整执行示例
示例1:WindowFunction(按key分区)
java
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Tuple2<String, Integer>> input = env.fromElements(
new Tuple2<>("user1", 10),
new Tuple2<>("user2", 20),
new Tuple2<>("user1", 15),
new Tuple2<>("user2", 30),
new Tuple2<>("user1", 5)
);
input
.keyBy(tuple -> tuple.f0) // 按用户ID分组
.timeWindow(Time.seconds(5)) // 5秒窗口
.apply(new WindowFunction<Tuple2<String,Integer>, String, String, TimeWindow>() {
@Override
public void apply(
String key, // 当前key(如"user1")
TimeWindow window, // 窗口时间信息
Iterable<Tuple2<String, Integer>> values,
Collector<String> out
) throws Exception {
int sum = 0;
int count = 0;
// 遍历窗口内该key的所有数据
for (Tuple2<String, Integer> value : values) {
sum += value.f1;
count++;
}
// 可以访问窗口信息
String result = String.format(
"Key: %s, Window: [%d-%d], Count: %d, Sum: %d",
key,
window.getStart(),
window.getEnd(),
count,
sum
);
out.collect(result);
}
})
.print();
/* 输出(假设都在同一个5秒窗口内):
Key: user1, Window: [0-5000], Count: 3, Sum: 30
Key: user2, Window: [0-5000], Count: 2, Sum: 50
*/
示例2:AllWindowFunction(全局窗口)
java
DataStream<Tuple2<String, Integer>> input = env.fromElements(
new Tuple2<>("user1", 10),
new Tuple2<>("user2", 20),
new Tuple2<>("user1", 15),
new Tuple2<>("user2", 30),
new Tuple2<>("user1", 5)
);
input
.timeWindowAll(Time.seconds(5)) // 全局5秒窗口(无keyBy)
.apply(new AllWindowFunction<Tuple2<String,Integer>, String, TimeWindow>() {
@Override
public void apply(
TimeWindow window, // 窗口信息(无key参数)
Iterable<Tuple2<String, Integer>> values,
Collector<String> out
) throws Exception {
int sum = 0;
int count = 0;
// 遍历窗口内所有数据(不区分key)
for (Tuple2<String, Integer> value : values) {
sum += value.f1;
count++;
}
String result = String.format(
"Window: [%d-%d], Total Count: %d, Total Sum: %d",
window.getStart(),
window.getEnd(),
count,
sum
);
out.collect(result);
}
})
.print();
/* 输出(所有数据一起统计):
Window: [0-5000], Total Count: 5, Total Sum: 80
*/
执行机制对比图
WindowFunction(分区执行)
输入数据流:
("user1", 10)
("user2", 20)
("user1", 15)
("user2", 30)
↓ keyBy(f0)
分区后:
[user1] → [10, 15] → WindowFunction → "user1: sum=25"
[user2] → [20, 30] → WindowFunction → "user2: sum=50"
并行处理,每个key独立
AllWindowFunction(全局执行)
输入数据流:
("user1", 10)
("user2", 20)
("user1", 15)
("user2", 30)
↓ windowAll (no keyBy)
全局窗口:
[All] → [10, 20, 15, 30] → AllWindowFunction → "total: sum=75"
单并行度,所有数据一起处理
常见使用场景
场景1:计算窗口统计指标
java
// WindowFunction:每个用户的窗口统计
windowedStream.apply(
new WindowFunction<Event, UserStats, String, TimeWindow>() {
public void apply(String userId, TimeWindow window,
Iterable<Event> events, Collector<UserStats> out) {
int clickCount = 0;
int purchaseCount = 0;
double totalAmount = 0;
for (Event event : events) {
if (event.type.equals("click")) clickCount++;
if (event.type.equals("purchase")) {
purchaseCount++;
totalAmount += event.amount;
}
}
out.collect(new UserStats(userId, clickCount,
purchaseCount, totalAmount));
}
}
);
场景2:窗口内排序
java
// 每个窗口内按金额排序,输出Top 3
windowedStream.apply(
new WindowFunction<Order, String, String, TimeWindow>() {
public void apply(String productId, TimeWindow window,
Iterable<Order> orders, Collector<String> out) {
// 收集到List并排序
List<Order> orderList = new ArrayList<>();
orders.forEach(orderList::add);
orderList.sort(Comparator.comparing(Order::getAmount).reversed());
// 输出Top 3
orderList.stream()
.limit(3)
.forEach(order -> out.collect(order.toString()));
}
}
);
场景3:全局TopN(AllWindowFunction)
java
// 每个窗口统计全局销量Top 10商品
allWindowedStream.apply(
new AllWindowFunction<Product, String, TimeWindow>() {
public void apply(TimeWindow window,
Iterable<Product> products,
Collector<String> out) {
// 统计每个商品的销量
Map<String, Integer> salesMap = new HashMap<>();
for (Product p : products) {
salesMap.merge(p.productId, p.quantity, Integer::sum);
}
// 排序并输出Top 10
salesMap.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(10)
.forEach(entry ->
out.collect(entry.getKey() + ": " + entry.getValue())
);
}
}
);
泛型参数解释
WindowFunction的泛型
java
WindowFunction<IN, OUT, KEY, W extends Window>
IN: 输入元素类型
OUT: 输出元素类型
KEY: Key的类型
W: 窗口类型(TimeWindow, GlobalWindow等)
AllWindowFunction的泛型
java
AllWindowFunction<IN, OUT, W extends Window>
IN: 输入元素类型
OUT: 输出元素类型
W: 窗口类型
重要缺点:全量窗口函数
⚠️ WindowFunction和AllWindowFunction是全量窗口函数,有以下问题:
性能问题
java
// ❌ WindowFunction:需要缓存窗口内所有数据
windowedStream.apply(new WindowFunction<...>() {
public void apply(..., Iterable<T> values, ...) {
// 窗口关闭时才调用,需要缓存所有数据
// 内存占用:O(窗口内元素数量)
}
});
// ✅ ReduceFunction:增量计算
windowedStream.reduce((v1, v2) -> v1 + v2);
// 内存占用:O(1),只保存累积值
新API推荐:ProcessWindowFunction
Flink推荐使用ProcessWindowFunction替代WindowFunction:
java
// 旧API(不推荐)
windowedStream.apply(new WindowFunction<...>() {
public void apply(KEY key, Window window,
Iterable<IN> values, Collector<OUT> out) {
// ...
}
});
// 新API(推荐)
windowedStream.process(new ProcessWindowFunction<IN, OUT, KEY, Window>() {
@Override
public void process(
KEY key,
Context context, // 更强大的上下文
Iterable<IN> elements,
Collector<OUT> out
) throws Exception {
// context可以访问更多信息:
// - context.window() - 窗口信息
// - context.currentWatermark() - 当前watermark
// - context.windowState() - 窗口状态
// - context.globalState() - 全局状态
int sum = 0;
for (IN element : elements) {
sum += element.getValue();
}
out.collect(sum);
}
});
ProcessWindowFunction的优势
java
windowedStream.process(
new ProcessWindowFunction<Tuple2<String,Integer>, String, String, TimeWindow>() {
@Override
public void process(
String key,
Context ctx,
Iterable<Tuple2<String, Integer>> elements,
Collector<String> out
) throws Exception {
// ✅ 可以访问窗口状态
ValueState<Long> state = ctx.windowState().getState(
new ValueStateDescriptor<>("count", Long.class)
);
// ✅ 可以获取watermark
long watermark = ctx.currentWatermark();
// ✅ 可以访问窗口信息
TimeWindow window = ctx.window();
// ✅ 可以注册定时器(ProcessWindowFunction不直接支持,需要KeyedProcessFunction)
int sum = 0;
for (Tuple2<String, Integer> element : elements) {
sum += element.f1;
}
out.collect(String.format(
"Key: %s, Window: [%d-%d], Sum: %d, Watermark: %d",
key, window.getStart(), window.getEnd(), sum, watermark
));
}
}
);
增量聚合 + 全量窗口函数结合
为了解决性能问题,可以结合使用:
java
// ✅ 最佳实践:增量聚合 + ProcessWindowFunction
windowedStream
.aggregate(
// 增量聚合函数(内存高效)
new AggregateFunction<Tuple2<String,Integer>, Integer, Integer>() {
public Integer createAccumulator() { return 0; }
public Integer add(Tuple2<String,Integer> value, Integer acc) {
return acc + value.f1;
}
public Integer getResult(Integer acc) { return acc; }
public Integer merge(Integer a, Integer b) { return a + b; }
},
// 全量窗口函数(获取窗口元信息)
new ProcessWindowFunction<Integer, String, String, TimeWindow>() {
public void process(String key, Context ctx,
Iterable<Integer> sums, Collector<String> out) {
Integer sum = sums.iterator().next(); // 增量结果
TimeWindow window = ctx.window();
out.collect(String.format(
"Key: %s, Window: [%d-%d], Sum: %d",
key, window.getStart(), window.getEnd(), sum
));
}
}
);
// 优点:
// 1. 增量计算,内存占用小
// 2. 可以访问窗口元信息
// 3. 性能和灵活性兼顾
完整对比示例
java
public class WindowFunctionComparison {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Tuple2<String, Integer>> input = env.fromElements(
new Tuple2<>("A", 1),
new Tuple2<>("B", 2),
new Tuple2<>("A", 3),
new Tuple2<>("B", 4),
new Tuple2<>("A", 5)
);
// 方式1:WindowFunction(旧API)
input.keyBy(t -> t.f0)
.countWindow(3)
.apply(new WindowFunction<Tuple2<String,Integer>, String, String, GlobalWindow>() {
public void apply(String key, GlobalWindow window,
Iterable<Tuple2<String, Integer>> values,
Collector<String> out) {
int sum = 0;
for (Tuple2<String, Integer> v : values) sum += v.f1;
out.collect(key + ": " + sum);
}
})
.print();
// 方式2:ProcessWindowFunction(新API,推荐)
input.keyBy(t -> t.f0)
.countWindow(3)
.process(new ProcessWindowFunction<Tuple2<String,Integer>, String, String, GlobalWindow>() {
public void process(String key, Context ctx,
Iterable<Tuple2<String, Integer>> elements,
Collector<String> out) {
int sum = 0;
for (Tuple2<String, Integer> e : elements) sum += e.f1;
out.collect(key + ": " + sum);
}
})
.print();
env.execute();
}
}
关键要点总结
- ✅ WindowFunction: 用于KeyedStream窗口,有key参数,可并行
- ✅ AllWindowFunction: 用于非分区窗口,无key参数,并行度=1
- ⚠️ 全量函数: 需要缓存窗口内所有数据,内存开销大
- ⚠️ 性能问题: 窗口大时可能OOM
- ✅ 推荐新API: 使用ProcessWindowFunction替代
- ✅ 最佳实践: 增量聚合 + ProcessWindowFunction结合
- ✅ 适用场景: 需要遍历窗口所有元素的复杂计算(排序、TopN等)