Apache Flink 全面知识点详解
一、Flink 简介
1.1 什么是 Flink
Apache Flink 是一个框架和分布式处理引擎,用于在无界和有界 数据流上进行有状态计算。
核心特征:
| 特征 | 说明 |
|---|---|
| 分布式 | 运行在多台机器集群上 |
| 有状态 | 能够记住已处理的数据信息 |
| 高吞吐 | 每秒处理数百万条事件 |
| 低延迟 | 毫秒级别延迟 |
| 精确一次 | Exactly-Once 语义保证 |
1.2 Flink 发展历程
2014年 → Flink 成为 Apache 顶级项目
2015年 → Flink 1.0 发布
2019年 → Flink 1.9 发布(阿里巴巴收购 Data Artisans)
2020年 → Flink 1.11 发布(重大改进)
2023年 → Flink 1.17+(持续优化)
二、为什么选择 Flink
2.1 数据处理的三种语义
java
/**
* Flink 处理数据的三种时间语义示例
* 说明:Flink 支持三种时间语义,这是选择 Flink 的重要原因之一
*/
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class TimeSemanticDemo {
public static void main(String[] args) throws Exception {
// 获取 Flink 流处理执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// ====== 1. 处理时间(Processing Time)======
// 处理时间:数据被算子处理时所在机器的系统时间
// 优点:性能最好,延迟最低
// 缺点:结果不确定,因为依赖机器时间
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
System.out.println("使用处理时间语义");
// ====== 2. 事件时间(Event Time)======
// 事件时间:事件实际发生的时间(数据本身携带的时间戳)
// 优点:结果确定,可回放重放
// 缺点:需要处理乱序事件,需要 Watermark
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
System.out.println("使用事件时间语义");
// ====== 3. 摄入时间(Ingestion Time)======
// 摄入时间:数据进入 Flink source 的时间
// 是处理时间和事件时间的折中方案
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);
System.out.println("使用摄入时间语义");
// 执行环境
env.execute("Time Semantic Demo");
}
}
三、传统数据处理架构
3.1 事务处理架构
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 应用程序 │────▶│ 数据库 │────▶│ 查询结果 │
│ (实时事务) │◀────│ (存储状态) │◀────│ (人工查询) │
└──────────┘ └──────────┘ └──────────┘
特点: 每个事务独立处理,直接读写数据库
问题: 无法在大量数据上做复杂分析
3.2 分析处理架构(OLAP)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 数据库 │────▶│ ETL过程 │────▶│ 数据仓库 │
│ (OLTP) │ │(周期性提取)│ │ (OLAP) │
└──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐
│ 分析报表 │
└──────────┘
特点: 数据定期从OLTP数据库抽取,转换后加载到数据仓库
问题: 存在延迟(小时/天级别),不支持实时分析
java
/**
* 传统 ETL 处理模式示例(伪代码)
* 说明:展示传统批处理 ETL 的处理流程
*/
public class TraditionalETL {
/**
* 模拟传统 ETL 过程
* 传统方式:数据按批次处理,存在较大延迟
*/
public static void main(String[] args) {
// ====== 第一步:从源数据库抽取数据(Extract)======
// 假设每小时抽取一次数据
System.out.println("凌晨1点:开始从源数据库抽取数据...");
List<String> rawData = extractFromDatabase("2024-01-01 00:00:00", "2024-01-01 01:00:00");
System.out.println("抽取数据量:" + rawData.size() + " 条");
// ====== 第二步:数据转换(Transform)======
// 对原始数据进行清洗、过滤、聚合
System.out.println("开始数据转换...");
List<String> transformedData = new ArrayList<>();
for (String record : rawData) {
// 清洗:去除无效数据
if (isValid(record)) {
// 转换:格式化数据
String formatted = formatRecord(record);
transformedData.add(formatted);
}
}
System.out.println("转换后数据量:" + transformedData.size() + " 条");
// ====== 第三步:加载到数据仓库(Load)======
System.out.println("开始加载数据到数据仓库...");
loadToDataWarehouse(transformedData);
System.out.println("ETL 完成!");
// 注意:整个过程有至少1小时的延迟
}
// 模拟从数据库抽取数据
private static List<String> extractFromDatabase(String startTime, String endTime) {
return Arrays.asList("record1", "record2", "record3");
}
// 判断数据是否有效
private static boolean isValid(String record) {
return record != null && !record.isEmpty();
}
// 格式化数据记录
private static String formatRecord(String record) {
return record.toUpperCase();
}
// 加载数据到数据仓库
private static void loadToDataWarehouse(List<String> data) {
System.out.println("已加载 " + data.size() + " 条记录");
}
}
四、大数据 Lambda 架构
4.1 Lambda 架构原理
┌─────────────────────────────┐
│ 查询层(Serving) │
│ 合并实时层和批处理层结果 │
└──────────┬──────────────────┘
│
┌───────────────────┼───────────────────┐
│ │
┌───────────▼──────────┐ ┌───────────────▼──────────┐
│ 批处理层(Batch) │ │ 实时层(Speed) │
│ - 高延迟但结果准确 │ │ - 低延迟但可能不精确 │
│ - 处理全量历史数据 │ │ - 处理增量实时数据 │
│ - Hadoop MapReduce │ │ - Storm/Spark Streaming │
└───────────┬──────────┘ └───────────────┬──────────┘
│ │
└───────────────────┬───────────────────┘
│
┌──────────▼──────────────────┐
│ 数据源(Data Source) │
└─────────────────────────────┘
4.2 Lambda 架构的问题
java
/**
* Lambda 架构的痛点演示
* 说明:Lambda 架构需要维护两套代码,开发和维护成本高
*/
// ====== 批处理层代码(使用 MapReduce 思想)======
public class BatchLayerJob {
/**
* 批处理层:计算每小时的单词统计
* 使用全量数据,结果准确但延迟高
*/
public static class BatchMapper {
/**
* Map 阶段:将每行文本拆分为单词并计数
* @param line 输入的文本行
* @param output 输出的键值对列表
*/
public void map(String line, List<Pair<String, Integer>> output) {
// 按空格拆分文本行
String[] words = line.split("\\s+");
// 每个单词输出一个 (word, 1) 的键值对
for (String word : words) {
output.add(new Pair<>(word, 1));
}
}
}
/**
* Reduce 阶段:将相同单词的计数求和
* @param word 单词
* @param counts 该单词的所有计数值
* @return 汇总后的计数结果
*/
public int reduce(String word, List<Integer> counts) {
int sum = 0;
// 遍历所有计数值并累加
for (int count : counts) {
sum += count;
}
return sum;
}
}
// ====== 实时处理层代码(使用 Storm 思想)======
// 注意:与批处理层完全不同的 API 和代码逻辑
public class SpeedLayerTopology {
/**
* 实时处理层:实时统计单词数量
* 使用增量数据,延迟低但可能不准确
*/
public static class RealtimeBolt {
// 使用 HashMap 维护实时计数状态
private HashMap<String, Long> wordCounts = new HashMap<>();
/**
* 实时处理每条消息
* @param tuple 包含单词的数据元组
*/
public void execute(Tuple tuple) {
// 获取单词
String word = tuple.getString(0);
// 更新实时计数(可能存在重复计数等问题)
Long currentCount = wordCounts.getOrDefault(word, 0L);
wordCounts.put(word, currentCount + 1);
// 发射更新后的计数
emit(word, wordCounts.get(word));
}
}
}
/**
* Lambda 架构的问题总结:
* 1. 需要维护两套代码(批处理 + 实时处理)
* 2. 两套代码的结果需要合并,逻辑复杂
* 3. 开发成本高,测试困难
* 4. 运维成本高,需要维护两套集群
* 5. Flink 的出现解决了这些问题:一套代码,同时支持流和批
*/
五、流处理架构
5.1 纯流处理架构
┌──────────┐ ┌───────────────────────────┐ ┌──────────┐
│ 数据源 │────▶│ Flink 流处理引擎 │────▶│ 数据输出 │
│ (持续输入) │ │ 有状态 + 精确一次语义 │ │ (持续输出) │
└──────────┘ └───────────────────────────┘ └──────────┘
java
/**
* Flink 纯流处理架构示例
* 说明:展示 Flink 作为统一流处理引擎的基本编程模式
*/
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
public class PureStreamProcessing {
public static void main(String[] args) throws Exception {
// ====== 1. 获取流处理执行环境 ======
// 所有 Flink 程序的入口点
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 设置并行度为 4(生产环境通常根据 CPU 核数设置)
env.setParallelism(4);
// ====== 2. 从数据源读取数据流 ======
// DataStream 是 Flink 流处理的核心抽象
DataStream<String> sourceStream = env.socketTextStream("localhost", 9999);
// ====== 3. 对数据流进行转换处理 ======
// 3.1 将每行文本按空格拆分为单词
DataStream<String> words = sourceStream
.flatMap((String line, Collector<String> out) -> {
// 按空格拆分每一行
String[] wordArray = line.split("\\s+");
// 将每个单词输出
for (String word : wordArray) {
out.collect(word);
}
})
.returns(Types.STRING); // 指定返回类型
// 3.2 将单词映射为 (word, 1) 的元组
DataStream<Tuple2<String, Integer>> wordPairs = words
.map(word -> Tuple2.of(word, 1)) // 每个单词计数为1
.returns(Types.TUPLE(Types.STRING, Types.INT));
// 3.3 按单词分组,并在 5 秒的时间窗口内聚合
DataStream<Tuple2<String, Integer>> result = wordPairs
.keyBy(tuple -> tuple.f0) // 按单词(第一个字段)分组
.timeWindow(Time.seconds(5)) // 5秒的滚动窗口
.sum(1); // 对第二个字段(计数)求和
// ====== 4. 输出结果 ======
// 将结果打印到控制台
result.print();
// ====== 5. 触发执行 ======
// 注意:execute() 是懒执行的,只有调用 execute() 才会真正开始执行
env.execute("Pure Stream Processing Demo");
}
}
六、Flink 是理想的流计算框架
6.1 Flink 与其他框架对比
| 特性 | Flink | Spark Streaming | Storm |
|---|---|---|---|
| 处理模型 | 真正的流处理 | 微批处理 | 真正的流处理 |
| 延迟 | 毫秒级 | 秒级 | 毫秒级 |
| 吞吐量 | 高 | 高 | 较低 |
| Exactly-Once | 支持 | 支持 | 支持(Trident) |
| 状态管理 | 内置强大 | 有限 | 基础 |
| 窗口支持 | 丰富灵活 | 基于批 | 无原生支持 |
| 批流统一 | 原生支持 | 核心是批 | 不支持 |
| SQL支持 | Flink SQL | Spark SQL | 无 |
6.2 Flink 窗口机制详解
java
/**
* Flink 窗口(Window)机制全面示例
* 说明:Flink 支持多种窗口类型,是其核心优势之一
*/
import org.apache.flink.api.common.functions.*;
import org.apache.flink.streaming.api.datastream.*;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.*;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.util.ArrayList;
import java.util.List;
public class WindowDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从 socket 读取数据
DataStream<String> source = env.socketTextStream("localhost", 9999);
// 转换为 (sensorId, temperature) 格式
DataStream<Tuple2<String, Double>> sensorData = source
.map(line -> {
String[] fields = line.split(",");
// 第一个字段是传感器ID,第二个是温度
return Tuple2.of(fields[0], Double.parseDouble(fields[1].trim()));
})
.returns(Types.TUPLE(Types.STRING, Types.DOUBLE));
// ====== 1. 滚动窗口(Tumbling Window)======
// 每 10 秒一个窗口,窗口之间不重叠
DataStream<Tuple2<String, Double>> tumblingResult = sensorData
.keyBy(tuple -> tuple.f0) // 按传感器ID分组
.window(TumblingEventTimeWindows.of(Time.seconds(10))) // 10秒滚动窗口
.reduce((t1, t2) -> {
// 在窗口内求最大温度
return Tuple2.of(t1.f0, Math.max(t1.f1, t2.f1));
});
System.out.println("=== 滚动窗口结果 ===");
tumblingResult.print();
// ====== 2. 滑动窗口(Sliding Window)======
// 窗口大小 10 秒,滑动步长 5 秒,窗口之间有重叠
DataStream<Tuple2<String, Double>> slidingResult = sensorData
.keyBy(tuple -> tuple.f0) // 按传感器ID分组
.window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
.reduce((t1, t2) -> {
// 在窗口内求平均温度
return Tuple2.of(t1.f0, (t1.f1 + t2.f1) / 2);
});
System.out.println("=== 滑动窗口结果 ===");
slidingResult.print();
// ====== 3. 会话窗口(Session Window)======
// 如果超过 5 秒没有新数据,则窗口关闭
DataStream<Tuple2<String, Double>> sessionResult = sensorData
.keyBy(tuple -> tuple.f0) // 按传感器ID分组
.window(EventTimeSessionWindows.withGap(Time.seconds(5))) // 5秒间隔
.reduce((t1, t2) -> {
// 在会话窗口内求温度总和
return Tuple2.of(t1.f0, t1.f1 + t2.f1);
});
System.out.println("=== 会话窗口结果 ===");
sessionResult.print();
// ====== 4. 计数窗口(Count Window)======
// 每收到 5 条数据触发一次计算
DataStream<Tuple2<String, Double>> countResult = sensorData
.keyBy(tuple -> tuple.f0) // 按传感器ID分组
.countWindow(5) // 每5条数据一个窗口
.reduce((t1, t2) -> {
// 在计数窗口内求最小温度
return Tuple2.of(t1.f0, Math.min(t1.f1, t2.f1));
});
System.out.println("=== 计数窗口结果 ===");
countResult.print();
env.execute("Window Demo");
}
}
七、Flink 的优势
7.1 精确一次(Exactly-Once)语义
java
/**
* Flink Checkpoint 机制示例
* 说明:通过 Checkpoint 实现 Exactly-Once 语义
*/
import org.apache.flink.api.common.restartstrategy.RestartStrategies;
import org.apache.flink.api.common.time.Time;
import org.apache.flink.contrib.streaming.state.RocksDBStateBackend;
import org.apache.flink.runtime.state.filesystem.FileSystemStateBackend;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.environment.CheckpointConfig;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class CheckpointDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// ====== 1. 开启 Checkpoint 机制 ======
// 每隔 60 秒做一次 Checkpoint(生产环境建议 60-120 秒)
env.enableCheckpointing(60000);
// ====== 2. 设置 Checkpoint 模式 ======
// EXACTLY_ONCE:精确一次语义(默认)
// AT_LEAST_ONCE:至少一次语义
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// ====== 3. Checkpoint 超时时间 ======
// 如果 Checkpoint 在 60 秒内未完成,则放弃
env.getCheckpointConfig().setCheckpointTimeout(60000);
// ====== 4. 最大并发 Checkpoint 数 ======
// 同一时间只允许 1 个 Checkpoint 进行
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
// ====== 5. Checkpoint 之间的最小间隔 ======
// 确保两次 Checkpoint 之间至少间隔 30 秒
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000);
// ====== 6. Checkpoint 失败时是否阻止任务继续 ======
// true:Checkpoint 失败会导致整个任务失败
env.getCheckpointConfig().setFailOnCheckpointingErrors(true);
// ====== 7. 外部存储 Checkpoint ======
// 任务取消时保留 Checkpoint 数据(默认会删除)
env.getCheckpointConfig().enableExternalizedCheckpoints(
CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION
);
// ====== 8. 设置 Checkpoint 存储后端 ======
// 方式1:文件系统后端
env.setStateBackend(new FileSystemStateBackend("hdfs://namenode:9000/flink/checkpoints"));
// 方式2:RocksDB 后端(支持超大状态)
// env.setStateBackend(new RocksDBStateBackend("hdfs://namenode:9000/flink/checkpoints"));
// ====== 9. 设置重启策略 ======
// 固定延迟重启:最多重试 3 次,每次间隔 10 秒
env.setRestartStrategy(
RestartStrategies.fixedDelayRestart(
3, // 最大重试次数
Time.seconds(10) // 每次重试间隔
)
);
env.execute("Checkpoint Configuration Demo");
}
}
7.2 状态管理
java
/**
* Flink 状态管理完整示例
* 说明:Flink 提供了强大的状态管理机制,包括多种状态类型
*/
import org.apache.flink.api.common.functions.*;
import org.apache.flink.api.common.state.*;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
public class StateManagementDemo {
// ====== 示例1:ValueState(单值状态)======
// 应用场景:统计每个传感器的温度报警次数
public static class TemperatureAlertFunction
extends KeyedProcessFunction<String, Double, String> {
// 声明一个 ValueState,存储报警次数
// ValueState 存储单个值,是最常用的状态类型
private ValueState<Integer> alertCountState;
// 声明一个 ValueState,存储上一次温度
private ValueState<Double> lastTempState;
@Override
public void open(Configuration parameters) throws Exception {
// 初始化状态描述符
// ValueStateDescriptor 定义了状态的名称和类型
ValueStateDescriptor<Integer> countDescriptor =
new ValueStateDescriptor<>("alert-count", Integer.class, 0);
alertCountState = getRuntimeContext().getState(countDescriptor);
ValueStateDescriptor<Double> tempDescriptor =
new ValueStateDescriptor<>("last-temp", Double.class, 0.0);
lastTempState = getRuntimeContext().getState(tempDescriptor);
}
@Override
public void processElement(
Double currentTemp, // 当前输入的温度值
Context ctx, // 上下文对象
Collector<String> out) throws Exception { // 输出收集器
// 获取上一次的温度
Double lastTemp = lastTempState.value();
// 如果温度超过 100 度,触发报警
if (currentTemp > 100.0) {
// 更新报警次数
Integer count = alertCountState.value();
alertCountState.update(count + 1);
// 输出报警信息
String alert = String.format(
"传感器 %s 报警!当前温度: %.1f, 累计报警: %d 次",
ctx.getCurrentKey(), currentTemp, count + 1
);
out.collect(alert);
}
// 如果温度突然上升超过 20 度,触发温度突变报警
if (lastTemp != 0.0 && (currentTemp - lastTemp) > 20.0) {
out.collect(String.format(
"传感器 %s 温度突变!从 %.1f 上升到 %.1f",
ctx.getCurrentKey(), lastTemp, currentTemp
));
}
// 更新上一次温度状态
lastTempState.update(currentTemp);
}
}
// ====== 示例2:ListState(列表状态)======
// 应用场景:缓存最近 5 条温度记录
public static class RecentTemperaturesFunction
extends KeyedProcessFunction<String, Double, String> {
// ListState 存储一个列表,可以添加和遍历
private ListState<Double> recentTemps;
@Override
public void open(Configuration parameters) throws Exception {
ListStateDescriptor<Double> descriptor =
new ListStateDescriptor<>("recent-temps", Double.class);
recentTemps = getRuntimeContext().getListState(descriptor);
}
@Override
public void processElement(Double temp, Context ctx, Collector<String> out)
throws Exception {
// 将新温度添加到列表
recentTemps.add(temp);
// 获取最近的 5 条温度
List<Double> tempHistory = new ArrayList<>();
for (Double t : recentTemps.get()) {
tempHistory.add(t);
}
// 如果超过 5 条,只保留最近 5 条
if (tempHistory.size() > 5) {
tempHistory = tempHistory.subList(tempHistory.size() - 5, tempHistory.size());
// 清空后重新添加
recentTemps.update(tempHistory);
}
// 计算平均值
double avg = tempHistory.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
out.collect(String.format(
"传感器 %s | 当前温度: %.1f | 近%d次平均温度: %.1f",
ctx.getCurrentKey(), temp, tempHistory.size(), avg
));
}
}
// ====== 示例3:MapState(映射状态)======
// 应用场景:统计每个传感器每天的最高温度
public static class DailyMaxTempFunction
extends KeyedProcessFunction<String, Tuple2<String, Double>, String> {
// MapState 存储键值对映射
private MapState<String, Double> dailyMaxTemp;
@Override
public void open(Configuration parameters) throws Exception {
MapStateDescriptor<String, Double> descriptor =
new MapStateDescriptor<>("daily-max", String.class, Double.class);
dailyMaxTemp = getRuntimeContext().getMapState(descriptor);
}
@Override
public void processElement(
Tuple2<String, Double> input,
Context ctx,
Collector<String> out) throws Exception {
String date = input.f0; // 日期,如 "2024-01-15"
Double temp = input.f1; // 温度
// 更新每日最高温度
if (dailyMaxTemp.contains(date)) {
Double currentMax = dailyMaxTemp.get(date);
if (temp > currentMax) {
dailyMaxTemp.put(date, temp); // 更新最高温度
}
} else {
dailyMaxTemp.put(date, temp); // 首次记录
}
// 输出所有日期的最高温度
StringBuilder sb = new StringBuilder();
sb.append("传感器 ").append(ctx.getCurrentKey()).append(" 每日最高温度: ");
for (var entry : dailyMaxTemp.entries()) {
sb.append(String.format("[%s: %.1f] ", entry.getKey(), entry.getValue()));
}
out.collect(sb.toString());
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 构造测试数据流
DataStream<String> input = env.socketTextStream("localhost", 9999);
// 解析输入数据为 (sensorId, temperature)
DataStream<Tuple2<String, Double>> sensorData = input
.map(line -> {
String[] parts = line.split(",");
return Tuple2.of(parts[0].trim(), Double.parseDouble(parts[1].trim()));
})
.returns(Types.TUPLE(Types.STRING, Types.DOUBLE));
// 应用 ValueState 示例
DataStream<String> alerts = sensorData
.keyBy(t -> t.f0) // 按传感器ID分组
.process(new TemperatureAlertFunction()); // 自定义处理逻辑
alerts.print("报警信息");
env.execute("State Management Demo");
}
}
八、Flink 典型应用场景
8.1 事件驱动型应用
java
/**
* 事件驱动型应用示例:实时欺诈检测系统
* 说明:当检测到可疑交易时触发报警
*/
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
public class FraudDetectionApp {
/**
* 交易数据结构
* userId: 用户ID
* amount: 交易金额
* timestamp: 交易时间戳
*/
// 在实际场景中,这里应定义为 POJO 类
// 此处使用 Tuple3 简化表示
/**
* 欺诈检测处理函数
* 规则1:单笔交易超过 10000 元
* 规则2:5 分钟内累计交易超过 20000 元
* 规则3:5 分钟内交易次数超过 5 次
*/
public static class FraudDetector
extends KeyedProcessFunction<String, Tuple3<String, Double, Long>, String> {
// 累计交易金额状态
private ValueState<Double> totalAmountState;
// 累计交易次数状态
private ValueState<Integer> transactionCountState;
// 最近一次交易时间状态
private ValueState<Long> lastTransactionTimeState;
// 窗口大小:5分钟(毫秒)
private static final long WINDOW_SIZE = 5 * 60 * 1000;
// 单笔交易金额上限
private static final double SINGLE_TX_LIMIT = 10000.0;
// 累计金额上限
private static final double TOTAL_AMOUNT_LIMIT = 20000.0;
// 交易次数上限
private static final int TX_COUNT_LIMIT = 5;
@Override
public void open(Configuration parameters) throws Exception {
// 初始化各种状态
totalAmountState = getRuntimeContext().getState(
new ValueStateDescriptor<>("total-amount", Double.class, 0.0));
transactionCountState = getRuntimeContext().getState(
new ValueStateDescriptor<>("tx-count", Integer.class, 0));
lastTransactionTimeState = getRuntimeContext().getState(
new ValueStateDescriptor<>("last-tx-time", Long.class, 0L));
}
@Override
public void processElement(
Tuple3<String, Double, Long> tx, // 输入的交易数据
Context ctx, // 上下文
Collector<String> out) throws Exception {
String userId = tx.f0; // 用户ID
Double amount = tx.f1; // 交易金额
Long timestamp = tx.f2; // 交易时间
Long lastTime = lastTransactionTimeState.value();
// 判断是否超出 5 分钟窗口
if (lastTime != 0 && (timestamp - lastTime) > WINDOW_SIZE) {
// 超过窗口,重置计数和金额
totalAmountState.update(0.0);
transactionCountState.update(0);
}
// ====== 规则1:单笔交易超限 ======
if (amount > SINGLE_TX_LIMIT) {
out.collect(String.format(
"[高危报警] 用户 %s 单笔交易 %.2f 元超过限额 %.2f 元!",
userId, amount, SINGLE_TX_LIMIT));
}
// 更新累计金额
Double totalAmount = totalAmountState.value() + amount;
totalAmountState.update(totalAmount);
// 更新交易次数
Integer count = transactionCountState.value() + 1;
transactionCountState.update(count);
// 更新最后交易时间
lastTransactionTimeState.update(timestamp);
// ====== 规则2:累计金额超限 ======
if (totalAmount > TOTAL_AMOUNT_LIMIT) {
out.collect(String.format(
"[高危报警] 用户 %s 5分钟内累计交易 %.2f 元超过限额 %.2f 元!",
userId, totalAmount, TOTAL_AMOUNT_LIMIT));
}
// ====== 规则3:交易次数超限 ======
if (count > TX_COUNT_LIMIT) {
out.collect(String.format(
"[可疑报警] 用户 %s 5分钟内交易 %d 次超过上限 %d 次!",
userId, count, TX_COUNT_LIMIT));
}
// 注册一个定时器,在窗口结束时清理状态
long cleanupTime = timestamp + WINDOW_SIZE;
ctx.timerService().registerEventTimeTimer(cleanupTime);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out)
throws Exception {
// 定时器触发时,清理过期状态
totalAmountState.clear();
transactionCountState.clear();
lastTransactionTimeState.clear();
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 模拟交易数据流:(userId, amount, timestamp)
DataStream<String> input = env.socketTextStream("localhost", 9999);
DataStream<Tuple3<String, Double, Long>> transactions = input
.map(line -> {
String[] parts = line.split(",");
return Tuple3.of(
parts[0].trim(), // 用户ID
Double.parseDouble(parts[1].trim()), // 金额
Long.parseLong(parts[2].trim()) // 时间戳
);
})
.returns(Types.TUPLE(Types.STRING, Types.DOUBLE, Types.LONG));
// 应用欺诈检测逻辑
DataStream<String> alerts = transactions
.keyBy(tx -> tx.f0) // 按用户ID分组
.process(new FraudDetector()); // 自定义欺诈检测
// 输出报警信息
alerts.print("欺诈报警");
env.execute("Fraud Detection Application");
}
}
8.2 数据分析应用
java
/**
* 数据分析应用示例:实时电商数据分析
* 说明:实时统计商品销量 Top-N
*/
import org.apache.flink.api.common.functions.*;
import org.apache.flink.api.common.state.*;
import org.apache.flink.api.java.tuple.*;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.*;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.util.*;
public class RealtimeAnalyticsApp {
/**
* 窗口聚合函数:在每个时间窗口内统计商品销量
* 输出格式:(category, productId, salesCount, windowEnd)
*/
public static class SalesCountFunction
extends ProcessWindowFunction<
Tuple3<String, String, Integer>, // 输入:(类别, 商品ID, 数量)
Tuple4<String, String, Long, Long>, // 输出:(类别, 商品ID, 销量, 窗口结束时间)
String, // Key 类型
TimeWindow // 窗口类型
> {
@Override
public void process(
String key, // 分组键(类别)
Context context, // 窗口上下文
Iterable<Tuple3<String, String, Integer>> elements, // 窗口内的元素
Collector<Tuple4<String, String, Long, Long>> out // 输出收集器
) throws Exception {
// 统计每个商品的销量
Map<String, Long> productSales = new HashMap<>();
for (Tuple3<String, String, Integer> element : elements) {
String productId = element.f1; // 商品ID
Integer quantity = element.f2; // 购买数量
// 累加商品销量
productSales.merge(productId, quantity.longValue(), Long::sum);
}
// 获取窗口结束时间
long windowEnd = context.window().getEnd();
// 输出每个商品的销量
for (Map.Entry<String, Long> entry : productSales.entrySet()) {
out.collect(Tuple4.of(key, entry.getKey(), entry.getValue(), windowEnd));
}
}
}
/**
* Top-N 排序函数:对每个类别的商品按销量排序
*/
public static class TopNFunction
extends KeyedProcessFunction<String, Tuple4<String, String, Long, Long>, String> {
private final int topN; // 取前 N 名
// 存储每个类别的商品销量数据
private MapState<String, Long> salesState;
public TopNFunction(int topN) {
this.topN = topN;
}
@Override
public void open(Configuration parameters) throws Exception {
MapStateDescriptor<String, Long> descriptor =
new MapStateDescriptor<>("product-sales", String.class, Long.class);
salesState = getRuntimeContext().getMapState(descriptor);
}
@Override
public void processElement(
Tuple4<String, String, Long, Long> input,
Context ctx,
Collector<String> out) throws Exception {
// 更新状态中的商品销量
salesState.put(input.f1, input.f2);
// 注册定时器,在窗口结束后输出 Top-N
ctx.timerService().registerEventTimeTimer(input.f3 + 1);
// 存储窗口结束时间用于后续输出
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out)
throws Exception {
// 收集所有商品的销量
List<Tuple2<String, Long>> salesList = new ArrayList<>();
for (var entry : salesState.entries()) {
salesList.add(Tuple2.of(entry.getKey(), entry.getValue()));
}
// 按销量降序排序
salesList.sort((a, b) -> Long.compare(b.f1, a.f1));
// 输出 Top-N
StringBuilder result = new StringBuilder();
result.append(String.format("\n=== %s 品类 Top-%d 商品 ===\n", ctx.getCurrentKey(), topN));
for (int i = 0; i < Math.min(topN, salesList.size()); i++) {
result.append(String.format(" 第%d名: 商品[%s], 销量: %d\n",
i + 1, salesList.get(i).f0, salesList.get(i).f1));
}
out.collect(result.toString());
// 清理状态
salesState.clear();
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 模拟电商订单流:(category, productId, quantity)
DataStream<Tuple3<String, String, Integer>> orderStream = env.socketTextStream("localhost", 9999)
.map(line -> {
String[] parts = line.split(",");
return Tuple3.of(
parts[0].trim(), // 商品类别
parts[1].trim(), // 商品ID
Integer.parseInt(parts[2].trim()) // 购买数量
);
})
.returns(Types.TUPLE(Types.STRING, Types.STRING, Types.INT));
// 按类别分组,5分钟窗口统计销量
DataStream<Tuple4<String, String, Long, Long>> categorySales = orderStream
.keyBy(t -> t.f0) // 按类别分组
.timeWindow(Time.minutes(5)) // 5分钟滚动窗口
.process(new SalesCountFunction()); // 窗口聚合
// 计算每个类别的 Top-3 商品
DataStream<String> topNResult = categorySales
.keyBy(t -> t.f0) // 按类别分组
.process(new TopNFunction(3)); // Top-3
topNResult.print("分析结果");
env.execute("Realtime E-commerce Analytics");
}
}
8.3 数据流水线应用
java
/**
* 数据流水线应用示例:ETL 数据清洗与转换
* 说明:从 Kafka 读取原始日志,清洗后写入另一个 Kafka 主题
*/
import org.apache.flink.api.common.serialization.*;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.FilterFunction;
import org.apache.flink.connector.kafka.source.KafkaSource;
import org.apache.flink.connector.kafka.sink.KafkaSink;
import org.apache.flink.connector.kafka.sink.KafkaRecordSerializationSchema;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.api.common.typeinfo.TypeInformation;
public class DataPipelineApp {
/**
* 日志清洗函数
* 从原始日志中提取有效字段,清洗数据
*/
public static class LogCleaner implements MapFunction<String, String> {
@Override
public String map(String rawLog) throws Exception {
try {
// 原始日志格式:时间戳|日志级别|服务名|消息内容
String[] parts = rawLog.split("\\|");
if (parts.length < 4) {
return null; // 格式不正确的日志过滤掉
}
String timestamp = parts[0].trim();
String level = parts[1].trim();
String service = parts[2].trim();
String message = parts[3].trim();
// 转换为标准 JSON 格式
return String.format(
"{\"timestamp\":\"%s\",\"level\":\"%s\",\"service\":\"%s\",\"message\":\"%s\"}",
timestamp, level, service, message
);
} catch (Exception e) {
return null; // 解析失败返回 null
}
}
}
/**
* 日志过滤函数
* 只保留 ERROR 和 WARN 级别的日志
*/
public static class ErrorLogFilter implements FilterFunction<String> {
@Override
public boolean filter(String logJson) throws Exception {
if (logJson == null) return false;
// 只保留包含 ERROR 或 WARN 的日志
return logJson.contains("\"level\":\"ERROR\"") ||
logJson.contains("\"level\":\"WARN\"");
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// ====== 1. 数据源:从 Kafka 读取原始日志 ======
KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
.setBootstrapServers("kafka1:9092,kafka2:9092") // Kafka broker 地址
.setTopics("raw-logs") // 源主题
.setGroupId("flink-etl-group") // 消费者组
.setValueOnlyDeserializer(new SimpleStringSchema()) // 反序列化器
.build();
// 创建 Kafka 数据流
DataStream<String> rawLogStream = env.fromSource(
kafkaSource,
org.apache.flink.api.common.eventtime.WatermarkStrategy.noWatermarks(),
"Kafka Raw Logs Source"
);
// ====== 2. 数据清洗:格式化日志 ======
DataStream<String> cleanedStream = rawLogStream
.map(new LogCleaner()) // 清洗和格式化
.filter(log -> log != null); // 过滤无效数据
// ====== 3. 数据过滤:只保留错误和警告日志 ======
DataStream<String> errorStream = cleanedStream
.filter(new ErrorLogFilter()); // 过滤错误级别日志
// ====== 4. 数据输出:写入 Kafka 目标主题 ======
KafkaSink<String> kafkaSink = KafkaSink.<String>builder()
.setBootstrapServers("kafka1:9092,kafka2:9092") // Kafka broker 地址
.setRecordSerializer(
KafkaRecordSerializationSchema.builder()
.setTopic("cleaned-error-logs") // 目标主题
.setValueSerializationSchema(new SimpleStringSchema()) // 序列化器
.build()
)
.build();
// 将清洗后的错误日志写入 Kafka
errorStream.sinkTo(kafkaSink);
// ====== 5. 同时也打印到控制台用于调试 ======
errorStream.print("错误日志");
env.execute("Data Pipeline ETL Application");
}
}
九、Flink 核心组件栈
9.1 Flink 组件栈层次图
┌─────────────────────────────────────────────────────────────────┐
│ Flink 上层 API │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Flink SQL │ │ Table API │ │ CEP (复杂事件处理) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Flink 核心 API │
│ ┌──────────────────────────┐ ┌────────────────────────────┐ │
│ │ DataStream API (流) │ │ DataSet API (批/已废弃) │ │
│ └──────────────────────────┘ └────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ 运行时核心层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Task执行 │ │ 状态管理 │ │ 内存管理 │ │ Checkpoint │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ 部署层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Standalone │ │ YARN │ │ Kubernetes│ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
十、Flink 体系架构
10.1 Flink 集群架构
┌───────────────────────────────────────────────────────────────────┐
│ Flink Cluster │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ JobManager (作业管理器) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ ResourceManager│ │ Dispatcher │ │ JobMaster (每个作业) │ │ │
│ │ │ (资源管理) │ │ (任务分发) │ │ (作业调度与协调) │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────┴──────────┐ │
│ │ TaskManager │ │
│ │ (任务管理器) │ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ Task Slot│ │ Task Slot│ │ Task Slot│ │ Task Slot│ │ Task Slot│ │ │
│ │ │ (任务槽) │ │ (任务槽) │ │ (任务槽) │ │ (任务槽) │ │ (任务槽) │ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ Task Slot│ │ Task Slot│ │ Task Slot│ │ Task Slot│ │ Task Slot│ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ │ TaskManager 2 │ │
│ └──────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
10.2 架构组件详解代码
java
/**
* Flink 架构组件理解示例
* 说明:通过代码展示 Flink 的 Task Chain 和 Slot Sharing 机制
*/
import org.apache.flink.streaming.api.datastream.*;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.configuration.Configuration;
public class ArchitectureDemo {
/**
* 自定义并行数据源
* 模拟从外部系统读取数据
*/
public static class CustomSource extends RichParallelSourceFunction<String> {
private volatile boolean isRunning = true; // 控制数据生成的开关
@Override
public void run(SourceContext<String> ctx) throws Exception {
int subtaskIndex = getRuntimeContext().getIndexOfThisSubtask();
while (isRunning) {
// 模拟生成数据,每个并行实例生成不同的数据
ctx.collect("Source-" + subtaskIndex + ": data-" + System.currentTimeMillis());
Thread.sleep(1000); // 每秒生成一条数据
}
}
@Override
public void cancel() {
isRunning = false; // 停止数据生成
}
}
/**
* RichMapFunction:带生命周期管理的 Map 函数
* 展示 Flink 算子的生命周期方法
*/
public static class LifecycleMap extends RichMapFunction<String, String> {
private String taskName;
@Override
public void open(Configuration parameters) throws Exception {
// ====== open():算子初始化时调用一次 ======
// 适合做:初始化连接、初始化状态等
taskName = getRuntimeContext().getTaskNameWithSubtasks();
System.out.println("[初始化] " + taskName + " 算子已打开");
}
@Override
public String map(String value) throws Exception {
// ====== 每条数据都会调用一次 ======
return value.toUpperCase();
}
@Override
public void close() throws Exception {
// ====== close():算子关闭时调用一次 ======
// 适合做:关闭连接、清理资源等
System.out.println("[关闭] " + taskName + " 算子已关闭");
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 设置全局并行度为 2
env.setParallelism(2);
// ====== 创建数据源(2个并行实例)======
DataStream<String> source = env.addSource(new CustomSource())
.name("Custom Source") // 设置算子名称
.setParallelism(2); // 设置并行度
// ====== Map 转换(2个并行实例)======
DataStream<String> mapped = source
.map(new LifecycleMap())
.name("Lifecycle Map")
.setParallelism(2);
// ====== FlatMap 转换(2个并行实例)======
DataStream<String> result = mapped
.flatMap(new RichFlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
// 将字符串拆分为单词
String[] words = value.split("[-:]+");
for (String word : words) {
out.collect(word.trim());
}
}
})
.name("Word Splitter")
.setParallelism(2);
// 输出结果
result.print("Result");
// 执行任务
env.execute("Architecture Demo");
}
}
十一、Flink 编程模型
11.1 DataStream API 完整示例
java
/**
* Flink DataStream API 全面编程模型示例
* 说明:涵盖 Flink DataStream API 的所有核心概念
*/
import org.apache.flink.api.common.functions.*;
import org.apache.flink.api.java.tuple.*;
import org.apache.flink.streaming.api.datastream.*;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.ProcessJoinFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;
public class DataStreamAPIDemo {
public static void main(String[] args) throws Exception {
// ====== 第一步:获取执行环境 ======
// 方式1:获取流处理环境(最常用)
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 设置并行度
env.setParallelism(1);
// ====== 第二步:定义数据源(Source)======
// 方式1:从集合创建(开发测试用)
DataStream<String> collectionSource = env.fromCollection(
Arrays.asList("hello flink", "hello world", "flink is great")
);
// 方式2:从元素创建
DataStream<String> elementSource = env.fromElements("apple", "banana", "cherry");
// 方式3:从 Socket 读取(实时调试用)
DataStream<String> socketSource = env.socketTextStream("localhost", 9999);
// 方式4:生成序列(测试用)
DataStream<Long> sequenceSource = env.generateSequence(1, 100);
// 使用集合作为数据源
DataStream<String> textStream = collectionSource;
// ====== 第三步:转换操作(Transformation)======
// ---- 3.1 map:一对一转换 ----
// 将每个字符串转为大写
DataStream<String> upperStream = textStream.map(
new MapFunction<String, String>() {
@Override
public String map(String value) throws Exception {
return value.toUpperCase(); // 转换为大写
}
}
);
upperStream.print("map结果");
// ---- 3.2 flatMap:一对多转换 ----
// 将每行文本拆分为单词
DataStream<String> wordStream = textStream.flatMap(
new FlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
// 按空格拆分
String[] words = value.split("\\s+");
for (String word : words) {
out.collect(word); // 输出每个单词
}
}
}
);
wordStream.print("flatMap结果");
// ---- 3.3 filter:过滤 ----
// 只保留包含 "flink" 的字符串
DataStream<String> filteredStream = textStream.filter(
new FilterFunction<String>() {
@Override
public boolean filter(String value) throws Exception {
return value.contains("flink"); // 过滤条件
}
}
);
filteredStream.print("filter结果");
// ---- 3.4 keyBy:分组 ----
// 将单词按单词内容分组,统计每个单词出现次数
DataStream<Tuple2<String, Integer>> wordCount = wordStream
.map(word -> Tuple2.of(word, 1)) // 映射为 (word, 1)
.returns(org.apache.flink.api.common.typeinfo.Types.TUPLE(
org.apache.flink.api.common.typeinfo.Types.STRING,
org.apache.flink.api.common.typeinfo.Types.INT))
.keyBy(tuple -> tuple.f0) // 按单词分组
.sum(1); // 对计数求和
wordCount.print("keyBy结果");
// ---- 3.5 reduce:聚合 ----
// 使用 reduce 实现更复杂的聚合逻辑
DataStream<Tuple2<String, Integer>> reducedCount = wordStream
.map(word -> Tuple2.of(word, 1))
.returns(org.apache.flink.api.common.typeinfo.Types.TUPLE(
org.apache.flink.api.common.typeinfo.Types.STRING,
org.apache.flink.api.common.typeinfo.Types.INT))
.keyBy(tuple -> tuple.f0)
.reduce(new ReduceFunction<Tuple2<String, Integer>>() {
@Override
public Tuple2<String, Integer> reduce(
Tuple2<String, Integer> value1, // 累积值
Tuple2<String, Integer> value2 // 新来的值
) throws Exception {
// 将两个值的计数相加
return Tuple2.of(value1.f0, value1.f1 + value2.f1);
}
});
reducedCount.print("reduce结果");
// ---- 3.6 union:合并流 ----
// 将两个流合并为一个流(两个流的类型必须相同)
DataStream<String> stream1 = env.fromElements("A1", "A2", "A3");
DataStream<String> stream2 = env.fromElements("B1", "B2", "B3");
DataStream<String> unionStream = stream1.union(stream2); // 合并两个流
unionStream.print("union结果");
// ---- 3.7 connect:连接两个不同类型的流 ----
// connect 可以连接两个不同类型的流,然后使用 CoProcessFunction 处理
DataStream<String> nameStream = env.fromElements("Alice", "Bob", "Charlie");
DataStream<Integer> scoreStream = env.fromElements(90, 85, 95);
ConnectedStreams<String, Integer> connectedStream = nameStream.connect(scoreStream);
DataStream<String> coResult = connectedStream.map(
new CoMapFunction<String, Integer, String>() {
@Override
public String map1(String name) throws Exception {
return "Name: " + name; // 处理第一个流的数据
}
@Override
public String map2(Integer score) throws Exception {
return "Score: " + score; // 处理第二个流的数据
}
}
);
coResult.print("connect结果");
// ====== 第四步:输出(Sink)======
// 方式1:print ------ 输出到标准输出
// 方式2:writeAsText ------ 写入文本文件
// 方式3:writeAsCsv ------ 写入 CSV 文件
// 方式4:addSink ------ 自定义 Sink
// writeAsText 示例
// wordCount.writeAsText("/tmp/wordcount_output");
// 自定义 Sink 示例
wordCount.addSink(
new org.apache.flink.streaming.api.functions.sink.SinkFunction<Tuple2<String, Integer>>() {
@Override
public void invoke(Tuple2<String, Integer> value, Context context) throws Exception {
// 自定义输出逻辑
System.out.println("[自定义Sink] " + value.f0 + " = " + value.f1);
}
}
);
// ====== 第五步:触发执行 ======
env.execute("DataStream API Complete Demo");
}
}
11.2 Lambda 表达式简化编程
java
/**
* 使用 Lambda 表达式简化 Flink 编程
* 说明:Flink 支持 Lambda 表达式,可以大幅简化代码
*/
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class LambdaStyleDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 准备数据
DataStream<String> text = env.fromElements(
"Apache Flink is a framework",
"Flink supports stream processing",
"Batch processing is also supported"
);
// ====== 使用 Lambda 表达式的 WordCount ======
// 注意:Lambda 表达式需要使用 .returns() 显式指定返回类型
// 因为 Java 的类型擦除机制会导致 Flink 无法推断泛型类型
DataStream<Tuple2<String, Integer>> wordCounts = text
// 第1步:flatMap - 拆分单词
.flatMap((String line, org.apache.flink.util.Collector<String> out) -> {
for (String word : line.split("\\s+")) {
out.collect(word.toLowerCase()); // 转小写后输出
}
})
.returns(Types.STRING) // 【重要】必须指定返回类型
// 第2步:map - 映射为 (word, 1)
.map(word -> Tuple2.of(word, 1))
.returns(Types.TUPLE(Types.STRING, Types.INT)) // 【重要】指定元组类型
// 第3步:keyBy - 按单词分组
.keyBy(tuple -> tuple.f0)
// 第4步:sum - 累加计数
.sum(1);
// 输出结果
wordCounts.print("WordCount");
env.execute("Lambda Style WordCount");
}
}
十二、Flink 编程实践
12.1 安装 Flink(详细步骤)
12.1.1 环境准备
bash
# ============================================================
# Flink 安装部署详细步骤
# ============================================================
# ====== 第一步:检查环境要求 ======
# 检查 Java 版本(Flink 1.14+ 需要 Java 8 或 Java 11)
java -version
# 预期输出:
# openjdk version "1.8.0_xxx" 或 "11.0.x"
# 检查是否安装了 Java(如果没有需要先安装)
sudo apt-get install openjdk-8-jdk # Ubuntu/Debian
# 或
sudo yum install java-1.8.0-openjdk-devel # CentOS/RHEL
# 设置 JAVA_HOME 环境变量
echo 'export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64' >> ~/.bashrc
echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
# 验证 JAVA_HOME
echo $JAVA_HOME
12.1.2 下载与解压 Flink
bash
# ====== 第二步:下载 Flink 安装包 ======
# 从 Apache 官方镜像下载(以 Flink 1.17.2 为例)
cd /opt
# 使用 wget 下载
wget https://archive.apache.org/dist/flink/flink-1.17.2/flink-1.17.2-bin-scala_2.12.tgz
# 或使用 curl 下载
# curl -O https://archive.apache.org/dist/flink/flink-1.17.2/flink-1.17.2-bin-scala_2.12.tgz
# ====== 第三步:解压安装包 ======
tar -zxvf flink-1.17.2-bin-scala_2.12.tgz
# 创建软链接,方便后续版本升级
ln -s flink-1.17.2 flink
# ====== 第四步:设置环境变量 ======
echo 'export FLINK_HOME=/opt/flink' >> ~/.bashrc
echo 'export PATH=$FLINK_HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
# 验证环境变量
echo $FLINK_HOME
# 输出:/opt/flink
12.1.3 配置 Flink(Standalone 模式)
bash
# ====== 第五步:配置 Flink ======
# 进入配置目录
cd /opt/flink/conf/
# ---- 5.1 配置 flink-conf.yaml ----
# 备份原始配置文件
cp flink-conf.yaml flink-conf.yaml.bak
# 编辑配置文件
vim flink-conf.yaml
flink-conf.yaml 关键配置项:
yaml
# ============================================================
# Flink 核心配置文件 flink-conf.yaml 详细说明
# ============================================================
# ----- JobManager 配置 -----
# JobManager 的 RPC 地址
jobmanager.rpc.address: localhost
# JobManager 的 RPC 端口(默认 6123)
jobmanager.rpc.port: 6123
# JobManager 总内存(根据机器配置调整)
# 生产环境建议 4G-8G
jobmanager.memory.process.size: 1600m
# JobManager JVM 堆内存
# jobmanager.memory.heap.size: 1024m
# ----- TaskManager 配置 -----
# TaskManager 总内存(根据机器配置调整)
# 生产环境建议 8G-16G
taskmanager.memory.process.size: 1728m
# TaskManager JVM 堆内存
# taskmanager.memory.heap.size: 1024m
# TaskManager 管理内存(用于排序、缓存等)
# taskmanager.memory.managed.size: 512m
# TaskManager 网络内存(用于数据交换缓冲)
# taskmanager.memory.network.fraction: 0.1
# TaskManager 的 Task Slot 数量
# 每个 TaskManager 的 Slot 数,一般设置为 CPU 核数
taskmanager.numberOfTaskSlots: 4
# ----- 并行度配置 -----
# 默认并行度(所有算子的默认并行度)
parallelism.default: 1
# 最大并行度(决定算子可以扩展的最大并行度)
# 默认值 128,通常设置为 parallelism 的倍数
# io.operator.max-parallelism: 128
# ----- 状态后端配置 -----
# 状态后端:用于存储和检查点的状态
# 可选值:hashmap, rocksdb
state.backend: hashmap
# Checkpoint 存储目录
state.checkpoints.dir: file:///opt/flink/checkpoints
# 保存点存储目录
state.savepoints.dir: file:///opt/flink/savepoints
# ----- Checkpoint 配置 -----
# Checkpoint 间隔(毫秒)
execution.checkpointing.interval: 60000
# Checkpoint 模式:EXACTLY_ONCE 或 AT_LEAST_ONCE
execution.checkpointing.mode: EXACTLY_ONCE
# Checkpoint 超时时间
execution.checkpointing.timeout: 60000
# ----- 高可用配置(可选)-----
# 使用 ZooKeeper 实现高可用
# high-availability: zookeeper
# high-availability.zookeeper.quorum: zk1:2181,zk2:2181,zk3:2181
# high-availability.zookeeper.path.root: /flink
# high-availability.storageDir: hdfs:///flink/ha/
# ----- Web UI 配置 -----
# Web UI 端口(默认 8081)
rest.port: 8081
# Web UI 绑定地址
rest.address: 0.0.0.0
bash
# ---- 5.2 配置 workers 文件 ----
# 定义 TaskManager 节点列表
vim workers
workers 文件内容:
# 每行一个 TaskManager 节点的主机名或 IP 地址
# 单机模式只有一台
worker1
worker2
worker3
bash
# ---- 5.3 配置 masters 文件 ----
# 定义 JobManager 节点
vim masters
masters 文件内容:
# JobManager 地址:端口
jobmanager:8081
12.1.4 启动与停止 Flink
bash
# ====== 第六步:启动 Flink 集群 ======
# 方式1:启动 Standalone 集群
/opt/flink/bin/start-cluster.sh
# 预期输出:
# Starting standalonesession daemon on host xxx.
# Starting taskexecutor daemon on host xxx.
# 方式2:单独启动 JobManager
/opt/flink/bin/jobmanager.sh start
# 方式3:单独启动 TaskManager
/opt/flink/bin/taskmanager.sh start
# ====== 第七步:验证 Flink 是否启动成功 ======
# 检查进程
jps | grep -i flink
# 预期输出应包含:
# StandaloneSessionClusterEntrypoint (JobManager)
# TaskManagerRunner (TaskManager)
# 检查 Web UI
# 在浏览器中访问:http://localhost:8081
# 可以看到 Flink Dashboard
# 检查日志(如有问题查看日志排查)
tail -f /opt/flink/log/flink-*-standalonesession-*.log
# ====== 提交一个测试任务 ======
# 运行 Flink 自带的 WordCount 示例
/opt/flink/bin/flink run /opt/flink/examples/batch/WordCount.jar
# 查看任务执行结果
cat /opt/flink/log/flink-*-taskexecutor-*.out
# ====== 停止 Flink 集群 ======
/opt/flink/bin/stop-cluster.sh
12.1.5 Flink 常用命令
bash
# ============================================================
# Flink 命令行工具详解
# ============================================================
# ---- 1. 提交任务 ----
# 提交 JAR 包运行
flink run -c com.example.MainClass /path/to/application.jar
# 提交任务并指定并行度
flink run -p 4 -c com.example.MainClass /path/to/application.jar
# 提交任务并传入参数
flink run -c com.example.MainClass /path/to/application.jar --input /data/input --output /data/output
# 提交任务到指定 JobManager
flink run -m jobmanager:6123 -c com.example.MainClass /path/to/application.jar
# ---- 2. 查看任务列表 ----
# 列出正在运行和等待中的任务
flink list
# 列出所有任务(包括已完成和失败的)
flink list -a
# ---- 3. 取消任务 ----
# 取消指定任务(任务ID可以通过 list 获取)
flink cancel <jobId>
# 取消任务并创建保存点
flink cancel -s /opt/flink/savepoints <jobId>
# ---- 4. 保存点操作 ----
# 创建保存点
flink savepoint <jobId> /opt/flink/savepoints
# 从保存点恢复任务
flink run -s /opt/flink/savepoints/savepoint-xxxxx -c com.example.MainClass /path/to/application.jar
# 删除保存点
flink savepoint -d /opt/flink/savepoints/savepoint-xxxxx
# ---- 5. 任务信息 ----
# 查看任务详情
flink info -c com.example.MainClass /path/to/application.jar
# ---- 6. 触发检查点 ----
# 手动触发检查点
flink triggerpoint <jobId>
12.1.6 YARN 模式部署
bash
# ============================================================
# Flink on YARN 部署方式
# ============================================================
# ---- 前提条件 ----
# 1. 已安装 Hadoop(YARN + HDFS)
# 2. 环境变量已设置 HADOOP_HOME 和 HADOOP_CONF_DIR
echo 'export HADOOP_HOME=/opt/hadoop' >> ~/.bashrc
echo 'export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop' >> ~/.bashrc
echo 'export PATH=$HADOOP_HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
# ---- 方式1:Session 模式(共享集群)----
# 先启动一个 YARN Session,然后可以提交多个任务
# 启动 YARN Session(2个 TaskManager,每个4个 Slot)
flink run \
-m yarn-cluster \
-yn 2 \ # TaskManager 数量
-ys 4 \ # 每个 TaskManager 的 Slot 数
-yjm 1024 \ # JobManager 内存(MB)
-ytm 2048 \ # 每个 TaskManager 内存(MB)
-c com.example.MainClass \
/path/to/application.jar
# 或者使用 yarn-session.sh 单独启动 Session
/opt/flink/bin/yarn-session.sh \
-n 2 \ # TaskManager 数量
-s 4 \ # 每个 TaskManager 的 Slot 数
-jm 1024 \ # JobManager 内存(MB)
-tm 2048 \ # 每个 TaskManager 内存(MB)
-d # 后台运行
# 然后提交任务到已有的 Session
flink run -c com.example.MainClass /path/to/application.jar
# ---- 方式2:Per-Job 模式(每个任务独立集群)----
# 每个任务单独申请 YARN 资源
flink run \
-m yarn-cluster \ # 使用 YARN 集群模式
-yn 3 \ # TaskManager 数量
-ys 4 \ # 每个 TaskManager 的 Slot 数
-yjm 2048 \ # JobManager 内存
-ytm 4096 \ # 每个 TaskManager 内存
-c com.example.MainClass \
/path/to/application.jar
# ---- 方式3:Application 模式(推荐,Flink 1.11+)----
# 应用级别的资源隔离
flink run-application \
-t yarn-application \
-Dyarn.application.name="MyFlinkApp" \
-Djobmanager.memory.process.size=2048m \
-Dtaskmanager.memory.process.size=4096m \
-Dtaskmanager.numberOfTaskSlots=4 \
-Dparallelism.default=8 \
-c com.example.MainClass \
/path/to/application.jar
# ---- YARN 相关命令 ----
# 查看 YARN 上的 Flink 应用
yarn application -list | grep flink
# 杀死 YARN 上的 Flink 应用
yarn application -kill application_xxxxx_xxxxx
12.2 编程实现 WordCount 程序
12.2.1 批处理 WordCount(DataSet API)
java
/**
* Flink 批处理 WordCount 程序
* 说明:使用 DataSet API 实现经典的词频统计
* DataSet API 在 Flink 1.12+ 已标记为废弃,建议使用 Table API
* 但作为学习入门仍然推荐先理解
*
* 数据流图:
* 文本数据 → FlatMap(拆分单词) → Map(映射为KV对) → ReduceBy(分组聚合) → 输出结果
*/
package com.example.flink;
import org.apache.flink.api.common.functions.*;
import org.apache.flink.api.java.DataSet;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.util.Collector;
public class BatchWordCount {
public static void main(String[] args) throws Exception {
// ====== 1. 创建批处理执行环境 ======
// ExecutionEnvironment 是批处理程序的入口点
// 相当于 Spark 的 SparkContext
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment();
// ====== 2. 读取文本数据 ======
// 从文件读取文本数据,每行作为一个元素
DataSet<String> text = env.readTextFile("input/words.txt");
// ====== 3. 数据转换处理 ======
// 3.1 FlatMap:将每行文本拆分为单词
// FlatMapFunction<输入类型, 输出类型>
DataSet<String> words = text.flatMap(
new FlatMapFunction<String, String>() {
/**
* flatMap 方法:对每个输入元素,可以输出零个、一个或多个元素
* @param value 输入的文本行
* @param out 收集器,用于输出拆分后的单词
*/
@Override
public void flatMap(String value, Collector<String> out) throws Exception {
// 使用正则表达式按非字母字符拆分
String[] wordArray = value.toLowerCase().split("\\W+");
// 将每个非空单词输出到收集器
for (String word : wordArray) {
if (word.length() > 0) {
out.collect(word); // 输出一个单词
}
}
}
}
);
// 3.2 Map:将每个单词映射为 (word, 1) 的二元组
// MapFunction<输入类型, 输出类型>
DataSet<Tuple2<String, Integer>> wordPairs = words.map(
new MapFunction<String, Tuple2<String, Integer>>() {
/**
* map 方法:一对一转换
* @param value 输入的单词
* @return (单词, 1) 的元组
*/
@Override
public Tuple2<String, Integer> map(String value) throws Exception {
// 每个单词初始计数为 1
return new Tuple2<>(value, 1);
}
}
);
// 3.3 ReduceBy:按单词分组并累加计数
// keyBy(0) 表示按第一个字段(即单词)分组
// sum(1) 表示对第二个字段(即计数)求和
DataSet<Tuple2<String, Integer>> wordCounts = wordPairs
.groupBy(0) // 按第一个字段(单词)分组
.sum(1); // 对第二个字段(计数)求和
// ====== 4. 输出结果 ======
// 方式1:打印到控制台
wordCounts.print();
// 方式2:写入文件
// wordCounts.writeAsText("output/wordcount_result");
// 方式3:写入 CSV 文件
// wordCounts.writeAsCsv("output/wordcount.csv", "\n", ",");
// 注意:批处理程序不需要显式调用 execute()
// print() 等操作会自动触发执行
}
}
12.2.2 流处理 WordCount(DataStream API)
java
/**
* Flink 流处理 WordCount 程序(完整注释版)
* 说明:使用 DataStream API 实现实时词频统计
* 这是 Flink 最核心、最常用的 API
*
* 数据流图:
* Socket数据流 → FlatMap(拆分单词) → Map(映射为KV对) →
* KeyBy(按单词分组) → Sum(累加计数) → Print(输出结果)
*
* 使用方法:
* 1. 先在终端启动 Socket:nc -lk 9999
* 2. 然后运行本程序
* 3. 在 Socket 终端输入文本,观察程序输出
*/
package com.example.flink;
import org.apache.flink.api.common.functions.*;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class StreamingWordCount {
public static void main(String[] args) throws Exception {
// ============================================================
// 第一步:获取流处理执行环境
// ============================================================
// StreamExecutionEnvironment 是所有 Flink 流处理程序的入口
// 它提供了配置并行度、设置时间特性、添加数据源等方法
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 设置程序的全局并行度
// 并行度决定了同时并行执行的 Task 数量
// 开发测试设为 1,生产环境根据集群资源调整
env.setParallelism(1);
// ============================================================
// 第二步:定义数据源(Source)
// ============================================================
// 从 Socket 读取文本数据流
// 参数1:主机名
// 参数2:端口号
// 需要先在终端执行: nc -lk 9999
DataStream<String> textStream = env.socketTextStream("localhost", 9999);
// 也可以使用集合数据源进行测试:
// DataStream<String> textStream = env.fromElements(
// "hello flink hello world",
// "flink streaming word count",
// "hello apache flink"
// );
// ============================================================
// 第三步:数据转换(Transformation)
// ============================================================
// ---- 3.1 FlatMap: 将每行文本拆分为单词 ----
// FlatMapFunction<输入类型, 输出类型>
// 输入:String(一行文本)
// 输出:String(单个单词)
DataStream<String> words = textStream.flatMap(
new FlatMapFunction<String, String>() {
/**
* flatMap - 一对多转换
* 对于输入的一行文本,可以拆分出多个单词
*
* @param line 输入的一行文本
* @param out 输出收集器,可以输出零个或多个结果
* @throws Exception
*/
@Override
public void flatMap(String line, Collector<String> out) throws Exception {
// 将文本按一个或多个空格拆分
String[] wordArray = line.trim().split("\\s+");
// 遍历拆分结果,将每个非空单词输出
for (String word : wordArray) {
// 转为小写并去除首尾空格
String cleanedWord = word.toLowerCase().trim();
// 只输出非空单词
if (!cleanedWord.isEmpty()) {
out.collect(cleanedWord); // 将单词发送到下游
}
}
}
}
);
// ---- 3.2 Map: 将每个单词转换为 (word, 1) 的二元组 ----
// MapFunction<输入类型, 输出类型>
// 输入:String(单个单词)
// 输出:Tuple2<String, Integer>(单词-计数对)
DataStream<Tuple2<String, Integer>> wordPairs = words.map(
new MapFunction<String, Tuple2<String, Integer>>() {
/**
* map - 一对一转换
* 将每个单词包装成 (单词, 1) 的元组
*
* @param word 输入的单词
* @return (单词, 1) 的二元组
*/
@Override
public Tuple2<String, Integer> map(String word) throws Exception {
// Tuple2 是 Flink 提供的二元组类型
// f0 是第一个字段(单词),f1 是第二个字段(计数)
return new Tuple2<>(word, 1);
}
}
);
// ---- 3.3 KeyBy: 按单词分组 ----
// keyBy 会将相同 key 的数据路由到同一个 Task 进行处理
// 注意:keyBy 之后才能使用 reduce、sum 等聚合操作
DataStream<Tuple2<String, Integer>> keyedStream = wordPairs
.keyBy(
new KeySelector<Tuple2<String, Integer>, String>() {
/**
* getKey - 定义分组键
* 从输入数据中提取用于分组的 key
*
* @param tuple 输入的二元组
* @return 分组键(单词)
*/
@Override
public String getKey(Tuple2<String, Integer> tuple) throws Exception {
return tuple.f0; // 使用单词作为分组键
}
}
);
// ---- 3.4 Sum: 按分组累加计数 ----
// sum(1) 表示对索引为 1 的字段(即计数字段)进行累加
// 每当有新数据到来,会和之前的结果累加
DataStream<Tuple2<String, Integer>> wordCounts = keyedStream.sum(1);
// ============================================================
// 第四步:输出结果(Sink)
// ============================================================
// print() 会将结果输出到标准输出(控制台)
// 在生产环境中,通常输出到 Kafka、数据库等
wordCounts.print("词频统计结果");
// ============================================================
// 第五步:触发程序执行
// ============================================================
// execute() 方法会触发程序的实际执行
// Flink 是懒执行的,只有调用 execute() 才会真正开始
// 参数是作业名称,会显示在 Flink Web UI 上
env.execute("Streaming WordCount Demo");
}
}
12.2.3 带窗口的流处理 WordCount
java
/**
* 带时间窗口的流处理 WordCount 程序
* 说明:在指定的时间窗口内统计词频
* 窗口关闭时输出该窗口内的统计结果
*/
package com.example.flink;
import org.apache.flink.api.common.functions.*;
import org.apache.flink.api.java.tuple.*;
import org.apache.flink.streaming.api.datastream.*;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.assigners.*;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;
public class WindowedWordCount {
public static void main(String[] args) throws Exception {
// 获取执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 从 Socket 读取数据
DataStream<String> source = env.socketTextStream("localhost", 9999);
// 第1步:拆分单词并映射为 (word, 1)
DataStream<Tuple2<String, Integer>> wordCounts = source
// flatMap: 拆分每行文本为单词
.flatMap((String line, Collector<Tuple2<String, Integer>> out) -> {
for (String word : line.split("\\s+")) {
if (!word.isEmpty()) {
out.collect(Tuple2.of(word.toLowerCase(), 1));
}
}
})
.returns(
TypeInformation.of(
new org.apache.flink.api.common.typeinfo.TypeHint<Tuple2<String, Integer>>() {}
)
)
// keyBy: 按单词分组
.keyBy(tuple -> tuple.f0)
// ====== 窗口操作 ======
// 方式1:滚动窗口(Tumbling Window)
// 每 10 秒一个窗口,窗口之间不重叠
.window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
// 方式2:滑动窗口(Sliding Window)
// 窗口大小 10 秒,每 5 秒滑动一次
// .window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
// 方式3:会话窗口(Session Window)
// 如果超过 5 秒没有数据到来,则关闭窗口
// .window(ProcessingTimeSessionWindows.withGap(Time.seconds(5)))
// 方式4:全局窗口 + 触发器(简化为计数窗口)
// .countWindow(10) // 每 10 条数据触发一次
// 对窗口内的计数求和
.sum(1);
// 输出结果
wordCounts.print("窗口词频统计");
// 执行
env.execute("Windowed WordCount");
}
}
12.2.4 使用 RichFunction 的增强版 WordCount
java
/**
* 使用 RichFunction 的增强版 WordCount
* 说明:RichFunction 提供了生命周期管理(open/close)和运行时上下文
* 可以访问状态、累加器等高级功能
*/
package com.example.flink;
import org.apache.flink.api.common.functions.*;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class RichFunctionWordCount {
/**
* RichFlatMapFunction:带生命周期的 FlatMap 函数
* 相比普通 FlatMapFunction,它有 open() 和 close() 方法
*/
public static class WordSplitter extends RichFlatMapFunction<String, Tuple2<String, Integer>> {
// 定义累加器,用于统计总单词数
private transient long totalWordCount;
/**
* open() 方法:在算子初始化时调用
* 适合做:
* - 初始化数据库连接
* - 初始化累加器
* - 读取配置信息
* - 注册状态
*/
@Override
public void open(Configuration parameters) throws Exception {
// 初始化单词计数器
totalWordCount = 0;
// 获取运行时上下文,可以获取并行度信息、累加器等
int subtaskIndex = getRuntimeContext().getIndexOfThisSubtask();
int numberOfParallelSubtasks = getRuntimeContext().getNumberOfParallelSubtasks();
System.out.printf("[WordSplitter] 初始化 - 子任务 %d/%d%n",
subtaskIndex, numberOfParallelSubtasks);
}
/**
* flatMap 方法:对每条数据进行处理
*/
@Override
public void flatMap(String line, Collector<Tuple2<String, Integer>> out) throws Exception {
// 将文本按空格拆分
String[] words = line.toLowerCase().split("\\W+");
for (String word : words) {
if (word.length() > 0) {
// 输出 (单词, 1) 元组
out.collect(Tuple2.of(word, 1));
totalWordCount++; // 累加单词计数
}
}
}
/**
* close() 方法:在算子关闭时调用
* 适合做:
* - 关闭数据库连接
* - 输出统计信息
* - 清理资源
*/
@Override
public void close() throws Exception {
int subtaskIndex = getRuntimeContext().getIndexOfThisSubtask();
System.out.printf("[WordSplitter] 关闭 - 子任务 %d 共处理 %d 个单词%n",
subtaskIndex, totalWordCount);
}
}
/**
* RichMapFunction:带生命周期的 Map 函数
* 可以访问运行时上下文,获取状态、累加器等
*/
public static class WordMapper extends RichMapFunction<String, Tuple2<String, Integer>> {
@Override
public void open(Configuration parameters) throws Exception {
System.out.println("[WordMapper] 初始化完成");
}
@Override
public Tuple2<String, Integer> map(String word) throws Exception {
return Tuple2.of(word, 1);
}
@Override
public void close() throws Exception {
System.out.println("[WordMapper] 已关闭");
}
}
/**
* RichReduceFunction:带生命周期的 Reduce 函数
* 可以在 open() 中初始化状态
*/
public static class WordReducer extends RichReduceFunction<Tuple2<String, Integer>> {
@Override
public void open(Configuration parameters) throws Exception {
System.out.println("[WordReducer] 初始化完成");
}
@Override
public Tuple2<String, Integer> reduce(
Tuple2<String, Integer> value1, // 累积值
Tuple2<String, Integer> value2 // 新到来的值
) throws Exception {
// 将两个值的计数相加
Tuple2<String, Integer> result = Tuple2.of(
value1.f0, // 保持单词不变
value1.f1 + value2.f1 // 计数相加
);
return result;
}
@Override
public void close() throws Exception {
System.out.println("[WordReducer] 已关闭");
}
}
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 使用集合数据源进行测试
DataStream<String> textStream = env.fromElements(
"Apache Flink is an open source framework",
"for distributed stream and batch data processing",
"Flink's core is a streaming dataflow engine",
"Apache Flink provides low latency high throughput"
);
// 使用 RichFlatMapFunction 拆分单词
DataStream<String> words = textStream
.flatMap(new WordSplitter())
.returns(Types.STRING);
// 使用 RichMapFunction 映射为 KV 对
DataStream<Tuple2<String, Integer>> wordPairs = words
.map(new WordMapper())
.returns(Types.TUPLE(Types.STRING, Types.INT));
// 使用 RichReduceFunction 进行聚合
DataStream<Tuple2<String, Integer>> result = wordPairs
.keyBy(t -> t.f0)
.reduce(new WordReducer());
result.print("RichFunction WordCount");
env.execute("RichFunction WordCount Demo");
}
}
12.2.5 使用 Flink SQL 的 WordCount
java
/**
* 使用 Flink SQL 实现 WordCount
* 说明:Flink SQL 提供了声明式的数据处理方式
* 对熟悉 SQL 的开发者非常友好
*/
package com.example.flink;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.*;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
public class SQLWordCount {
public static void main(String[] args) {
// ====== 1. 创建流处理环境 ======
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// ====== 2. 创建表环境 ======
// TableEnvironment 是 Flink Table API 和 SQL 的入口
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// ====== 3. 创建输入表 ======
// 方式1:从 DataStream 创建表
// 首先创建一个数据流
// 使用 SQL 直接定义数据源
tableEnv.executeSql(
"CREATE TABLE word_source (" +
" id BIGINT," + // 记录ID
" content STRING" + // 文本内容
") WITH (" +
" 'connector' = 'filesystem'," + // 使用文件系统连接器
" 'path' = 'input/words.txt'," + // 文件路径
" 'format' = 'csv'" + // 文件格式
")"
);
// ====== 4. 使用 SQL 进行词频统计 ======
// SQL 查询:使用 LATERAL TABLE + SPLIT 函数拆分单词
Table resultTable = tableEnv.sqlQuery(
"SELECT " +
" word," + // 单词
" COUNT(*) AS word_count" + // 计数
"FROM word_source," +
" LATERAL TABLE(SPLIT(content, ' ')) AS T(word)" + // 将内容按空格拆分为行
"WHERE word IS NOT NULL AND word <> ''" + // 过滤空值
"GROUP BY word" + // 按单词分组
"ORDER BY word_count DESC" // 按计数降序排序
);
// ====== 5. 输出结果 ======
// 将结果表转换为 DataStream 输出
resultTable.execute().print();
// 或者使用撤回流(retract stream),可以处理更新操作
// DataStream<Tuple2<Boolean, Row>> retractStream =
// tableEnv.toRetractStream(resultTable, Row.class);
// retractStream.print();
}
}
十三、综合实战案例
13.1 实时传感器数据处理系统
java
/**
* 综合实战:实时传感器数据处理系统
* 说明:综合运用 Flink 的各种核心概念
* 功能:
* 1. 从 Socket 读取传感器数据
* 2. 数据清洗和转换
* 3. 实时计算每个传感器的平均温度
* 4. 检测异常温度并报警
* 5. 输出统计结果
*/
package com.example.flink;
import org.apache.flink.api.common.functions.*;
import org.apache.flink.api.common.state.*;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.*;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.*;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.util.*;
public class SensorDataPipeline {
// ============= 数据清洗函数 =============
/**
* 数据清洗:解析原始传感器数据
* 输入格式:sensorId,temperature,timestamp
* 输出格式:Tuple3<sensorId, temperature, timestamp>
*/
public static class SensorDataParser
implements MapFunction<String, Tuple3<String, Double, Long>> {
@Override
public Tuple3<String, Double, Long> map(String raw) throws Exception {
try {
String[] fields = raw.split(",");
// 数据验证
if (fields.length != 3) {
return null; // 格式不正确,返回 null(后续过滤)
}
// 解析各个字段
String sensorId = fields[0].trim(); // 传感器ID
double temperature = Double.parseDouble(fields[1].trim()); // 温度值
long timestamp = Long.parseLong(fields[2].trim()); // 时间戳
// 有效性检查
if (temperature < -100 || temperature > 200) {
return null; // 温度值不合理,过滤掉
}
return Tuple3.of(sensorId, temperature, timestamp);
} catch (NumberFormatException e) {
return null; // 数字格式错误,过滤掉
}
}
}
// ============= 异常检测函数 =============
/**
* 异常温度检测器
* 规则1:温度超过 100 度为异常
* 规则2:温度在 1 分钟内变化超过 30 度为异常
*/
public static class TemperatureAnomalyDetector
extends KeyedProcessFunction<String,
Tuple3<String, Double, Long>,
Tuple4<String, Double, String, Long>> {
// 存储上一次的温度
private ValueState<Double> lastTempState;
// 存储上一次温度的时间
private ValueState<Long> lastTimestampState;
// 异常温度阈值
private static final double MAX_TEMP = 100.0;
// 温度变化阈值
private static final double TEMP_CHANGE_THRESHOLD = 30.0;
// 时间窗口(1分钟 = 60000毫秒)
private static final long TIME_WINDOW = 60000;
@Override
public void open(Configuration parameters) throws Exception {
// 初始化状态
lastTempState = getRuntimeContext().getState(
new ValueStateDescriptor<>("last-temp", Double.class, 0.0));
lastTimestampState = getRuntimeContext().getState(
new ValueStateDescriptor<>("last-timestamp", Long.class, 0L));
}
@Override
public void processElement(
Tuple3<String, Double, Long> input,
Context ctx,
Collector<Tuple4<String, Double, String, Long>> out) throws Exception {
String sensorId = input.f0;
double currentTemp = input.f1;
long currentTime = input.f2;
// 获取历史状态
Double lastTemp = lastTempState.value();
Long lastTime = lastTimestampState.value();
// ====== 规则1:检查绝对温度异常 ======
if (currentTemp > MAX_TEMP) {
out.collect(Tuple4.of(
sensorId, // 传感器ID
currentTemp, // 当前温度
"温度超过阈值 " + MAX_TEMP + "°C", // 异常描述
currentTime // 发生时间
));
}
// ====== 规则2:检查温度突变 ======
if (lastTime > 0 && (currentTime - lastTime) < TIME_WINDOW) {
double tempChange = Math.abs(currentTemp - lastTemp);
if (tempChange > TEMP_CHANGE_THRESHOLD) {
out.collect(Tuple4.of(
sensorId,
currentTemp,
String.format("温度突变 %.1f°C(从 %.1f 变为 %.1f)",
tempChange, lastTemp, currentTemp),
currentTime
));
}
}
// 更新状态
lastTempState.update(currentTemp);
lastTimestampState.update(currentTime);
// 注册清理定时器(5分钟后清理状态)
ctx.timerService().registerEventTimeTimer(currentTime + 5 * 60 * 1000);
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx,
Collector<Tuple4<String, Double, String, Long>> out) throws Exception {
// 清理过期状态
lastTempState.clear();
lastTimestampState.clear();
}
}
// ============= 窗口平均温度计算 =============
/**
* 计算每个传感器在 10 秒窗口内的平均温度
*/
public static class AverageTemperature
extends ProcessWindowFunction<
Tuple3<String, Double, Long>,
Tuple3<String, Double, Long>, // 输出:(sensorId, avgTemp, windowEnd)
String,
TimeWindow> {
@Override
public void process(
String sensorId,
Context context,
Iterable<Tuple3<String, Double, Long>> elements,
Collector<Tuple3<String, Double, Long>> out) throws Exception {
double sum = 0.0; // 温度总和
long count = 0; // 数据条数
// 遍历窗口内所有元素
for (Tuple3<String, Double, Long> element : elements) {
sum += element.f1; // 累加温度
count++; // 计数
}
// 计算平均值
double average = sum / count;
// 获取窗口结束时间
long windowEnd = context.window().getEnd();
// 输出结果
out.collect(Tuple3.of(sensorId, average, windowEnd));
}
}
// ============= 主程序 =============
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// ====== 数据源:从 Socket 读取传感器数据 ======
// 数据格式:sensorId,temperature,timestamp
// 示例:sensor_001,25.5,1705123456789
DataStream<String> rawData = env.socketTextStream("localhost", 9999);
// ====== 第一步:数据清洗 ======
DataStream<Tuple3<String, Double, Long>> sensorData = rawData
.map(new SensorDataParser()) // 解析原始数据
.returns(Types.TUPLE(Types.STRING, Types.DOUBLE, Types.LONG))
.filter(data -> data != null); // 过滤无效数据
// ====== 第二步:异常检测 ======
DataStream<Tuple4<String, Double, String, Long>> alerts = sensorData
.keyBy(t -> t.f0) // 按传感器ID分组
.process(new TemperatureAnomalyDetector()); // 异常检测
// ====== 第三步:计算窗口平均温度 ======
DataStream<Tuple3<String, Double, Long>> avgTemps = sensorData
.keyBy(t -> t.f0) // 按传感器ID分组
.timeWindow(Time.seconds(10)) // 10秒滚动窗口
.process(new AverageTemperature()); // 计算平均温度
// ====== 第四步:输出结果 ======
// 输出报警信息
alerts.print("⚠ 异常报警");
// 输出平均温度
avgTemps.print("📊 平均温度");
// 执行程序
env.execute("Real-time Sensor Data Pipeline");
}
}
本章小结
| 知识点 | 核心内容 |
|---|---|
| Flink 简介 | 分布式流处理引擎,支持有状态计算 |
| 为什么选 Flink | 低延迟、高吞吐、精确一次语义、真正的流处理 |
| 传统架构 | 事务处理(延迟低但分析能力弱)、批处理(分析强但延迟高) |
| Lambda 架构 | 维护批处理和实时处理两套代码,开发维护成本高 |
| 流处理架构 | Flink 一套代码统一批流处理,是理想的解决方案 |
| Flink 优势 | 状态管理、Checkpoint 机制、窗口支持、精确一次语义 |
| 应用场景 | 事件驱动(欺诈检测)、数据分析(实时报表)、数据流水线(ETL) |
| 核心组件 | DataStream API、Table API、SQL、CEP |
| 体系架构 | JobManager(调度)+ TaskManager(执行)+ Task Slot(并行) |
| 编程模型 | Source → Transformation → Sink 的数据流模型 |
| 安装部署 | Standalone(单机/集群)、YARN、Kubernetes 三种模式 |
| WordCount | 批处理版、流处理版、窗口版、SQL 版多种实现 |