flink keyby指定key方式详解

本文详细介绍了Flink中KeyBy算子的使用方法及其应用场景,包括通过字段位置、字段名称和KeySelector等方式指定key,以及如何处理复杂嵌套数据结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.keyby算子

keyby是flink中非常常见的操作。其作用为在逻辑上将流划分为不相交的分区,而具有相同key的数据都分配到同一个分区。这种操作在各种大数据计算引擎中都非常常见,比如最早的mapreduce,从map阶段到reduce阶段,就是通过shuffle操作将具有相同key的数据分配到同一个reduce端进行处理。在flink内部,keyby是通过哈希分区来实现的,并且自带有多种指定key的方式。

2.源码分析

我们先通过源码,来看看keyby指定key的几种不同方式,flink版本1.7.2

	/**
	 * Partitions the operator state of a {@link DataStream} by the given key positions.
	 *
	 * @param fields
	 *            The position of the fields on which the {@link DataStream}
	 *            will be grouped.
	 * @return The {@link DataStream} with partitioned state (i.e. KeyedStream)
	 */
	public KeyedStream<T, Tuple> keyBy(int... fields) {
		if (getType() instanceof BasicArrayTypeInfo || getType() instanceof PrimitiveArrayTypeInfo) {
			return keyBy(KeySelectorUtil.getSelectorForArray(fields, getType()));
		} else {
			return keyBy(new Keys.ExpressionKeys<>(fields, getType()));
		}
	}

第一种方式,通过指定字段的位置来进行分组,输入参数为一个或多个整数,整数即代表字段对应位置。

	/**
	 * Partitions the operator state of a {@link DataStream} using field expressions.
	 * A field expression is either the name of a public field or a getter method with parentheses
	 * of the {@link DataStream}'s underlying type. A dot can be used to drill
	 * down into objects, as in {@code "field1.getInnerField2()" }.
	 *
	 * @param fields
	 *            One or more field expressions on which the state of the {@link DataStream} operators will be
	 *            partitioned.
	 * @return The {@link DataStream} with partitioned state (i.e. KeyedStream)
	 **/
	public KeyedStream<T, Tuple> keyBy(String... fields) {
		return keyBy(new Keys.ExpressionKeys<>(fields, getType()));
	}

第二种方式,通过指定字段名来指定key。这个字段名是有一定要求的,后面我们再详细解释。

	/**
	 * It creates a new {@link KeyedStream} that uses the provided key for partitioning
	 * its operator states.
	 *
	 * @param key
	 *            The KeySelector to be used for extracting the key for partitioning
	 * @return The {@link DataStream} with partitioned state (i.e. KeyedStream)
	 */
	public <K> KeyedStream<T, K> keyBy(KeySelector<T, K> key) {
		Preconditions.checkNotNull(key);
		return new KeyedStream<>(this, clean(key));
	}

第三种方式,通过KeySelector的方式指定。

而KeySelector是一个接口,里面只有一个方法getKey,我们使用的时候实现getKey方法即可。

@FunctionalInterface
public interface KeySelector<IN, KEY> extends Function, Serializable {
	KEY getKey(IN value) throws Exception;
}

3.通过字段号指定key

通过字段号指定key相对比较简单,直接看一个wordcount例子即可。

    public static void baseVersion() throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        DataStream<String> stream = env.fromElements("java python c python python c");
        DataStream<Tuple2<String, Integer>> flatstream = stream.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
                for(String word: value.split("\\W+")) {
                    out.collect(new Tuple2<>(word, 1));
                }
            }
        })
                .keyBy(0)
                .sum(1);

        flatstream.print();
        env.execute("keyby base version");
    }

keyBy(0)表示对第一个字段,即word进行分区,而sum(1)则表示对第二个字段即count进行求和。

4.通过字段名指定key

通过字段号指定key使用比较简单方便,但是如果是比较复杂的场景,就不好搞定了。比如如果数据是个比较复杂的嵌套结构Tuple2<Tuple2<String, Integer>, Integer>,如果我们想对内部嵌套的Tuple2的第一个字段进行keyby操作,就无法通过字段号来操作,这个时候我们可以通过字段名的方式来进行代替。
字段名的方式相对来说复杂一些,下面我们来进行示范。

还是先以简单的wordcount为例。

先定义个内部静态类,静态类包含有两个字段,分别为word与count。

    public static final class WC {
        public String word;
        public int count;
        public WC() {}
        public WC(String word, int count) {
            this.word = word;
            this.count = count;
        }
        public String getWord() {
            return word;
        }
        public int getCount() {
            return count;
        }
        public void setWord(String word) {
            this.word = word;
        }
        public void setCount(int count) {
            this.count = count;
        }

        @Override
        public String toString() {
            return this.word + ": " + this.count;
        }
    }

