flink定时器(Timer)
定时器(Timer)和定时服务(TimerService)
只有在 KeyedStream 中才支持使用 TimerService 设置定时器的 操作。所以一般情况下,我们都是先做了 keyBy 分区之后,再去定义处理操作;代码中更加常见的处理函数是 KeyedProcessFunction,最基本的 ProcessFunction 反而出镜率没那么高。
接下来我们就先从定时服务(TimerService)入手,详细讲解 KeyedProcessFunction 的用法。
KeyedProcessFunction 的一个特色,就是可以灵活地使用定时器。
定时器(timers)是处理函数中进行时间相关操作的主要机制。在.onTimer()方法中可以实 现定时处理的逻辑,而它能触发的前提,就是之前曾经注册过定时器、并且现在已经到了触发 时间。注册定时器的功能,是通过上下文中提供的“定时服务”(TimerService)来实现的。
TimerService 是 Flink 关于时间和定时器的基础服务接口,包含以下六个方法:
// 获取当前的处理时间
long currentProcessingTime();
// 获取当前的水位线(事件时间)
long currentWatermark();
// 注册处理时间定时器,当处理时间超过 time 时触发
void registerProcessingTimeTimer(long time);
// 注册事件时间定时器,当水位线超过 time 时触发
void registerEventTimeTimer(long time);
// 删除触发时间为 time 的处理时间定时器
void deleteProcessingTimeTimer(long time);
// 删除触发时间为 time 的事件时间定时器
void deleteEventTimeTimer(long time)
六个方法可以分成两大类:基于处理时间和基于事件时间。而对应的操作主要有三个:获 取当前时间,注册定时器,以及删除定时器。需要注意,尽管处理函数中都可以直接访问 TimerService,不过只有基于 KeyedStream 的处理函数,才能去调用注册和删除定时器的方法; 未作按键分区的 DataStream 不支持定时器操作,只能获取当前时间。
基于 KeyedStream 注册定时器时,会传入一个定时器触发的时间戳,这个时间戳的定时器对于每个 key 都是有效的。这样,我们的代码并不需要做额外的处理,底层就可以直接对不同 key 进行独立的处理操作了。
这里注意定时器的时间戳必须是毫秒数,所以我们得到整秒之后还要乘以 1000。定时器 默认的区分精度是毫秒。 另外 Flink 对.onTimer()和.processElement()方法是同步调用的(synchronous),所以也不会 出现状态的并发修改。 Flink 的定时器同样具有容错性,它和状态一起都会被保存到一致性检查点(checkpoint) 中。当发生故障时,Flink 会重启并读取检查点中的状态,恢复定时器。如果是处理时间的定时器,有可能会出现已经“过期”的情况,这时它们会在重启时被立刻触发。
KeyedProcessFunction 的使用
KeyedProcessFunction 可以说是处理函数中的“嫡系部队”,可以认为是 ProcessFunction 的 一个扩展。我们只要基于 keyBy 之后的 KeyedStream,直接调用.process()方法,这时需要传入 的参数就是 KeyedProcessFunction 的实现类。
stream.keyBy( t -> t.f0 )
.process(new MyKeyedProcessFunction())
类似地,KeyedProcessFunction 也是继承自 AbstractRichFunction 的一个抽象类,源码中定 义如下:
public abstract class KeyedProcessFunction<K, I, O> extends AbstractRichFunction
{
...
public abstract void processElement(I value, Context ctx, Collector<O> out)
throws Exception;
public void onTimer(long timestamp, OnTimerContext ctx, Collector<O> out)
throws Exception {}
public abstract class Context {...}
...
}
可以看到与 ProcessFunction 的定义几乎完全一样,区别只是在于类型参数多了一个 K, 这是当前按键分区的 key 的类型。同样地,我们必须实现一个.processElement()抽象方法,用来处理流中的每一个数据;另外还有一个非抽象方法.onTimer(),用来定义定时器触发时的回调操作。由于定时器只能在 KeyedStream 上使用,所以到了 KeyedProcessFunction 这里,我们 才真正对时间有了精细的控制,定时方法.onTimer()才真正派上了用场。
下面是一个使用处理时间定时器的具体示例:
package com.ambitfly.timer;
import com.ambitfly.pojo.Event;
import com.ambitfly.source.ClickSource;
import org.apache.flink.api.common.functions.RuntimeContext;
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.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import java.sql.Timestamp;
public class ProcessingTimeTimerTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource());
stream.keyBy(data->data.user).process(new KeyedProcessFunction<String, Event, String>() {
@Override
public void processElement(Event event, KeyedProcessFunction<String, Event, String>.Context ctx, Collector<String> out) throws Exception {
Long currTs = ctx.timerService().currentProcessingTime();
out.collect(ctx.getCurrentKey()+" 数据到达,到达时间:"+new Timestamp(currTs));
// 注册一个十秒后的定时器
ctx.timerService().registerProcessingTimeTimer(currTs + 10*1000L);
}
@Override
public void onTimer(long timestamp, KeyedProcessFunction<String, Event, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
out.collect(ctx.getCurrentKey()+" 定时器触发时间"+new Timestamp(timestamp));
}
}).print();
env.execute();
}
}
在上面的代码中,由于定时器只能在 KeyedStream 上使用,所以先要进行 keyBy;这里 的.keyBy(data -> true)是将所有数据的 key 都指定为了 true,其实就是所有数据拥有相同的 key, 会分配到同一个分区。
之后我们自定义了一个 KeyedProcessFunction,其中.processElement()方法是每来一个数据 都会调用一次,主要是定义了一个 10 秒之后的定时器;而.onTimer()方法则会在定时器触发时 调用。所以我们会看到,程序运行后先在控制台输出“数据到达”的信息,等待 10 秒之后, 又会输出“定时器触发”的信息,打印出的时间间隔正是 10 秒。
当然,上面的例子是处理时间的定时器,所以我们是真的需要等待 10 秒才会看到结果。 事件时间语义下,又会有什么不同呢?我们可以对上面的代码略作修改,做一个测试:
package com.ambitfly.timer;
import com.ambitfly.pojo.Event;
import com.ambitfly.source.ClickSource;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import java.sql.Timestamp;
public class EventTimeTimerTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<Event> stream = env.addSource(new ClickSource())
.assignTimestampsAndWatermarks(WatermarkStrategy.<Event>forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
@Override
public long extractTimestamp(Event event, long recordTimestamp) {
return event.timestamp;
}
}));
stream.keyBy(data->data.user).process(new KeyedProcessFunction<String, Event, String>() {
@Override
public void processElement(Event event, KeyedProcessFunction<String, Event, String>.Context ctx, Collector<String> out) throws Exception {
Long currTs = ctx.timestamp();
out.collect(ctx.getCurrentKey()+" 数据到达,时间戳:"+new Timestamp(currTs) + " watermark: " + ctx.timerService().currentWatermark());
// 注册一个十秒后的定时器
ctx.timerService().registerEventTimeTimer(currTs + 10*1000L);
}
@Override
public void onTimer(long timestamp, KeyedProcessFunction<String, Event, String>.OnTimerContext ctx, Collector<String> out) throws Exception {
out.collect(ctx.getCurrentKey()+" 定时器触发时间"+new Timestamp(timestamp));
}
}).print();
env.execute();
}
}