• Flink 双流联结——间隔联结(Interval Join)


    在有些场景下,我们要处理的时间间隔可能并不是固定的。比如,在交易系统中,需要实时地对每一笔交易进行核验,保证两个账户转入转出数额相等,也就是所谓的“实时对账”。两次转账的数据可能写入了不同的日志流,它们的时间戳应该相差不大,所以我们可以考虑只统计一段时间内是否有出账入账的数据匹配。这时显然不应该用滚动窗口或滑动窗口来处理——因为匹配的两个数据有可能刚好“卡在”窗口边缘两侧,于是窗口内就都没有匹配了;会话窗口虽然时间不固定,但也明显不适合这个场景。基于时间的窗口联结已经无能为力了。为了应对这样的需求,Flink提供了一种叫作“间隔联结”(interval join)的合流操作。顾名思义,间隔联结的思路就是针对一条流的每个数据,开辟出其时间戳前后的一段时间间隔,看这期间是否有来自另一条流的数据匹配。

    1、间隔联结的原理 

    间隔联结具体的定义方式是,我们给定两个时间点,分别叫作间隔的“上界”(upperBound)和“下界”(lowerBound);于是对于一条流(不妨叫作A)中的任意一个数据元素a,就可以开辟一段时间间隔:[a.timestamp + lowerBound, a.timestamp + upperBound],即以a的时间戳为中心,下至下界点、上至上界点的一个闭区间:我们就把这段时间作为可以匹配另一条流数据的“窗口”范围。所以对于另一条流(不妨叫B)中的数据元素b,如果它的时间戳落在了这个区间范围内,a和b就可以成功配对,进而进行计算输出结果。所以匹配的条件为:
    a.timestamp + lowerBound <= b.timestamp <= a.timestamp + upperBound

    这里需要注意,做间隔联结的两条流A和B,也必须基于相同的key;下界lowerBound应该小于等于上界upperBound,两者都可正可负;间隔联结目前只支持事件时间语义。如下图所示,我们可以清楚地看到间隔联结的方式:

    下方的流A去间隔联结上方的流B,所以基于A的每个数据元素,都可以开辟一个间隔区间。我们这里设置下界为-2毫秒,上界为1毫秒。于是对于时间戳为2的A中元素,它的可匹配区间就是[0, 3],流B中有时间戳为0、1的两个元素落在这个范围内,所以就可以得到匹配数据对(2, 0)和(2, 1)。同样地,A中时间戳为3的元素,可匹配区间为[1, 4],B中只有时间戳为1的一个数据可以匹配,于是得到匹配数据对(3, 1)。所以我们可以看到,间隔联结同样是一种内连接(inner join)。与窗口联结不同的是,interval join做匹配的时间段是基于流中数据的,所以并不确定;而且流B中的数据可以不只在一个区间内被匹配。

    2、间隔联结的调用

    间隔联结在代码中,是基于KeyedStream的联结(join)操作。DataStream在keyBy得到KeyedStream之后,可以调用.intervalJoin()来合并两条流,传入的参数同样是一个KeyedStream,两者的key类型应该一致;得到的是一个IntervalJoin类型。后续的操作同样是完全固定的:先通过.between()方法指定间隔的上下界,再调用.process()方法,定义对匹配数据对的处理操作。调用.process()需要传入一个处理函数,这是处理函数家族的最后一员:“处理联结函数”ProcessJoinFunction。
    /**
     * Interval Join
     */
    public class IntervalJoinTest {
        public static void main(String[] args) throws Exception {
    
            //1、获取执行时间
            StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            //1.1、便于测试,测试环境设置并行度为 1,生产环境记得设置为 kafka topic 的分区数
            env.setParallelism(1);
            //2.1、读取数据 声明水位线
            SingleOutputStreamOperator<Tuple2<String, Long>> stream = env.fromElements(
                    Tuple2.of("依琳", 5000L),
                    Tuple2.of("令狐冲", 5000L),
                    Tuple2.of("依琳", 20000L),
                    Tuple2.of("令狐冲", 20000L),
                    Tuple2.of("依琳", 51000L))
                    .assignTimestampsAndWatermarks(WatermarkStrategy
                            .<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                            .withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Long>>() {
                                @Override
                                public long extractTimestamp(Tuple2<String, Long> element, long recordTimestamp) {
                                    return element.f1;
                                }
                            }));
            //2.2、读取数据 声明水位线
            SingleOutputStreamOperator<Event> stream1 = env.fromElements(
                    new Event("令狐冲", "/home", 1000L),
                    new Event("依琳", "/cat", 9000L),
                    new Event("任盈盈", "/pay", 36000L),
                    new Event("依琳", "/info?id=2", 30000L),
                    new Event("任盈盈", "/home", 23000L),
                    new Event("依琳", "/error", 33000L))
                    .assignTimestampsAndWatermarks(WatermarkStrategy
                            .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2))
                            .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                                @Override
                                public long extractTimestamp(Event event, long recordTimestamp) {
                                    return event.timestamp;
                                }
                            }));
            //keyby 后 进行 intervalJoin
            stream.keyBy(data -> data.f0)
                    .intervalJoin(stream1.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, Context ctx, Collector<String> out) throws Exception {
                            out.collect(right + " -> " + left);
                        }
                    }).print();
    
            env.execute();
        }
    }

    运行效果

    Event{user='令狐冲', url='/home', timestamp=1970-01-01 08:00:01.0} -> (令狐冲,5000)
    Event{user='依琳', url='/cat', timestamp=1970-01-01 08:00:09.0} -> (依琳,5000)
    Event{user='依琳', url='/info?id=2', timestamp=1970-01-01 08:00:30.0} -> (依琳,20000)
  • 相关阅读:
    web前端的面试真题
    web前端面试真题! 面试的经历和回答只做参考1
    web前端面试真题! 面试的经历和回答只做参考
    html面试资料
    angluar.js的核心介绍
    解决 Chrome支持小于12px 的文字
    div居中效果出现的问题和解决方法
    li和li之间的bug解决方法
    前端面试题笔试考题和答案
    html5新增的标签和使用的方法
  • 原文地址:https://www.cnblogs.com/wdh01/p/16650390.html
Copyright © 2020-2023  润新知