该POLO类中的两个字段word与count,可以传到keyby算子中。

然后再进行flink相关代码的编写。

    public static void nameVersion() throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);`在这里插入代码片`
        WC wc1 = new WC("java", 1);
        WC wc2 = new WC("python", 2);
        WC wc3 = new WC("c", 3);
        WC wc4 = new WC("c", 4);
        WC wc5 = new WC("java", 5);

        DataStream<WC> stream = env.fromElements(wc1, wc2, wc3, wc4, wc5);
        stream = stream.keyBy("word").sum("count");
        stream.print();

        env.execute("keyed base");
    }

上面代码输出为

java: 1
python: 2
c: 3
c: 7
java: 6

注意上面的WC pojo类是有要求的
1.keyby中的字段名必须与pojo类的字段名一致。
2.pojo类一定要提供默认的构造函数,否则代码会报如下错误。

Exception in thread "main" org.apache.flink.api.common.InvalidProgramException: This type (GenericType<xxx.xxx.xxx.WC>) cannot be used as key.
	at org.apache.flink.api.common.operators.Keys$ExpressionKeys.<init>(Keys.java:330)
	at org.apache.flink.streaming.api.datastream.DataStream.keyBy(DataStream.java:337)
	...

3.字段需要提供get/set方法。(但是在1.7.2版本测试,如果对字段不提供get/set方法,wordcount代码也可以正常运行)

5.通过嵌套字段名指定key

接下来我们看嵌套的字段名如何在keyby中被指定。

    public static final class WC {
        public int count;
        public InnerClass inner;
        public WC() {}
        public WC(InnerClass inner, int count) {
            this.inner = inner;
            this.count = count;
        }

        public int getCount() {
            return count;
        }
        public WC setCount(int count) {
            this.count = count;
            return this;
        }

        @Override
        public String toString() {
            return this.inner.name + ": " + this.count;
        }
    }
    public static final class InnerClass {
        public String name;
        public String department;

        public InnerClass() {}
        public InnerClass(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public InnerClass setName(String name) {
            this.name = name;
            return this;
        }
        public String getDepartment() {
            return department;
        }
        public InnerClass setDepartment(String department) {
            this.department = department;
            return this;
        }
    }

首先我们定义了两个pojo类,一个是WC类,包含有count字段以及InnerClass对象。而InnerClass有name与department两个字段。
有同学可能会问,搞这么复杂干嘛,直接将所有字段定义到WC类中不就好了。同学们,我们这里是演示嵌套字段的用法…

接下来,我们想将name字段指定为keyby中的key

    public static void run() throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        InnerClass inn1 = new InnerClass("jojo"); InnerClass inn2 = new InnerClass("jojo");
        InnerClass inn3 = new InnerClass("lili"); InnerClass inn4 = new InnerClass("lili");
        WC wc1 = new WC(inn1, 1);
        WC wc2 = new WC(inn2, 2);
        WC wc3 = new WC(inn3, 3);
        WC wc4 = new WC(inn4, 4);

        DataStream<WC> stream = env.fromElements(wc1, wc2, wc3, wc4)
                .keyBy("inner.name")
                .sum("count");
        stream.print();
        env.execute("keyby complex version");
    }

上面的例子中
count指的是WC中的count字段
inner.word指的是InnerClass中的word字段,inner则表示WC类中的inner属性。

这样就达到了指定复杂嵌套结构中key的目的。

6.通过KeySelector的方式指定key

看一个例子,就能明白上述方式的用法。

    public static void keyselect() throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        DataStream<String> stream = env.fromElements("java python c python python c");
        DataStream<Tuple2<String, Integer>> flatstream = stream.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
            @Override
            public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
                for(String word: value.split("\\W+")) {
                    out.collect(new Tuple2<>(word, 1));
                }
            }
        })
                .keyBy((KeySelector<Tuple2<String, Integer>, Object>) value -> value.f0)
                .sum(1);

        flatstream.print();
        env.execute("key select version");
    }
