多流转换

简单划分,多流转换可以分为“分流”和“合流”两大类

目前分流的操作一般是通过侧输出流(side output)来实现

合流的算子比较丰富,根据不同的需求可以调用 union、 connect、join 以及 coGroup 等接口进行连接合并操作


分流

分流如果采用 filter算子 来实现,其本质是将原始数据流 stream 复制三份,然后对每一份分别做筛选来得到不同的流,这显然是不高效的

取而代之,我们可以使用处理函数(process function)的侧输出流(side output)来实现分流

简单来说,只需要调用上下文 ctx 的.output()方法,就可以输出任意类型的数据

而侧输出流的标记和提取,都离不开一个“输出标签”(OutputTag),注这里侧输出流的类型可以和主流不一样

// 定义输出标签,侧输出流的类型可以和主流不一样
OutputTag<Tuple3<String, String, Long>> maryTag = new OutputTag<Tuple3<String, String, Long>>("Mary"){};
OutputTag<Tuple3<String, String, Long>> bobTag = new OutputTag<Tuple3<String, String, Long>>("Bob"){};

SingleOutputStreamOperator<Event> processedStream = stream.process(new ProcessFunction<Event, Event>() {
	@Override
	public void processElement(Event event, ProcessFunction<Event, Event>.Context context, Collector<Event> collector) throws Exception {
		if (event.user.equals("Mary"))
            // 输出到分支
			context.output(maryTag, Tuple3.of(event.user, event.url, event.timestamp));
		else if (event.user.equals("Bob"))
			context.output(bobTag, Tuple3.of(event.user, event.url, event.timestamp));
		else
			collector.collect(event);
	}
});

// 获取侧输出流 并打印输出
processedStream.print("else");
processedStream.getSideOutput(maryTag).print("Mary");
processedStream.getSideOutput(bobTag).print("Bob");

合流

联合(Union)

stream1.union(stream2, stream3, ...)

在事件时间语义下,水位线是时间的进度标志;不同的流中可能水位线的进展快慢完全不同,如果它们合并在一起,水位线又该以哪个为准呢?

考虑到水位线的本质是“之前的所有数据已经到齐了”,所以对于合流之后的 水位线,也是要以最小的那个为准,这样才可以保证所有流都不会再传来之前的数据


连接(Connect)

流的联合虽然简单,不过受限于数据类型不能改变,灵活性大打折扣,所以实际应用较少出现

连接操作允许流的数据类型不同,但是它只能连接两条流,而union可以合并多条流

我们知道一个 DataStream 中的 数据只能有唯一的类型,所以连接得到的并不是 DataStream,而是一个“连接流” (ConnectedStreams)

连接流可以看成是两条流形式上的 “统一”,被放在了一个同一个流中; 事实上内部仍保持各自的数据形式不变,彼此之间是相互独立的

要想得到新的 DataStream, 还需要进一步定义一个“同处理”(co-process)转换操作,用来说明对于不同来源、不同类型 的数据,怎样分别进行处理转换、得到统一的输出类型

DataStreamSource<Integer> stream1 = env.fromElements(1, 2, 3);
DataStreamSource<Long> stream2 = env.fromElements(4L, 5L, 6L, 7L);

// 对两条不同类型的流进行处理,统一类型后输出
stream2.connect(stream1).map(new CoMapFunction<Long, Integer, String>() {
	@Override
	public String map1(Long aLong) throws Exception {
		return "Long: " + aLong.toString();
	}

	@Override
	public String map2(Integer integer) throws Exception {
		return "Integer: " + integer.toString();
	}
}).print();

案例:实时对账需求

我们可以通过CoProcessFunction来实现一个实时对账的需求,也就是 app 的支付操作和第三方的支付操作的一个双流 Join

App 的支付事件和第三方的支付事件将 会互相等待 5 秒钟,如果等不来对应的支付事件,那么就输出报警信息

package split_stream;

import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple3;
import org.apache.flink.api.java.tuple.Tuple4;
import org.apache.flink.configuration.Configuration;
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.streaming.api.functions.co.CoProcessFunction;
import org.apache.flink.util.Collector;

import java.time.Duration;

/**
 * @author hdbing
 * @create 2022-11-06 12:37
 */
public class SplitStreamTest03_BillCheckExample {
	public static void main(String[] args) throws Exception {
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(1);

		// 来自app的支付日志,(单号,来源,时间戳)
		SingleOutputStreamOperator<Tuple3<String, String, Long>> appStream = env.fromElements(
				Tuple3.of("order-1", "app", 1000L),
				Tuple3.of("order-2", "app", 2000L),
				Tuple3.of("order-3", "app", 3500L)
		).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple3<String, String, Long>>forBoundedOutOfOrderness(Duration.ZERO)
				.withTimestampAssigner(new SerializableTimestampAssigner<Tuple3<String, String, Long>>() {
					@Override
					public long extractTimestamp(Tuple3<String, String, Long> stringStringLongTuple3, long l) {
						return stringStringLongTuple3.f2;
					}
				})
		);

