Flink 中KeyBy、分区、分组的正确理解

1.Flink中的KeyBy

在Flink中,KeyBy作为我们常用的一个聚合类型算子,它可以按照相同的Key对数据进行重新分区,分区之后分配到对应的子任务当中去。
源码解析
keyBy 得到的结果将不再是 DataStream,而是会将 DataStream 转换为 KeyedStream(键控流),KeyedStream 可以认为是“分区流”或者“键控流”,它是对 DataStream 按照 key 的一个逻辑分区。
所以泛型有两个类型:除去当前流中的元素类型外,还需要指定 key 的类型。
在这里插入图片描述
KeyBy是如何实现分区的呢

Flink中的KeyBy底层其实就是通过Hash实现的,通过对Key的值进行Hash,再做一次murmurHash,取模运算。
再通过Job的并行度,就能获取每个Key应该分配到那个子任务中了。

在这里插入图片描述

2.分组和分区在Flink中的区别

分区:分区(Partitioning)是将数据流划分为多个子集,这些子集可以在不同的任务实例上进行处理,以实现数据的并行处理。
数据具体去往哪个分区,是通过指定的 key 值先进行一次 hash 再进行一次 murmurHash,通过上述计算得到的值再与并行度进行相应的计算得到。
分组:分组(Grouping)是将具有相同键值的数据元素归类到一起,以便进行后续操作(如聚合、窗口计算等)。
key值相同的数据将进入同一个分组中。
注意:数据如果具有相同的key将一定去往同一个分组和分区,但是同一分区中的数据不一定属于同一组。

3.代码示例

package com.flink.DataStream.Aggregation;

import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