<think>好的,我现在需要处理用户关于Apache Flink工作原理和应用案例的查询。首先,用户希望了解Flink的详细信息和使用方法,特别是其工作原理和实际应用案例。根据用户提供的引用内容,我需要整合这些信息,确保回答结构清晰,符合用户的要求,并且正确引用相关资料。 首先,用户提到的引用[2]指出Flink是一个支持流处理和批处理的开源平台,基于同一个运行时。这应该是Flink的核心特点之一,需要强调。引用[4]提到Flink使用多线程和TaskSlot机制进行资源管理,这与MapReduce的多进程方式不同,提升了CPU效率。这部分内容对解释工作原理很重要。 接下来是应用案例。用户提供的引用[1]和[3]提到了Flink在大规模数据流处理、事件驱动应用以及具体案例分析,比如实时风控和电商实时推荐。这些可以作为实际应用的例子。引用[5]关于Apache Beam的示例代码可能关联到Flink作为执行引擎的应用,但用户主要关注Flink本身,所以可能不需要深入Beam的内容,但可以简要提及与其他工具的集成。 用户要求回答结构清晰,分点说明工作原理和应用案例。同时,需要遵循系统级指令,正确使用LaTeX格式,例如数学表达式用$...$,独立公式用$$...$$。但当前问题涉及的是技术原理,可能数学公式较少,但如果有涉及计算模型或公式的地方需要正确应用。 另外,用户要求生成相关问题,我需要根据回答内容提出三个相关的问题,确保问题与Flink的工作原理、优化方法、应用场景相关。例如,可以涉及Flink的容错机制、与Spark Streaming的对比,以及实时数据分析的具体实现等。 需要检查引用是否正确标注,每个引用末尾添加[^n]。例如,当提到Flink的架构原理时,引用[2]和[4]支持;应用案例部分引用[1]、[3]和可能的[5]。 现在,组织回答结构: 1. Flink工作原理 - 分布式架构:JobManager、TaskManager、TaskSlot - 流批一体:运行时统一,引用[2] - 资源管理:TaskSlot机制,引用[4] - 状态管理与容错:检查点机制,Chandy-Lamport算法 2. 应用案例 - 实时风控(引用[1]) - 电商实时推荐(引用[3]) - 物联网数据处理(可能结合一般知识补充) 3. 使用示例(代码示例) - 流处理示例,引用[5]的代码结构,但需要调整成Flink的API 最后,生成相关问题,确保覆盖工作原理、应用场景和对比分析。 需要确保LaTeX正确,例如在提到检查点机制时,如果有数学公式,比如分布式快照的算法,使用$$...$$。例如,Chandy-Lamport算法的标记过程可能需要公式,但如果没有具体公式,可能不需要。可能不需要数学表达式,但需注意用户提到的系统指令中的要求,确保符合格式。 现在开始撰写正式回答,确保各部分引用正确,结构清晰,并附上相关问题。</think>### Apache Flink 工作原理及应用案例 #### 一、Apache Flink 的核心工作原理 1. **分布式架构设计** Flink 的运行时由两类进程构成: - **JobManager**:负责作业调度、检查点协调和故障恢复。 - **TaskManager**:执行具体的任务(Task),通过**TaskSlot**机制管理资源,每个 TaskSlot 代表一个固定大小的资源单元[^4]。 这种设计使得 Flink 可以高效利用 CPU 和内存资源,支持高并发任务。 2. **流批一体运行时** Flink 基于统一的运行时(Flink Runtime),同时支持流处理和批处理。其核心模型是**数据流(DataStream)**,将批处理视为有限流的特例,通过以下公式表示流处理的计算逻辑: $$ \text{Result} = \sum_{t \in T} f(\text{Data}_t) $$ 其中 $T$ 是时间域,$f$ 是用户定义的计算函数[^2]。 3. **状态管理与容错** Flink 通过**分布式快照**(Chandy-Lamport 算法)实现状态一致性。周期性生成检查点(Checkpoint),确保故障时能回滚到最近一致状态[^4]。例如,检查点间隔可通过配置 `checkpoint.interval` 参数调整。 4. **事件时间与水位线** 支持事件时间(Event Time)语义,通过水位线(Watermark)机制处理乱序事件。水位线 $W$ 表示“时间进展到 $t$”,确保窗口计算的正确性。 --- #### 二、Flink 的应用案例 1. **实时风险控制(金融领域)** - **场景**:监测交易流水,识别异常行为(如高频交易)。 - **实现**:通过 `DataStream API` 定义规则引擎,结合 CEP(复杂事件处理)库匹配异常模式[^1]。 2. **电商实时推荐系统** - **场景**:根据用户实时浏览行为更新推荐列表。 - **实现**:使用 Flink SQL 分析用户点击流,动态更新 Redis 中的推荐结果[^3]。 3. **物联网设备监控** - **场景**:实时分析传感器数据,触发告警(如温度超标)。 - **实现**:通过 `WindowedStream` 计算滑动窗口内的指标均值,输出到 Kafka 或 Dashboard。 --- #### 三、Flink 代码示例(流处理) ```java // 定义流处理环境 StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // 从Kafka读取数据 DataStream<String> stream = env .addSource(new FlinkKafkaConsumer<>("topic", new SimpleStringSchema(), properties)); // 数据处理:过滤并统计 stream.filter(s -> s.contains("ERROR")) .map(s -> new Tuple2<>(s, 1)) .keyBy(0) .sum(1) .print(); // 执行作业 env.execute("Real-time Error Counter"); ``` 此代码实现了从 Kafka 消费日志流,实时统计错误信息频次[^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值