		// 来自第三方支付平台的支付日志,(单号,来源,支付状态,时间戳)
		SingleOutputStreamOperator<Tuple4<String, String, String, Long>> thirdpartStream = env.fromElements(
				Tuple4.of("order-1", "third-party", "success", 3000L),
				Tuple4.of("order-3", "third-party", "success", 4000L)
		).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple4<String, String, String, Long>>forBoundedOutOfOrderness(Duration.ZERO)
				.withTimestampAssigner(new SerializableTimestampAssigner<Tuple4<String, String, String, Long>>() {
					@Override
					public long extractTimestamp(Tuple4<String, String, String, Long> event, long l) {
						return event.f3;
					}
				})
		);

		// 检测统一支付单在两条流中是否匹配,不匹配就报警
		// 找得到就匹配,那么找不到呢?不可能一直等下去,等一段时间后如果还是没收到 就报警
		appStream.keyBy(data -> data.f0)
				.connect(thirdpartStream.keyBy(data -> data.f0));

		// 两种实现其实是一样的
		appStream.connect(thirdpartStream)
				.keyBy(data -> data.f0, data -> data.f0)
				.process(new OrderMatchResult())
				.print();

		env.execute();
	}

	// 自定义实现CoProcessFunction,分别让一条流等另一条流就行
	public static class OrderMatchResult extends CoProcessFunction<Tuple3<String, String, Long>, Tuple4<String, String, String, Long>, String> {
		// 定义状态变量,用于保存已经到达的事件
		// 如果只是用于判断是否到达,直接定义有个boolean变量即可,这里主要是为了打印信息所以保存了完整事件
		private ValueState<Tuple3<String, String, Long>> appEventState;
		private ValueState<Tuple4<String, String, String, Long>> thirdPartyEventState;

		@Override
		public void open(Configuration parameters) throws Exception {
			appEventState = getRuntimeContext().getState(
					new ValueStateDescriptor<Tuple3<String, String, Long>>("app-event", Types.TUPLE(Types.STRING, Types.STRING, Types.LONG))
			);
			thirdPartyEventState = getRuntimeContext().getState(
					new ValueStateDescriptor<Tuple4<String, String, String, Long>>("thirdparty-event", Types.TUPLE(Types.STRING, Types.STRING, Types.STRING, Types.LONG))
			);
		}

		@Override
		public void processElement1(Tuple3<String, String, Long> value, CoProcessFunction<Tuple3<String, String, Long>, Tuple4<String, String, String, Long>, String>.Context context, Collector<String> collector) throws Exception {
			// 来的是app event,看另一条流中事件是否来过
			if (thirdPartyEventState.value() != null) {
				collector.collect("对账成功:" + value + " " + thirdPartyEventState.value());
				// 清空状态
				thirdPartyEventState.clear();
			} else {
				// 更新状态
				appEventState.update(value);
				// 注册一个5秒后的定时器,开始等待另一条流的事件
				context.timerService().registerEventTimeTimer(value.f2 + 5000L);
			}
		}

		@Override
		public void processElement2(Tuple4<String, String, String, Long> value, CoProcessFunction<Tuple3<String, String, Long>, Tuple4<String, String, String, Long>, String>.Context context, Collector<String> collector) throws Exception {
			// 来的是thirdparty event,看另一条流中事件是否来过
			if (appEventState.value() != null) {
				collector.collect("对账成功:" + appEventState.value() + " " + value);
				// 清空状态
				appEventState.clear();
			} else {
				// 更新状态
				thirdPartyEventState.update(value);
				// 注册一个定时器,开始等待另一条流的事件,这里是假设第一条流先到,就不用再设置延迟了,因为水位线一定不会晚于 当前事件时间
				context.timerService().registerEventTimeTimer(value.f3);
			}
		}

		@Override
		public void onTimer(long timestamp, CoProcessFunction<Tuple3<String, String, Long>, Tuple4<String, String, String, Long>, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
			// 定时器触发,判断状态,如果某个状态不为空,说明另一条流中事件没来,因为到了对账成功肯定都被清空了
			if (appEventState.value() != null) {
				out.collect("对账失败:" + appEventState.value() + " " + "第三方平台支付信息未到");
			}
			if (thirdPartyEventState.value() != null) {
				out.collect("对账失败:" + thirdPartyEventState.value() + " " + "app支付信息未到");
			}
			appEventState.clear();
			thirdPartyEventState.clear();
		}

	}
}

DataStream 调用.connect()方法还可以传入一个“广播流”(BroadcastStream),这时合并两条流得 到的就变成了一个“广播连接流”(BroadcastConnectedStream)

