大数据-124 - Flink State:Keyed State、Operator State KeyGroups 工作原理 案例解析

点一下关注吧!!!非常感谢!!持续更新!!!

🚀 AI篇持续更新中!(长期更新)

AI炼丹日志-31- 千呼万唤始出来 GPT-5 发布!"快的模型 + 深度思考模型 + 实时路由",持续打造实用AI工具指南!📐🤖

💻 Java篇正式开启!(300篇)

目前2025年10月13日更新到: Java-147 深入浅出 MongoDB 分页查询详解:skip() + limit() + sort() 实现高效分页、性能优化与 WriteConcern 写入机制全解析 MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务正在更新!深入浅出助你打牢基础!

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈! 大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解

章节内容

上节我们完成了如下的内容:

  • Flink 并行度
  • Flink 并行度详解
  • Flink 并行度 案例

状态类型

Flink根据是否需要保存中间结果,将计算分为有状态计算和无状态计算两种类型:

  1. 有状态计算(Stateful Computation)

    • 特点:需要保存中间结果或状态,依赖之前或之后的事件进行数据处理
    • 状态类型:
      • 键控状态(Keyed State):与特定键相关联的状态
      • 算子状态(Operator State):与算子实例绑定的状态
    • 典型应用场景:
      • 窗口聚合(如计算5分钟内的平均温度)
      • 模式检测(如检测异常登录序列)
      • 数据去重(如电商订单去重处理)
    • 状态后端支持:
      • MemoryStateBackend(内存)
      • FsStateBackend(文件系统)
      • RocksDBStateBackend(嵌入式数据库)
  2. 无状态计算(Stateless Computation)

    • 特点:每个数据记录的处理都是独立的,不需要保存中间状态
    • 典型算子:
      • Map(一对一转换)
      • Filter(数据过滤)
      • FlatMap(一对多转换)
    • 应用场景:
      • 简单的数据格式转换
      • 数据过滤(如过滤掉日志中的调试信息)
      • 数据拆分(如将CSV行拆分为多个字段)
    • 优势:执行效率高,资源消耗低,可线性扩展

在实际应用中,Flink作业通常会混合使用这两种计算方式。例如,在实时推荐系统中,可能先用无状态的Map算子进行数据清洗,然后通过有状态的窗口聚合计算用户偏好。

根据数据结构不同,Flink定义了多种State,应用于不同的场景。

  • ValueState:即类型为T的单值状态,这个状态与对应的Key绑定,是最简单的状态了。它可以通过update方法更新状态值,通过 value() 方法获取状态值
  • ListState:即Key上的状态值为一个列表,可以通过add方法往列表中附加值,也可以通过get()方法返回一个Iterable来遍历状态值
  • ReducingState:这种状态通过用户传入的ReduceFunction,每次调用add方法添加值的时候,会调用ReduceFunction,最后合并到一个单一的状态值。
  • FoldingState:跟ReducingState有点类似,不过它的状态值类型可以与add方法中传入的元素类型不同(这种状态会在未来的Flink版本当中删除)
  • MapState:即状态值为一个Map,用户通过put和putAll方法添加元素

State按照是否有Key划分为:

  • KeyedState
  • OperatorState

案例1 利用State求平均值

实现思路

  • 读数据源
  • 将数据源根据Key分组
  • 按照Key分组策略,对流式数据调用状态化处理:实例化出一个状态实例,随着流式数据的到来更新状态,最后输出结果

编写代码

