DataStream API 简介 & Flink 出租车实验

该练习的重点是充分全面地了解 DataStream API,以便于编写流式应用入门。

什么能被转化成流?

Flink 的 Java 和 Scala DataStream API 可以将任何可序列化的对象转化为流。Flink 自带的序列化器有

基本类型,即 String、Long、Integer、Boolean、Array
复合类型:Tuples、POJOs 和 Scala case classes
而且 Flink 会交给 Kryo 序列化其他类型。也可以将其他序列化器和 Flink 一起使用。特别是有良好支持的 Avro。

Java tuples 和 POJOs

Flink 的原生序列化器可以高效地操作 tuples 和 POJOs

Tuples

对于 Java,Flink 自带有 Tuple0 到 Tuple25 类型。后面的数字表示可容纳的单个元祖元素的数据量。


Tuple1<Integer> tuple1 =  Tuple1.of( 1 );
Integer t1_f0 = tuple1.f0;

Tuple2<Integer, String> tuple2 =  Tuple2.of( 1, "Hello" );
Integer t2_f0 = tuple2.f0;
String t2_f1 = tuple2.f1;


Tuple3<Integer, String, Long> tuple3 =  Tuple3.of( 1, "Hello", 1L);
Integer t3_f0 = tuple3.f0;
String t3_f1 = tuple3.f1;
Long t3_f2 = tuple3.f2;

POJOs

如果满足以下条件,Flink 将数据类型识别为 POJO 类型(并允许“按名称”字段引用):

该类是公有且独立的(没有非静态内部类)
该类有公有的无参构造函数
类(及父类)中所有的所有不被 static、transient 修饰的属性要么是公有的(且不被 final 修饰),要么是包含公有的 getter 和 setter 方法,这些方法遵循 Java bean 命名规范。
示例:

public class Person {
    public String name;  
    public Integer age;  
    public Person() {};  
    public Person(String name, Integer age) {  
        . . .
    };  
}  

Person person = new Person("Fred Flintstone", 35);

Flink 的序列化器支持的 POJO 类型数据结构升级。

Scala tuples 和 case classes

如果你了解 Scala,那一定知道 tuple 和 case class。

转换成流
Stream 执行环境

每个 Flink 应用都需要有执行环境,在该示例中为 env。流式应用需要用到 StreamExecutionEnvironment。

DataStream API 将你的应用构建为一个 job graph,并附加到 StreamExecutionEnvironment 。当调用 env.execute() 时此 graph 就被打包并发送到 JobManager 上,后者对作业并行处理并将其子任务分发给 Task Manager 来执行。每个作业的并行子任务将在 task slot 中执行。
注意,如果没有调用 execute(),应用就不会运行。

在这里插入图片描述
此分布式运行时取决于你的应用是否是可序列化的。它还要求所有依赖对集群中的每个节点均可用。

基本的 stream source
// 指定执行环境

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();


// env.fromCollection(..)
List<Person> people = new ArrayList<Person>();

people.add(new Person("Fred", 35));
people.add(new Person("Wilma", 35));
people.add(new Person("Pebbles", 2));

DataStream<Person> flintstones = env.fromCollection(people);

//Tuples
Tuple2<String, Integer> person = Tuple2.of("Fred", 35);
// zero based index!
String name = person.f0;
Integer age = person.f1;

// env.fromCollection(..)
List<Person> people = new ArrayList<>();
people.add(new Person("Fred", 35));
people.add(new Person("Wilma", 35));
people.add(new Person("Pebbles", 2));

// DataStream<Person> flintstones = env.fromCollection(people);

// env.fromElements
DataStream<Person> flintstones = env.fromElements(
	new Person("Fred", 35),
	new Person("Wilma", 35),
	new Person("Pebbles", 2));
 
// env.socketTextStream
DataStreamSource<String> streamSource = env.socketTextStream("localhost", 9000, "\n");

// env.readTextFile
DataStream<String> lines = env.readTextFile("file:///path");

Flink 出租车实验 ◆ 行程清理

项目地址:ride-cleansing

目的: 清理纽约市以外的行程。

知识点:
主要通过 filter api 对数据源进行过滤来达到形成清理的目的,过滤排除经纬度在纽约市之外的行程。

API: org.apache.flink.streaming.api.datastream.DataStream#filter(FilterFunction<T> filter);

...
DataStream<TaxiRide> filteredRides = rides
			// 过滤掉不在纽约启动或停止的行程
			.filter(new NYCFilter());
...

private static class NYCFilter implements FilterFunction<TaxiRide> {

@Override
	public boolean filter(TaxiRide taxiRide) throws Exception {
		//throw new MissingSolutionException();
		return GeoUtils.isInNYC(taxiRide.startLon,taxiRide.startLat)
				&& GeoUtils.isInNYC(taxiRide.endLon, taxiRide.endLat);

	}
}
Flink 出租车实验 ◆ 行程信息与计价信息合并

项目地址:rides-and-fares

目的: 将行程信息与计价信息合并,可以同时获取到行程信息和行程计价信息