这种连接方式往往用在需要动态定义某些规则或配置的场景

因为规则是实时变动的,所以我们可以用一个单独的流来获取规则数据;而这些规则或配置是对整个应用全局有效的,所以不能只把这数据传递给一个下游并行子任务处理,而是要“广播”(broadcast)给所有的并行子任务。而下游子任务收到广播出来的规则,会把它保存成一个状态,这就是所谓的“广播状态”(broadcast state)


基于时间的合流:双流联结(Join)

窗口联结

事实上,Flink 中两条流 的 connect 操作,就可以通过 keyBy 指定键进行分组后合并,实现了类似于 SQL 中的 join 操作;另外 connect 支持处理函数,可以使用自定义状态和 TimerService 灵活实现各种需求,其 实已经能够处理双流合并的大多数场景

不过处理函数是底层接口,所以尽管 connect 能做的事情多,但在一些具体应用场景下还是显得太过抽象了。比如,如果我们希望统计固定时间内两条流数据的匹配情况,那就需要设置定时器、自定义触发逻辑来实现,其实这完全可以用窗口(window)来表示

Union太过简单,只能合并两条类型相同的流,而Connect调用process来实现定时等又太底层,为了更方便地实现基于时间的合流操作,Flink 的 DataStrema API 提供了两种内置的 join 算子,以及 coGroup 算子


窗口联结的调用如下:

stream1.join(stream2)
       .where(<KeySelector>)
       .equalTo(<KeySelector>)
       .window(<WindowAssigner>)
       .apply(<JoinFunction>)
SingleOutputStreamOperator<Tuple2<String, Long>> stream1 = env.fromElements(
		Tuple2.of("a", 1000L),
		Tuple2.of("b", 1000L),
		Tuple2.of("a", 2000L),
		Tuple2.of("b", 2000L),
		Tuple2.of("b", 5100L)
).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ZERO)
		.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Long>>() {
			@Override
			public long extractTimestamp(Tuple2<String, Long> stringLongTuple2, long l) {
				return stringLongTuple2.f1;
			}
		})
);
SingleOutputStreamOperator<Tuple2<String, Integer>> stream2 = env.fromElements(
		Tuple2.of("a", 3000),
		Tuple2.of("b", 4000),
		Tuple2.of("a", 4500),
		Tuple2.of("b", 5500)
).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple2<String, Integer>>forBoundedOutOfOrderness(Duration.ZERO)
		.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Integer>>() {
			@Override
			public long extractTimestamp(Tuple2<String, Integer> stringLongTuple2, long l) {
				return stringLongTuple2.f1;
			}
		})
);

stream1.join(stream2)
		.where(data -> data.f0)
		.equalTo(data -> data.f0)
		.window(TumblingEventTimeWindows.of(Time.seconds(5)))
		.apply(new JoinFunction<Tuple2<String, Long>, Tuple2<String, Integer>, String>() {
			@Override
			public String join(Tuple2<String, Long> first, Tuple2<String, Integer> second) throws Exception {
				return first + "->" + second;
			}
		}).print();

间隔联结

那么之前的对账需求能否采用窗口联结来实现呢?答案是不行的,因为窗口是指定大小的,如果对账的数据不在窗口内那么就会匹配不到

这种情况可以使用 间隔联结(Interval Join)

对于A间隔联结B,A中的数据元素记为a,B中的数据元素记为b

匹配的条件:a.timestamp + lowerBound <= b.timestamp <= a.timestamp + upperBound

更直观地,如图所示,其中下面一条流表示A,上面一条流表示B

间隔联结的调用如下:

stream1
   .keyBy(<KeySelector>)
   .intervalJoin(stream2.keyBy(<KeySelector>))
   .between(Time.milliseconds(-2), Time.milliseconds(1))
   .process (new ProcessJoinFunction<Integer, Integer, String(){
         @Override
         public void processElement(Integer left, Integer right, Context ctx, Collector<String> out) {
         	out.collect(left + "," + right);
         }
   });

案例:实时推荐

在电商网站中,某些用户行为往往会有短时间内的强关联。我们这里举一个例子,我们有两条流,一条是下订单的流,一条是浏览数据的流。我们可以针对同一个用户,来做这样一个联结。也就是使用一个用户的下订单的事件和这个用户的最近十分钟的浏览数据进行一个联结查询

package split_stream;

import bean.Event;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.JoinFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.ProcessJoinFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;

import java.time.Duration;

/**
 * @author hdbing
 * @create 2022-11-06 11:15
 */