java 复制代码
package icu.wzk;
public class FlinkStateTest01 {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        DataStreamSource<Tuple2<Long, Long>> data = env
                .fromElements(
                        Tuple2.of(1L, 3L),
                        Tuple2.of(1L, 5L),
                        Tuple2.of(1L, 7L),
                        Tuple2.of(1L, 4L),
                        Tuple2.of(1L, 2L)
                );
        KeyedStream<Tuple2<Long, Long>, Long> keyed = data
                .keyBy(new KeySelector<Tuple2<Long, Long>, Long>() {
                    @Override
                    public Long getKey(Tuple2<Long, Long> value) throws Exception {
                        return value.f0;
                    }
                });
        SingleOutputStreamOperator<Tuple2<Long, Long>> flatMapped = keyed
                .flatMap(new RichFlatMapFunction<Tuple2<Long, Long>, Tuple2<Long, Long>>() {
                    private transient ValueState<Tuple2<Long, Long>> sum;

                    @Override
                    public void flatMap(Tuple2<Long, Long> value, Collector<Tuple2<Long, Long>> out) throws Exception {
                        Tuple2<Long, Long> currentSum = sum.value();
                        if (currentSum == null) {
                            currentSum = Tuple2.of(0L, 0L);
                        }
                        // 更新
                        currentSum.f0 += 1L;
                        currentSum.f1 += value.f1;
                        System.out.println("currentValue: " + currentSum);
                        // 更新状态值
                        sum.update(currentSum);
                        // 如果 count >= 5 清空状态值 重新计算
                        if (currentSum.f0 >= 5) {
                            out.collect(new Tuple2<>(value.f0, currentSum.f1 / currentSum.f0));
                            sum.clear();
                        }
                    }

                    @Override
                    public void open(Configuration parameters) throws Exception {
                        ValueStateDescriptor<Tuple2<Long, Long>> descriptor = new ValueStateDescriptor<>(
                                "average",
                                TypeInformation.of(new TypeHint<Tuple2<Long, Long>>() {})
                        );
                        sum = getRuntimeContext().getState(descriptor);
                    }
                });
        flatMapped.print();
        env.execute("Flink State Test");
    }
}

运行结果

执行分析

Keyed State

Keyed State 是 Flink 中一种与 Key 相关联的状态类型,它只能应用于 KeyedStream 类型的数据集所对应的 Function 和 Operator 上。KeyedState 实际上是 OperatorState 的一个特例,其关键区别在于 KeyedState 会事先按照 Key 对数据集进行区分,使得每个 KeyedState 仅对应一个特定的 Operator 和 Key 的组合。

核心特性

  1. Key 关联性:每个状态都严格绑定到特定的 Key,相同 Key 的数据会访问相同的状态实例
  2. 自动分区:状态会根据 Key 自动分区,保证相同 Key 的数据总是被路由到相同的并行任务实例
  3. 高效访问:通过 Key 可以直接定位到对应的状态,避免了全量扫描

管理机制

KeyedState 通过 KeyGroups 机制进行管理,这个机制主要服务于以下场景:

  • 动态扩缩容:当算子的并行度发生变化时,系统会自动重新分布 KeyedState 数据
  • 故障恢复:在故障恢复时确保状态正确分配到新的任务实例
KeyGroups 工作原理
  1. 分组策略:系统会将所有可能的 Key 通过哈希等方式分配到固定数量的 KeyGroups 中
  2. 动态分配:在运行时,一个 Keyed 算子实例可能负责处理一个或多个 KeyGroups 的 Keys
  3. 再平衡:当并行度变化时,系统会重新计算 KeyGroups 到任务实例的映射关系

典型应用场景

  1. 实时聚合计算:如计算每个用户的点击量统计
java 复制代码
// 示例:使用 ValueState 实现用户点击统计
public class UserClickCounter extends KeyedProcessFunction<String, ClickEvent, Tuple2<String, Integer>> {
    private ValueState<Integer> countState;
    
    @Override
    public void open(Configuration parameters) {
        ValueStateDescriptor<Integer> descriptor = 
            new ValueStateDescriptor<>("clickCount", Integer.class);
        countState = getRuntimeContext().getState(descriptor);
    }
    
    @Override
    public void processElement(ClickEvent event, Context ctx, Collector<Tuple2<String, Integer>> out) throws Exception {
        Integer currentCount = countState.value();
        if (currentCount == null) {
            currentCount = 0;
        }
        currentCount++;
        countState.update(currentCount);
        out.collect(new Tuple2<>(ctx.getCurrentKey(), currentCount));
    }
}
  1. 模式检测:如检测用户异常登录行为
  2. 会话窗口处理:跟踪用户会话活动

状态类型

Flink 提供了多种 KeyedState 实现:

  1. ValueState:存储单个值
  2. ListState:存储值的列表
  3. MapState:存储键值对映射
  4. ReducingState:存储聚合结果
  5. AggregatingState:支持更复杂的聚合

性能考量

  1. 状态后端选择:可选择 MemoryStateBackend、FsStateBackend 或 RocksDBStateBackend
  2. 状态序列化:优化状态序列化方式可以显著提升性能
  3. 状态清理:合理设置状态 TTL 避免状态无限增长

