Hadoop学习教程,从入门到精通,Apache Flink 全面知识点详解(15)


Apache Flink 是一个框架和分布式处理引擎,用于在无界和有界 数据流上进行有状态计算

核心特征:

特征 说明
分布式 运行在多台机器集群上
有状态 能够记住已处理的数据信息
高吞吐 每秒处理数百万条事件
低延迟 毫秒级别延迟
精确一次 Exactly-Once 语义保证
复制代码
2014年 → Flink 成为 Apache 顶级项目
2015年 → Flink 1.0 发布
2019年 → Flink 1.9 发布(阿里巴巴收购 Data Artisans)
2020年 → Flink 1.11 发布(重大改进)
2023年 → Flink 1.17+(持续优化)

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 Spark Streaming Storm
处理模型 真正的流处理 微批处理 真正的流处理
延迟 毫秒级 秒级 毫秒级
吞吐量 较低
Exactly-Once 支持 支持 支持(Trident)
状态管理 内置强大 有限 基础
窗口支持 丰富灵活 基于批 无原生支持
批流统一 原生支持 核心是批 不支持
SQL支持 Flink SQL Spark SQL
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");
    }
}

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

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 上层 API                                │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐     │
│  │  Flink SQL   │  │  Table API   │  │  CEP (复杂事件处理)   │     │
│  └─────────────┘  └─────────────┘  └─────────────────────┘     │
├─────────────────────────────────────────────────────────────────┤
│                    Flink 核心 API                                │
│  ┌──────────────────────────┐  ┌────────────────────────────┐  │
│  │   DataStream API (流)     │  │   DataSet API (批/已废弃)   │  │
│  └──────────────────────────┘  └────────────────────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                    运行时核心层                                    │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐      │
│  │  Task执行  │ │  状态管理  │ │  内存管理  │ │  Checkpoint   │      │
│  └──────────┘ └──────────┘ └──────────┘ └──────────────┘      │
├─────────────────────────────────────────────────────────────────┤
│                    部署层                                        │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐                       │
│  │ Standalone │ │   YARN   │ │ Kubernetes│                       │
│  └──────────┘ └──────────┘ └──────────┘                       │
└─────────────────────────────────────────────────────────────────┘

复制代码
┌───────────────────────────────────────────────────────────────────┐
│                        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");
    }
}

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

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
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
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
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");
    }
}
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 版多种实现