知识点:
主要是通过将两个流合并成一个流来达到目的,其中通过 KeyedStream 分区合并,ValueState 记录状态。

API:

  • org.apache.flink.streaming.api.functions.co.RichCoFlatMapFunction<IN1, IN2, OUT>
    将两个流合并成一个流输出
  • org.apache.flink.api.common.state.ValueState<T>
    参考文档:Working with State

疑问:
这里假定了 行程事件和计价事件完全匹配的过程,在实际过程中,可能行程或者计价事件缺失其中一个,导致另一个事件将永远处于状态中,后面的实验 (Long Ride Alerts)我们可以通过计时器可能有助于解决这个问题。


public static class EnrichmentFunction extends RichCoFlatMapFunction<TaxiRide, TaxiFare, Tuple2<TaxiRide, TaxiFare>> {

		private ValueState<TaxiRide> rideState;
		private ValueState<TaxiFare> fareState;

		@Override
		public void open(Configuration config) throws Exception {
			//throw new MissingSolutionException();
			rideState = getRuntimeContext().getState(new ValueStateDescriptor<>("saved ride", TaxiRide.class));
			fareState = getRuntimeContext().getState(new ValueStateDescriptor<>("saved taxiFare", TaxiFare.class));

		}

		@Override
		public void flatMap1(TaxiRide ride, Collector<Tuple2<TaxiRide, TaxiFare>> out) throws Exception {

			TaxiFare fare = fareState.value();
			if (fare == null) {
				rideState.update( ride );
				// 如果暂时没有消费产生,则先暂存状态
			} else {
				fareState.clear();
				out.collect(Tuple2.of(ride, fare));
			}

		}

		@Override
		public void flatMap2(TaxiFare fare, Collector<Tuple2<TaxiRide, TaxiFare>> out) throws Exception {
			TaxiRide ride = rideState.value();
			if (ride == null) {
				fareState.update( fare );
			} else {
				rideState.clear();
				out.collect( Tuple2.of( ride, fare) );
			}
		}
	}

Flink 出租车实验 ◆ 超时行程警告(Long Ride Alerts)

项目地址:long-ride-alerts

目的: 统计出超过2个小时仍未结束的行程

知识点:

ProcessFunction是一个低级的流处理操作,允许访问所有流应用程序的基本构件:

  • events (stream elements)
  • state (fault-tolerant, consistent, only on keyed stream)
  • timers (event time and processing time, only on keyed stream)

ProcessFunction 可以被认为是一种提供了对 KeyedState 和定时器访问的 FlatMapFunction。每在输入流中接收到一个事件,就会调用来此函数来处理。对于容错的状态,ProcessFunction 可以通过 RuntimeContext 访问 KeyedState,类似于其他有状态函数访问 KeyedState。

Timers 定时器可以对处理时间和事件时间的变化做一些处理。每次调用 processElement() 都可以获得一个 Context 对象,通过该对象可以访问元素的事件时间戳以及 TimerService。TimerService 可以为尚未发生的事件时间/处理时间实注册回调。当定时器到达某个时刻时,会调用 onTimer() 方法。在调用期间,所有状态再次限定为定时器创建的键,允许定时器操作 KeyedState。

具体可以参考: https://blog.csdn.net/boling_cavalry/article/details/106040312

API:
org.apache.flink.streaming.api.TimerService#registerEventTimeTime(long time);
注册一个指定时间戳的触发器

org.apache.flink.streaming.api.TimerService#deleteEventTimeTimer(long time);
清除一个指定时间戳的触发器

public static class MatchFunction extends KeyedProcessFunction<Long, TaxiRide, TaxiRide> {

		private ValueState<TaxiRide> rideState;

		@Override
		public void open(Configuration config) throws Exception {
			rideState = getRuntimeContext().getState(new ValueStateDescriptor<>("saved rides", TaxiRide.class));
		}

@Override
		public void processElement(TaxiRide ride, Context context, Collector<TaxiRide> out) throws Exception {
			TimerService timerService = context.timerService();

			TaxiRide preRide = rideState.value();
			if (preRide == null) {
				// 如果不存在,则记录本次事件,与下次事件对比
				rideState.update(ride);
				if (ride.isStart) {
					// 注册触发器时间为 2小时以后事件
					context.timerService().registerEventTimeTimer(getTimerTime(ride));
				}
			} else {

				if (!ride.isStart) {
					// 如果本次事件是一个结束事件,并且之前有记录过事件,则取消触发器
					context.timerService().deleteEventTimeTimer(getTimerTime(preRide));
				}

				rideState.clear();

			}
		}
		
		@Override
		public void onTimer(long timestamp, OnTimerContext context, Collector<TaxiRide> out) throws Exception {
			// 到达指定时间戳执行合并流汇集操作
			out.collect(rideState.value());
			rideState.clear();

		}

		/**
		 * 获取开始行程2小时后时间戳
		 * @param ride
		 * @return
		 */
		private long getTimerTime(TaxiRide ride) {
			return ride.startTime.plusSeconds(120 * 60).toEpochMilli();
		}

	}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值