public class SplitStreamTest05_IntervalJoin {
	public static void main(String[] args) throws Exception {
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(1);

		SingleOutputStreamOperator<Tuple2<String, Long>> orderStream = env.fromElements(
				Tuple2.of("Mary", 5000L),
				Tuple2.of("Alice", 5000L),
				Tuple2.of("Bob", 20000L),
				Tuple2.of("Alice", 20000L),
				Tuple2.of("Cary", 51000L)
		).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ZERO)
				.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Long>>() {
					@Override
					public long extractTimestamp(Tuple2<String, Long> stringLongTuple2, long l) {
						return stringLongTuple2.f1;
					}
				})
		);

		SingleOutputStreamOperator<Event> clickStream = env.fromElements(
				new Event("Mary", "./home", 1000L),
				new Event("Alice", "./game", 1500L),
				new Event("Bob", "./cart", 2000L),
				new Event("Alice", "./game1", 18000L),
				new Event("Mary", "./home2", 20000L),
				new Event("Bob", "./cart2", 36000L),
				new Event("Bob", "./cart3", 55000L)
		).assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ZERO)
				.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
					@Override
					public long extractTimestamp(Event event, long l) {
						return event.timestamp;
					}
				})
		);

        // 注意顺序应该为,订单 间隔联结 点击数据
		orderStream.keyBy(data -> data.f0)
				.intervalJoin(clickStream.keyBy(data -> data.user))
				.between(Time.seconds(-5), Time.seconds(10))
				.process(new ProcessJoinFunction<Tuple2<String, Long>, Event, String>() {
					@Override
					public void processElement(Tuple2<String, Long> left, Event right, ProcessJoinFunction<Tuple2<String, Long>, Event, String>.Context context, Collector<String> collector) throws Exception {
						collector.collect(right + "=>" + left);
					}
				}).print();

		env.execute();
	}
}

窗口同组联结(Window CoGroup)

前面介绍了两种基于时间的合流操作,窗口联结和间隔联结

窗口联结是开窗口,然后把窗口内的数据进行两两配对然后进行处理 最后输出结果;

间隔联结是对于一条流中的每一个元素,然后找出另一条流中一段时间间隔内的数据 两两配对处理输出

窗口同组联结不仅可以实现内连接,还可以实现外连接

CoGroup 操作比 窗口的 join 更加通用,不仅可以实现类似 SQL 中的 “内连接”(inner join),也可以实现左外连接(left outer join)、右外连接(right outer join)和 全外连接(full outer join)。事实上,窗口 join 的底层,也是通过 CoGroup 来实现的


调用和窗口联结相似:

stream1.coGroup(stream2)
 	   .where(<KeySelector>)
 	   .equalTo(<KeySelector>)
 	   .window(TumblingEventTimeWindows.of(Time.hours(1)))
 	   .apply(<CoGroupFunction>)
SingleOutputStreamOperator<Tuple2<String, Long>> stream1 = env.fromElements(
		Tuple2.of("a", 1000L),
		Tuple2.of("b", 1000L),
		Tuple2.of("a", 2000L),
		Tuple2.of("b", 2000L),
		Tuple2.of("b", 5100L)
).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ZERO)
		.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Long>>() {
			@Override
			public long extractTimestamp(Tuple2<String, Long> stringLongTuple2, long l) {
				return stringLongTuple2.f1;
			}
		})
);
SingleOutputStreamOperator<Tuple2<String, Integer>> stream2 = env.fromElements(
		Tuple2.of("a", 3000),
		Tuple2.of("b", 4000),
		Tuple2.of("a", 4500)
).assignTimestampsAndWatermarks(WatermarkStrategy.<Tuple2<String, Integer>>forBoundedOutOfOrderness(Duration.ZERO)
		.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Integer>>() {
			@Override
			public long extractTimestamp(Tuple2<String, Integer> stringLongTuple2, long l) {
				return stringLongTuple2.f1;
			}
		})
);

stream1.coGroup(stream2)
		.where(data -> data.f0)
		.equalTo(data -> data.f0)
		.window(TumblingEventTimeWindows.of(Time.seconds(5)))
		.apply(new CoGroupFunction<Tuple2<String, Long>, Tuple2<String, Integer>, String>() {
			@Override
			public void coGroup(Iterable<Tuple2<String, Long>> first, Iterable<Tuple2<String, Integer>> second, Collector<String> collector) throws Exception {
				collector.collect(first + "=>" + second);
			}
		}).print();

打印输出结果,CoGroup输出为窗口内的 Iterable 对象,而对于窗口联结则是输出笛卡尔积后的一条条event,所以说窗口联结底层其实是通过CoGroup实现的;

而且窗口联结对于窗口内没有匹配的数据不会输出,Cogroup则会输出一个空的 Iterable 对象,即外连接

[(a,1000), (a,2000)]=>[(a,3000), (a,4500)]
[(b,1000), (b,2000)]=>[(b,4000)]
[(b,5100)]=>[]
posted @ 2022-11-06 15:02  黄一洋  阅读(38)  评论(0)    收藏  举报