public class FlinkKeyByDemo {
    public static void main(String[] args) throws Exception {
        //TODO 创建Flink上下文执行环境
        StreamExecutionEnvironment streamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
        //设置并行度为1
        streamExecutionEnvironment.setParallelism(1);
        //设置执行模式为批处理
        streamExecutionEnvironment.setRuntimeMode(RuntimeExecutionMode.BATCH);
        //TODO source 从集合中创建数据源
        DataStreamSource<String> dataStreamSource = streamExecutionEnvironment.fromElements("hello word", "hello flink");
        //TODO 方式一 匿名实现类
        SingleOutputStreamOperator<Tuple2<String, Integer>> outputStreamOperator1 = dataStreamSource
                .flatMap(new FlatMapFunction<String, String>() {
                    @Override
                    public void flatMap(String s, Collector<String> collector) throws Exception {
                        String[] s1 = s.split(" ");
                        for (String word : s1) {
                            collector.collect(word);
                        }
                    }
                })
                .map(new MapFunction<String, Tuple2<String, Integer>>() {
                    @Override
                    public Tuple2<String, Integer> map(String s) throws Exception {
                        Tuple2<String, Integer> aa = Tuple2.of(s, 1);
                        return aa;
                    }
                })
                /**
                 * keyBy 得到的结果将不再是 DataStream,而是会将 DataStream 转换为 KeyedStream(键控流)
                 * KeyedStream 可以认为是“分区流”或者“键控流”,它是对 DataStream 按照 key 的一个逻辑分区
                 * 所以泛型有两个类型:除去当前流中的元素类型外,还需要指定 key 的类型。
                 * */

                /**
                 * 分组和分区在Flink 中具有不同的含义和作用:
                 * 分区:分区(Partitioning)是将数据流划分为多个子集,这些子集可以在不同的任务实例上进行处理,以实现数据的并行处理。
                 *      数据具体去往哪个分区,是通过指定的 key 值先进行一次 hash 再进行一次 murmurHash,通过上述计算得到的值再与并行度进行相应的计算得到。
                 * 分组:分组(Grouping)是将具有相同键值的数据元素归类到一起,以便进行后续操作 (如聚合、窗口计算等)。
                 *      key 值相同的数据将进入同一个分组中。
                 * 注意:数据如果具有相同的key将一定去往同一个分组和分区,但是同一分区中的数据不一定属于同一组。
                 * */

                .keyBy(new KeySelector<Tuple2<String, Integer>, String>() {
                    @Override
                    public String getKey(Tuple2<String, Integer> stringIntegerTuple2) throws Exception {
                        return stringIntegerTuple2.f0;
                    }
                })
                .sum(1);
        //TODO 方式二 Lamda表达式实现
        SingleOutputStreamOperator<Tuple2<String, Integer>> outputStreamOperator2 = dataStreamSource
                .flatMap((String s, Collector<String> collector) -> {
                    String[] s1 = s.split(" ");
                    for (String word : s1) {
                        collector.collect(word);
                    }
                })
                .returns(Types.STRING)
                .map((String word) -> {
                    return Tuple2.of(word, 1);
                })
                //Java中lamda表达式存在类型擦除
                .returns(Types.TUPLE(Types.STRING, Types.INT))
                .keyBy((Tuple2<String, Integer> s) -> {
                    return s.f0;
                })
                .sum(1);
        //TODO sink
        outputStreamOperator1.print("方式一");
        outputStreamOperator2.print("方式二");
        //TODO 执行
        streamExecutionEnvironment.execute("Flink KeyBy Demo");
    }
}
<think>嗯,用户提到FlinkkeyBy随机数报错,首先我需要理解具体的问题场景。FlinkkeyBy操作通常用于根据某个键对数据流进行分区,确保相同键的数据进入同一个子任务。如果用户在使用随机数作为keyBy的参数时出现错误,可能的原因可能有几个方面。 首先,用户可能是在生成随机数时出现了问题。比如,如果在生成随机数时没有正确设置范围或者类型,导致生成的键值不符合Flink的要求。例如,使用浮点数作为键可能会导致类型不兼容,因为Flink的键需要是可序列化的,并且最好是不可变类型,比如整数或字符串。 接下来,我需要考虑用户使用的随机数生成方式。如果他们是在数据源的map或flatMap操作生成随机数,并直接传递给keyBy,那么需要确保生成的随机数在同一个并行度实例是稳定的。不过,这里可能更常见的问题是类型问题,比如生成的是Double但期望的是Integer,或者使用了非确定性的随机数生成方式,导致状态管理出现问题。 另外,FlinkkeyBy操作要求键的选择必须满足确定性,即对于相同的输入数据,每次生成的键必须一致。如果用户使用的是随机数,并且在每次处理记录时都生成新的随机数,这会导致相同的记录在不同时间处理时得到不同的键,从而可能引发状态不一致的问题。尤其是在有状态计算,如窗口操作或状态聚合,这会导致无法正确访问或更新状态,进而抛出异常。 还可能涉及到并行度的问题。例如,如果用户为每个任务生成不同的随机数范围,可能会导致不同并行实例的键分布不均匀,或者出现重复键的问题,从而影响分区的正确性。不过,这种情况下报错的可能性较低,更多可能导致性能问题而非直接错误。 另外,用户可能在代码错误地使用了keyBy的参数。例如,传递了一个非法的字段名称,或者试图通过索引访问不存在的字段。如果用户使用的是元组或POJO类型,需要确保字段索引或名称正确无误。 还有可能用户在使用随机数时没有正确序列化。例如,如果随机数生成器本身没有被正确序列化,在分布式环境传递时会出现问题,导致任务失败。这种情况下,需要确保所有在算子使用的变量都是可序列化的,或者被正确地初始化和关闭。 接下来,我需要考虑用户可能遇到的异常类型。常见的错误包括ClassCastException,这是由于键的类型不匹配导致的;或者InvalidProgramException,可能是因为类型不可序列化;还有可能PartitionException,如果键的生成方式导致分区错误。 最后,我需要总结解决步骤:首先检查随机数的生成逻辑,确保类型正确且稳定;其次验证keyBy的参数是否正确引用字段;然后确保使用的类型可序列化;最后考虑状态管理的正确性,避免因随机键导致的状态问题。此外,建议用户如果确实需要随机分布,可以考虑使用更合适的分区策略,如shuffle(),而不是依赖随机键的keyBy。</think>在Flink使用`keyBy`配合随机数时遇到报错,通常由以下原因导致。以下是逐步分析及解决方法: --- ### **1. 随机数生成方式问题** #### 现象 - 代码示例直接生成随机数作为键(如`keyBy(r -> new Random().nextInt(100))`),可能导致以下报错: ``` The key returned by the KeySelector must be deterministic! ``` #### 原因 - Flink要求`KeySelector`产生的键**必须确定**,即相同输入必须生成相同键。若每次调用生成新的随机数,键会变化,破坏状态一致性(尤其在窗口或状态操作)。 - 随机数生成器(如`new Random()`)未被序列化,导致任务提交时序列化失败。 #### 解决 - **固定随机种子**:确保相同输入生成相同键。 ```java // 在可序列化的类初始化Random private final Random random = new Random(固定的种子); .keyBy(r -> random.nextInt(100)) ``` - **使用数据本身特征**:避免依赖随机数,改用数据稳定字段(如时间戳取模、哈希值)。 --- ### **2. 键类型非法** #### 现象 - 报错类似`Type ... cannot be used as key`或`ClassCastException`。 #### 原因 - Flink要求键类型必须实现`hashCode()`和`equals()`方法,且需可序列化。 - 若使用`Double`、`Float`等浮点类型作为键,可能因精度问题导致不可预测行为(如`0.1`实际存储为近似值)。 #### 解决 - **转为整数或字符串**:如`(int)(randomValue * 100)`或`String.valueOf(randomValue)`。 - **确保键类型可序列化**:检查自定义键类是否实现`Serializable`接口。 --- ### **3. 并行度与分区不匹配** #### 现象 - 数据分布不均,部分子任务负载过高或报超时错误。 #### 原因 - 随机键可能导致数据倾斜(如某些键出现频率过高)。 - 并行度设置不合理,与键空间大小不匹配(如键范围是0-99,但并行度为10,可能分布不均)。 #### 解决 - **调整键范围**:根据并行度合理设计键范围,如`并行度 × N`。 - **改用`rebalance()`**:若需均匀分布数据,直接使用`rebalance()`代替`keyBy`。 --- ### **4. 正确使用随机分区的替代方案** 若目的是**均匀分配数据**而非按逻辑分组,应避免滥用`keyBy`,改用以下方法: - **`shuffle()`**:完全随机分区,无键约束。 ```java dataStream.shuffle(); ``` - **`rebalance()`**:轮询分区,均衡负载。 ```java dataStream.rebalance(); ``` --- ### **总结步骤** 1. **检查键生成逻辑**:确保键确定且可序列化。 2. **替换键类型**:使用整数或字符串代替浮点数。 3. **调整分区策略**:若无需分组,改用`shuffle()`或`rebalance()`。 4. **验证并行度与键范围**:避免数据倾斜。 通过以上步骤,可解决因随机数导致的`keyBy`报错问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

文文鑫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值