Flink DatastreamAPI 详解(一)

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)
);

关键要点

  1. 增量计算:不需要缓存窗口内所有数据,节省内存
  2. 输入输出类型相同:reduce函数的输入和输出必须是同一类型
  3. 结合律:reduce操作应满足结合律(即计算顺序不影响结果)
  4. ⚠️ 需要先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提供了Tuple0Tuple25,字段通过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));

关键要点总结

  1. 数据分区:keyBy根据key的hash值将数据路由到不同任务
  2. 相同key保证:相同key的所有数据一定在同一个并行实例
  3. 状态基础:keyBy是使用Keyed State的前提
  4. POJO vs Tuple:POJO用getter,Tuple用f0/f1/f2...
  5. ⚠️ 实现hashCode/equals:自定义Key类必须正确实现这两个方法
  6. ⚠️ 避免null:key不能为null
  7. ⚠️ 避免数据倾斜:确保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");
    }
}

关键要点

  1. 无状态操作:map不维护状态,元素间相互独立
  2. 并行执行:可以并行处理,性能高
  3. 类型灵活:可以改变输出类型
  4. ⚠️ 不能过滤 :必须输出一个元素(想过滤用filter
  5. ⚠️ 不能拆分 :不能输出多个元素(想拆分用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);  // 保存到状态
        }
    }
});

关键要点

  1. 一对多转换:一个输入可以产生0到N个输出
  2. 使用Collector :通过out.collect()输出元素,可以调用多次
  3. 灵活性高:可以实现拆分、过滤、膨胀等多种操作
  4. 常用于预处理:如日志解析、数据拆分、结构展平
  5. ⚠️ 需要类型推断 :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();
    }
}

关键要点总结

  1. WindowFunction: 用于KeyedStream窗口,有key参数,可并行
  2. AllWindowFunction: 用于非分区窗口,无key参数,并行度=1
  3. ⚠️ 全量函数: 需要缓存窗口内所有数据,内存开销大
  4. ⚠️ 性能问题: 窗口大时可能OOM
  5. 推荐新API: 使用ProcessWindowFunction替代
  6. 最佳实践: 增量聚合 + ProcessWindowFunction结合
  7. 适用场景: 需要遍历窗口所有元素的复杂计算(排序、TopN等)
相关推荐
Olrookie1 天前
StreamX部署详细步骤
大数据·笔记·flink
wending-Y2 天前
如何正确理解flink 消费kafka时的watermark
flink·kafka·linq
wudl55663 天前
Flink 1.20 自定义SQL连接器实战
大数据·sql·flink
想ai抽3 天前
Flink中的Lookup join和Temporal join 的语法是一样的吗?
java·大数据·flink
阿里云大数据AI技术4 天前
云栖实录 | 理想汽车基于 Hologres + Flink 构建万亿级车联网信号实时分析平台
数据分析·flink
想ai抽4 天前
Flink的checkpoint interval与mini-batch什么区别?
大数据·flink·batch
教练、我想打篮球4 天前
12 pyflink 的一个基础使用, 以及环境相关
python·flink·pyflink
Hello.Reader4 天前
在 Flink 中用好 Java 8 Lambda类型推断、`.returns(...)` 与常见坑位
java·python·flink
隔壁寝室老吴4 天前
Flink中自定义序列化器
大数据·flink