Apache flink 为什么Flink会在数据流联接+;全球窗口?

Apache flink 为什么Flink会在数据流联接+;全球窗口?,apache-flink,Apache Flink,我正在学习/试验Flink,我正在观察DataStream连接的一些意外行为,我想了解发生了什么 假设我有两个流,每个流有10条记录,我想在id字段中加入它们。让我们假设一个流中的每个记录在另一个流中都有一个匹配的记录,并且ID在每个流中都是唯一的。也就是说,我必须使用一个全局窗口(需求) 使用DataStream API(我在Scala中的简化代码)加入: 结果: 所有内容都按预期打印,第一个流中的每条记录与第二个流中的一条记录合并 然而: 如果我将其中一个记录(例如,带有更新的字段)从

我正在学习/试验Flink,我正在观察DataStream连接的一些意外行为,我想了解发生了什么

假设我有两个流,每个流有10条记录,我想在
id
字段中加入它们。让我们假设一个流中的每个记录在另一个流中都有一个匹配的记录,并且ID在每个流中都是唯一的。也就是说,我必须使用一个全局窗口(需求)

使用DataStream API(我在Scala中的简化代码)加入:

结果:

  • 所有内容都按预期打印,第一个流中的每条记录与第二个流中的一条记录合并
然而:


  • 如果我将其中一个记录(例如,带有更新的字段)从一个流重新发送到该流,则会发出两个重复的连接事件问题是,记录永远不会从全局窗口中删除。因此,只要有新记录到达,但旧记录仍然存在,就在全局窗口上触发联接操作

    因此,为了让它在您的案例中运行,您需要实现一个定制的。我在一个最小的工作示例中扩展了您的示例,并添加了驱逐器,我将在代码片段之后解释

    val data1 = List(
      (1L, "myId-1"),
      (2L, "myId-2"),
      (5L, "myId-1"),
      (9L, "myId-1"))
    
    val data2 = List(
      (3L, "myId-1", "myValue-A"))
    
    val stream1 = env.fromCollection(data1)
    val stream2 = env.fromCollection(data2)
    
    stream1.join(stream2)
      .where(_._2).equalTo(_._2)
      .window(GlobalWindows.create()) // assume this is a requirement
      .trigger(CountTrigger.of(1))
      .evictor(new Evictor[CoGroupedStreams.TaggedUnion[(Long, String), (Long, String, String)], GlobalWindow](){
        override def evictBefore(elements: lang.Iterable[TimestampedValue[CoGroupedStreams.TaggedUnion[(Long, String), (Long, String, String)]]], size: Int, window: GlobalWindow, evictorContext: Evictor.EvictorContext): Unit = {}
    
        override def evictAfter(elements: lang.Iterable[TimestampedValue[CoGroupedStreams.TaggedUnion[(Long, String), (Long, String, String)]]], size: Int, window: GlobalWindow, evictorContext: Evictor.EvictorContext): Unit = {
          import scala.collection.JavaConverters._
          val lastInputTwoIndex = elements.asScala.zipWithIndex.filter(e => e._1.getValue.isTwo).lastOption.map(_._2).getOrElse(-1)
          if (lastInputTwoIndex == -1) {
            println("Waiting for the lookup value before evicting")
            return
          }
          val iterator = elements.iterator()
          for (index <- 0 until size) {
            val cur = iterator.next()
            if (index != lastInputTwoIndex) {
              println(s"evicting ${cur.getValue.getOne}/${cur.getValue.getTwo}")
              iterator.remove()
            }
          }
        }
      })
      .apply((r, l) => (r, l))
      .print()
    

    最后一句话:如果表API已经提供了一种简洁的方式来做您想要的事情,我会坚持使用它,然后在需要时使用它。

    问题是,记录永远不会从全局窗口中删除。因此,只要有新记录到达,但旧记录仍然存在,就在全局窗口上触发联接操作

    因此,为了让它在您的案例中运行,您需要实现一个定制的。我在一个最小的工作示例中扩展了您的示例,并添加了驱逐器,我将在代码片段之后解释

    val data1 = List(
      (1L, "myId-1"),
      (2L, "myId-2"),
      (5L, "myId-1"),
      (9L, "myId-1"))
    
    val data2 = List(
      (3L, "myId-1", "myValue-A"))
    
    val stream1 = env.fromCollection(data1)
    val stream2 = env.fromCollection(data2)
    
    stream1.join(stream2)
      .where(_._2).equalTo(_._2)
      .window(GlobalWindows.create()) // assume this is a requirement
      .trigger(CountTrigger.of(1))
      .evictor(new Evictor[CoGroupedStreams.TaggedUnion[(Long, String), (Long, String, String)], GlobalWindow](){
        override def evictBefore(elements: lang.Iterable[TimestampedValue[CoGroupedStreams.TaggedUnion[(Long, String), (Long, String, String)]]], size: Int, window: GlobalWindow, evictorContext: Evictor.EvictorContext): Unit = {}
    
        override def evictAfter(elements: lang.Iterable[TimestampedValue[CoGroupedStreams.TaggedUnion[(Long, String), (Long, String, String)]]], size: Int, window: GlobalWindow, evictorContext: Evictor.EvictorContext): Unit = {
          import scala.collection.JavaConverters._
          val lastInputTwoIndex = elements.asScala.zipWithIndex.filter(e => e._1.getValue.isTwo).lastOption.map(_._2).getOrElse(-1)
          if (lastInputTwoIndex == -1) {
            println("Waiting for the lookup value before evicting")
            return
          }
          val iterator = elements.iterator()
          for (index <- 0 until size) {
            val cur = iterator.next()
            if (index != lastInputTwoIndex) {
              println(s"evicting ${cur.getValue.getOne}/${cur.getValue.getTwo}")
              iterator.remove()
            }
          }
        }
      })
      .apply((r, l) => (r, l))
      .print()
    

    最后一句话:如果表API已经提供了一种简洁的方式来完成您想要的操作,我会坚持使用它,然后在需要时使用。

    您是否可以更具体地说明“如果我将一条记录(例如,带有更新的字段)从一个流重新发送到该流,将发出两个重复的连接事件。”你能举个简单的例子吗?例如,如果流1只有一条记录KeyValueRecord(1,10),流2只有一条KeyValueRecord(1,42),我的应用程序会打印[KeyValueRecord(1,10),KeyValueRecord(1,42)],因为两条记录都有相同的键“1”。如果以后,我将新记录KeyValueRecord(1,11)推送到流1,我的应用程序不仅会再次打印以前的[KeyValueRecord(1,10),KeyValueRecord(1,42)],而且还会打印[KeyValueRecord(1,11),KeyValueRecord(1,42)](我只希望是后者)。如果我再次推送相同的记录,它将打印相同的记录,再加上[KeyValueRecord(1,11),KeyValueRecord(1,42)],等等……您能否更具体地说“如果我将其中一个记录(例如,带有更新的字段)从一个流重新发送到该流,将发出两个重复的连接事件。”你能举个简单的例子吗?例如,如果流1只有一条记录KeyValueRecord(1,10),流2只有一条KeyValueRecord(1,42),我的应用程序会打印[KeyValueRecord(1,10),KeyValueRecord(1,42)],因为两条记录都有相同的键“1”。如果以后,我将新记录KeyValueRecord(1,11)推送到流1,我的应用程序不仅会再次打印以前的[KeyValueRecord(1,10),KeyValueRecord(1,42)],而且还会打印[KeyValueRecord(1,11),KeyValueRecord(1,42)](我只希望是后者)。如果我再推同一条记录,它会打印出同样的内容,再加上[KeyValueRecord(1,11),KeyValueRecord(1,42)],等等……谢谢你的详细回答!我将更深入地研究驱逐器:)我还尝试实现自己的KeyedCoProcessFunction来加入流,它也如预期的那样工作(尽管我更喜欢使用更高级的API)。再次感谢!谢谢你的详细回答!我将更深入地研究驱逐器:)我还尝试实现自己的KeyedCoProcessFunction来加入流,它也如预期的那样工作(尽管我更喜欢使用更高级的API)。再次感谢!
    Waiting for the lookup value before evicting
    Waiting for the lookup value before evicting
    Waiting for the lookup value before evicting
    Waiting for the lookup value before evicting
    4> ((1,myId-1),(3,myId-1,myValue-A))
    4> ((5,myId-1),(3,myId-1,myValue-A))
    4> ((9,myId-1),(3,myId-1,myValue-A))
    evicting (1,myId-1)/null
    evicting (5,myId-1)/null
    evicting (9,myId-1)/null