KeyedState 的这种设计使得 Flink 能够高效处理有键数据流的状态管理,同时保证了系统的弹性和可扩展性。

Operator State

Operator State(算子状态)是 Flink 中一种重要的状态类型,它与 Keyed State 有着本质区别。Operator State 直接与特定的算子实例(operator instance)绑定,而非依赖于数据中的 key。这种设计意味着:

  1. 状态分配机制

    • 每个算子实例维护自己处理的数据所对应的状态
    • 状态数据在算子实例间是分区存储的
    • 当并行度调整时,Flink 会自动重新分配状态数据
  2. 并行度适应性

    • 支持动态扩容/缩容场景
    • 提供三种内置的重新分配策略:
      • 均匀分配(Even-split redistribution):将状态均匀划分到所有新算子实例
      • 全量广播(Union redistribution):将完整状态复制到每个新实例
      • 自定义分配 :通过实现 ListCheckpointed 接口实现
  3. 典型应用场景

    • 源算子(如 Kafka Consumer)记录消费偏移量
    • 窗口算子维护窗口触发状态
    • 自定义聚合算子保存中间结果

状态管理形式

在 Flink 中,无论是 KeyedState 还是 OperatorState 都支持两种管理形式:

托管状态(Managed State)

这是 Flink 推荐的使用方式,具有以下特点:

  • 运行时管理:由 Flink 运行时环境统一管理状态
  • 存储优化
    • 自动转换为高效的内存数据结构(如 HashTables)
    • 可选 RocksDB 作为状态后端实现大状态存储
  • 持久化机制
    • 通过 Checkpoint 机制定期持久化状态
    • 提供精确一次(exactly-once)的状态保证
    • 使用高效的序列化框架(如 Kryo)进行状态序列化
  • 恢复流程
    • 任务失败时自动从最近一次成功的 Checkpoint 恢复
    • 支持增量 Checkpoint 减少恢复时间

原生状态(Raw State)

这种形式提供更大的灵活性但需要开发者承担更多责任:

  • 自主管理:算子需要自行维护状态数据结构
  • 序列化要求
    • 开发者需要自己实现状态序列化逻辑
    • 需要处理版本兼容性问题
  • 检查点处理
    • Checkpoint 触发时需要显式进行状态快照
    • 恢复时需要自行反序列化状态数据
  • 典型用例
    • 需要特殊数据结构优化的场景
    • 集成第三方库时需要保持特定格式的状态

状态后端比较

对于托管状态,Flink 提供多种状态后端实现:

特性 MemoryStateBackend FsStateBackend RocksDBStateBackend
存储介质 JVM 堆内存 文件系统 RocksDB
状态大小限制 <10MB 单任务可达TB级 仅受磁盘容量限制
访问速度 最快 中等 相对较慢
适用场景 测试/小状态 常规生产环境 超大状态场景

DataStreamAPI支持使用ManagedState和RawState两种状态形式,在Flink中推荐用户使用ManagedState管理状态数据,主要原因是ManagedState能够更好地支持状态数据的重平衡以及更加完善的内存管理。

状态描述

State既然是暴露给用户的,那么就需要有一些属性需要指定:

  • State名称
  • Value Serializer
  • State Type Info

在对应的StateBackend中,会去调用对应的create方法获取到stateDescriptor中的值。 Flink通过StateDescriptor来定义一个状态,这是一个抽象类,内部定义了状态名称、类型、序列化器等基础信息,与上面的状态对应,从StateDescriptor派生ValueStateDescriptor、ListStateDescriptor等等

  • ValueState getState(ValueStateDescriptor)
  • ReducingState getReducingState(ReducingStateDescriptor)
  • ListState getListState(ListStateDescriptor)
  • FoldingState getFoldingState(FoldingStateDescriptor)
  • MapState getMapState(MapStateDescriptot)
相关推荐
间彧11 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧11 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧11 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧11 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧12 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang12 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构
科技峰行者13 小时前
微软与OpenAI联合研发“Orion“超大规模AI模型:100万亿参数开启“科学家AI“新纪元
大数据·人工智能·microsoft
拓端研究室13 小时前
2025母婴用品双11营销解码与AI应用洞察报告|附40+份报告PDF、数据、绘图模板汇总下载
大数据·人工智能
GOATLong13 小时前
git使用
大数据·c语言·c++·git·elasticsearch
草明13 小时前
Go 的 IO 多路复用
开发语言·后端·golang