Flink 中的时间语义

flink timestamp 转 日期 flink时间类型_flink

处理时间(Processing Time)

处理时间的概念非常简单,就是指执行处理操作的机器的系统时间。

事件时间(Event Time)

事件时间,是指每个事件在对应的设备上发生的时间,也就是数据生成的时间。

flink timestamp 转 日期 flink时间类型_时间戳_02


另外,除了事件时间和处理时间, Flink 还有一个“摄入时间”( Ingestion Time )的概念,


它是指数据进入 Flink 数据流的时间,也就是 Source 算子读入数据的时间。摄入时间相当于是


事件时间和处理时间的一个中和,它是把 Source 任务的处理时间,当作了数据的产生时间添


加到数据里。这样一来,水位线( watermark )也就基于这个时间直接生成,不需要单独指定


了。这种时间语义可以保证比较好的正确性,同时又不会引入太大的延迟。它的具体行为跟事


件时间非常像,可以当作特殊的事件时间来处理。


水位线

为什么会用到水位线?

主要是解决,分布式数据处理的真确性,如果使用处理时间,那么打比方如果8:59的数据没有在9点的时候到达,那么这个时候8:59的数据就没有办法处理,用到水位线加一定的l乱序延迟那么就可以根据事件的发生时间进行正确的处理

flink timestamp 转 日期 flink时间类型_数据_03


如果出现下游有多个并行子任务的情形,我们 只要将水位线广播出去,就可以通知到所有下游任务当前的时间进度了。


有序流中的水位线(理想情况)

flink timestamp 转 日期 flink时间类型_数据_04

有序流中周期性插入水位线

乱序流中的水位线(实际情况)

flink timestamp 转 日期 flink时间类型_时间戳_05

乱序流

处理方式,如果后面的数据的时间要比水位线小,那么就是不用改变水位线

flink timestamp 转 日期 flink时间类型_ide_06

但是问题是处理太频繁了,所以这里我们使用的是周期性的处理一批的数据,然后得到最大的水位线用来作为现在的水位线进行往下面进行传播 

flink timestamp 转 日期 flink时间类型_ide_07

上面就会有窗口关闭的问题

 

flink timestamp 转 日期 flink时间类型_大数据_08

想解决窗口什么时候关闭,那么就是用到了延迟的机制,就是水位线到2的时候,这个时候我们就是设置为0,就是2的数据到了的时候等2的水位线过来的时候再处理 


下面是一个示例,我们可以使用周期性的方式生成正确的水位线。




flink timestamp 转 日期 flink时间类型_时间戳_09


      第一个水位线时间戳为 7 ,它表示当前事件时间是 7 秒, 7 秒之前的数据 都已经到齐,之后再也不会有了;同样,第二个、第三个水位线时间戳分别为 12 和 20 ,表示 11 秒、 20 秒之前的数据都已经到齐,如果有对应的窗口就可以直接关闭了,统计的结果一定 是正确的。这里由于水位线是周期性生成的,所以插入的位置不一定是在时间戳最大的数据后 面。


      另外需要注意的是,这里一个窗口所收集的数据,并不是之前所有已经到达的数据。因为


数据属于哪个窗口,是由数据本身的时间戳决定的,一个窗口只会收集真正属于它的那些数据。


也就是说,上图中尽管水位线 W(20) 之前有时间戳为 22 的数据到来, 10~20 秒的窗口中也不


会收集这个数据,进行计算依然可以得到正确的结果。关于窗口的原理,我们会在后面继续展


开讲解。


水位线的特性

      现在我们可以知道,水位线就代表了当前的事件时间时钟,而且可以在数据的时间戳基础
上加一些延迟来保证不丢数据,这一点对于乱序流的正确处理非常重要。
我们可以总结一下水位线的特性:
⚫ 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据
⚫ 水位线主要的内容是一个时间戳,用来表示当前事件时间的进展


⚫ 水位线是基于数据的时间戳生成的


⚫ 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进


⚫ 水位线可以通过设置延迟,来保证正确处理乱序数据


⚫ 一个水位线 Watermark(t) ,表示在当前流中事件时间已经达到了时间戳 t, 这代表 t 之


前的所有数据都到齐了,之后流中不会出现时间戳 t’ ≤ t 的数据


水位线是 Flink 流处理中保证结果正确性的核心机制,它往往会跟窗口一起配合,完成对


乱序数据的正确处理。关于这部分内容,我们会稍后进一步展开讲解。


水位线在代码中生成

有序流

stream.assignTimestampsAndWatermarks(
                WatermarkStrategy.<Event>forMonotonousTimestamps()
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>()
                        {
                            @Override
                            public long extractTimestamp(Event element, long recordTimestamp)
                            {
                                return element.timestamp;
                            }
                        })
        );

乱序流

stream.assignTimestampsAndWatermarks(
                        // 针对乱序流插入水位线,延迟时间设置为 5s
                        WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                                .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                                    // 抽取时间戳的逻辑
                                    @Override
                                    public long extractTimestamp(Event element, long
                                            recordTimestamp) {
                                        return element.timestamp;
                                    }
                                })
                ).print();

水位线的传递

flink timestamp 转 日期 flink时间类型_数据_10

  • 自己本身的水位线就是上游分区的最小的水位线
  • 对于向下游传递的时候,广播自己的水位线给下游所有的水位线
  •  如果接收到的水位线要比现在的水位线小,那么就不改变自己的水位线

窗口

flink的窗口,根据时间的处理时间放到不同的桶里面,spark没有处理事件乱序过来的能力

flink timestamp 转 日期 flink时间类型_数据_11

窗口的分类

按照驱动类型分类

窗口本身是截取有界数据的一种方式,所以窗口一个非常重要的信息其实就是“怎样截取


数据”。换句话说,就是以什么标准来开始和结束数据的截取,我们把它叫作窗口的“驱动类


型”。


我们最容易想到的就是按照时间段去截取数据,这种窗口就叫作“时间窗口”( Time


Window )。这在实际应用中最常见,之前所举的例子也都是时间窗口。除了由时间驱动之外,


窗口其实也可以由数据驱动,也就是说按照固定的个数,来截取一段数据集,这种窗口叫作“计


数窗口”( Count Window )


flink timestamp 转 日期 flink时间类型_时间戳_12


按照窗口分配数据的规则分类

滚动窗口(Tumbling Windows)

按时间,或者是信息的数量进行滚动

flink timestamp 转 日期 flink时间类型_ide_13

滑动窗口(Sliding Windows

下面也是按时间还有信息的数量

flink timestamp 转 日期 flink时间类型_时间戳_14

会话窗口(Session Windows

他是根据会话的超时时间来划分,这里就不能用计算的概念了

flink timestamp 转 日期 flink时间类型_时间戳_15

全局窗口(Global Windows

要自定义一个触发器才能触发下一个窗口的操作

flink timestamp 转 日期 flink时间类型_ide_16

窗口Api

窗口分配器

时间窗口


滚动处理时间窗口


stream.keyBy(...).window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
                .aggregate(...)


滑动处理时间窗口


stream.keyBy(...).window(SlidingProcessingTimeWindows.of(Time.seconds(10), Time.seconds(5)))
                .aggregate(...)


滚动事件时间窗口


stream.keyBy(...).window(TumblingEventTimeWindows.of(Time.seconds(5))) .aggregate(...)


滑动事件时间窗口


stream.keyBy(...).window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
                .aggregate(...)

计数窗口


滚动计数窗口


stream.keyBy(...)
.countWindow(10)


滑动计数窗口


stream.keyBy(...)
.countWindow(10,3)

窗口函数

流之间的装换

flink timestamp 转 日期 flink时间类型_ide_17

增量聚合函数:就是数据来一条就把结果计算出来,时间到了的时候,直接把结果往后面传递

全量聚合函数:  就是数据一批到时间在处理 

增量聚合函数reduce

pojo

public class Event {
    public String id;

    public String name;

    public Long timeStemp;

    public Event() {
    }

    public Event(String id, String name, Long timeStemp) {
        this.id = id;
        this.name = name;
        this.timeStemp = timeStemp;
    }

    @Override
    public String toString() {
        return "Event{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", timeStemp=" + timeStemp +
                '}';
    }
}

应用程序

public class FlinkApp {
    public static void main(String[] args) throws Exception {
        //得到执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Event> initData = env.addSource(new SourceFunction<Event>() {
            boolean flag = true;

            String[] names = {"a", "boy", "mary"};

            String[] urls = {"/baidu", "xinlang", "google"};

            Random random = new Random();

            @Override
            public void run(SourceContext<Event> ctx) throws Exception {
                while (flag) {
                    Thread.sleep(1000);
                    ctx.collect(new Event(
                            urls[random.nextInt(3)],
                            names[random.nextInt(3)],
                            Calendar.getInstance().getTimeInMillis()
                    ));
                }
            }

            @Override
            public void cancel() {
                flag = false;
            }
        });

        //对于初始的数据设置水位线
        SingleOutputStreamOperator<Event> watermarksData = initData.assignTimestampsAndWatermarks(
                // 针对乱序流插入水位线,延迟时间设置为 10s
                WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            // 抽取时间戳的逻辑
                            @Override
                            public long extractTimestamp(Event element, long
                                    recordTimestamp) {
                                return element.timeStemp;
                            }
                        })
        );

        //对于数据进行map操作用于后面的聚合
        KeyedStream<Tuple2<String, Integer>, String> keyedStream = watermarksData.map(new MapFunction<Event, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(Event value) throws Exception {
                return Tuple2.of(value.name, 1);
            }
        }).keyBy(new KeySelector<Tuple2<String, Integer>, String>() {
            @Override
            public String getKey(Tuple2<String, Integer> value) throws Exception {
                return value.f0;
            }
        });

        //对于分组以后的数据进行开窗处理,滑动窗口的大小为5秒
        keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
                        .reduce(new ReduceFunction<Tuple2<String, Integer>>() {
                            @Override
                            public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
                                return Tuple2.of(value1.f0,value1.f1+value2.f1);
                            }
                        }).print();

        env.execute();

    }
}

预聚合函数aggregate

public class FlinkApp {
    public static void main(String[] args) throws Exception {
        //得到执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Event> initData = env.addSource(new SourceFunction<Event>() {
            boolean flag = true;

            String[] names = {"a", "boy", "mary"};

            String[] urls = {"/baidu", "xinlang", "google"};

            Random random = new Random();

            @Override
            public void run(SourceContext<Event> ctx) throws Exception {
                while (flag) {
                    Thread.sleep(1000);
                    ctx.collect(new Event(
                            urls[random.nextInt(3)],
                            names[random.nextInt(3)],
                            Calendar.getInstance().getTimeInMillis()
                    ));
                }
            }

            @Override
            public void cancel() {
                flag = false;
            }
        });

        //对于初始的数据设置水位线
        SingleOutputStreamOperator<Event> watermarksData = initData.assignTimestampsAndWatermarks(
                // 针对乱序流插入水位线,延迟时间设置为 10s
                WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            // 抽取时间戳的逻辑
                            @Override
                            public long extractTimestamp(Event element, long
                                    recordTimestamp) {
                                return element.timeStemp;
                            }
                        })
        );

        //对于数据进行map操作用于后面的聚合
        KeyedStream<Tuple2<String, Integer>, String> keyedStream = watermarksData.map(new MapFunction<Event, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(Event value) throws Exception {
                return Tuple2.of(value.name, 1);
            }
        }).keyBy(new KeySelector<Tuple2<String, Integer>, String>() {
            @Override
            public String getKey(Tuple2<String, Integer> value) throws Exception {
                return value.f0;
            }
        });

        //对于分组以后的数据进行开窗处理,滑动窗口的大小为10秒
        keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
                        //窗口规约聚合,这里也就是窗口里面数据的预聚合的功能
                        .aggregate(new AggregateFunction<Tuple2<String, Integer>, Tuple2<String,Integer>, Tuple2<String,Integer>>() {
                            //初始化中间的状态
                            @Override
                            public Tuple2<String, Integer> createAccumulator() {
                                return Tuple2.of("init",0);
                            }
                            //窗口里面每来一个数据就调用一次,第一个数据是新数据,第二个参数就是累加器
                            @Override
                            public Tuple2<String, Integer> add(Tuple2<String, Integer> value, Tuple2<String, Integer> accumulator) {
                                return Tuple2.of(value.f0,accumulator.f1+value.f1);
                            }
                            //窗口触发的时候调用

                            @Override
                            public Tuple2<String, Integer> getResult(Tuple2<String, Integer> accumulator) {
                                return accumulator;
                            }
                            //会话窗口的时候会用到合并的功能,就是两个累加器合并,如果不是会话窗口就不用实现
                            @Override
                            public Tuple2<String, Integer> merge(Tuple2<String, Integer> a, Tuple2<String, Integer> b) {
                                return Tuple2.of(a.f0,a.f1+b.f1);
                            }
                        }).print();

        env.execute();

    }
}

全窗口函数process

就是窗口触发以后,然后所有的数据过来一块处理,上面的预聚合就是窗口里面的数据来一条处理一条,窗口触发的时候就输出结果

public class FlinkApp {
    public static void main(String[] args) throws Exception {
        //得到执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Event> initData = env.addSource(new SourceFunction<Event>() {
            boolean flag = true;

            String[] names = {"a", "boy", "mary"};

            String[] urls = {"/baidu", "xinlang", "google"};

            Random random = new Random();

            @Override
            public void run(SourceContext<Event> ctx) throws Exception {
                while (flag) {
                    Thread.sleep(1000);
                    ctx.collect(new Event(
                            urls[random.nextInt(3)],
                            names[random.nextInt(3)],
                            Calendar.getInstance().getTimeInMillis()
                    ));
                }
            }

            @Override
            public void cancel() {
                flag = false;
            }
        });

        //对于初始的数据设置水位线
        SingleOutputStreamOperator<Event> watermarksData = initData.assignTimestampsAndWatermarks(
                // 针对乱序流插入水位线,延迟时间设置为 10s
                WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            // 抽取时间戳的逻辑
                            @Override
                            public long extractTimestamp(Event element, long
                                    recordTimestamp) {
                                return element.timeStemp;
                            }
                        })
        );

        //对于数据进行map操作用于后面的聚合
        KeyedStream<Tuple2<String, Integer>, String> keyedStream = watermarksData.map(new MapFunction<Event, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(Event value) throws Exception {
                return Tuple2.of(value.name, 1);
            }
        }).keyBy(new KeySelector<Tuple2<String, Integer>, String>() {
            @Override
            public String getKey(Tuple2<String, Integer> value) throws Exception {
                return value.f0;
            }
        });

        //对于分组以后的数据进行开窗处理,滑动窗口的大小为10秒
        keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
                        //        IN, OUT, KEY, W
                        .process(new ProcessWindowFunction<Tuple2<String, Integer>, String, String, TimeWindow>() {
                            //第一个参数是key第二个参数是窗口触发以后过来的所有的数据,第三个参数是上下文对象
                            //第四个参数是传递给下游的收集器
                            @Override
                            public void process(String s, Context context, Iterable<Tuple2<String, Integer>> elements, Collector<String> out) throws Exception {
                                Integer sum=0;
                                //对于窗口里面过来的数据进行累加
                                for (Tuple2<String, Integer> element : elements) {
                                    sum += element.f1;
                                }
                                //得到窗口的开始时间和结束的时间
                                long start = context.window().getStart();
                                long end = context.window().getEnd();
                                String res="key: "+s+" start: "+start+" end: "+end+" "+"sum: "+sum;
                                out.collect(res);
                            }
                        }).print();

        env.execute();

    }
}

输出的结果

11> key: a start: 1657974850000 end: 1657974860000 sum: 4
14> key: boy start: 1657974850000 end: 1657974860000 sum: 1
11> key: mary start: 1657974850000 end: 1657974860000 sum: 2

aggregate和process结合使用

public class FlinkApp {
    public static void main(String[] args) throws Exception {
        //得到执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Event> initData = env.addSource(new SourceFunction<Event>() {
            boolean flag = true;

            String[] names = {"a", "boy", "mary"};

            String[] urls = {"/baidu", "xinlang", "google"};

            Random random = new Random();

            @Override
            public void run(SourceContext<Event> ctx) throws Exception {
                while (flag) {
                    Thread.sleep(1000);
                    ctx.collect(new Event(
                            urls[random.nextInt(3)],
                            names[random.nextInt(3)],
                            Calendar.getInstance().getTimeInMillis()
                    ));
                }
            }

            @Override
            public void cancel() {
                flag = false;
            }
        });

        //对于初始的数据设置水位线
        SingleOutputStreamOperator<Event> watermarksData = initData.assignTimestampsAndWatermarks(
                // 针对乱序流插入水位线,延迟时间设置为 10s
                WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
                        .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
                            // 抽取时间戳的逻辑
                            @Override
                            public long extractTimestamp(Event element, long
                                    recordTimestamp) {
                                return element.timeStemp;
                            }
                        })
        );

        //对于数据进行map操作用于后面的聚合
        KeyedStream<Tuple2<String, Integer>, String> keyedStream = watermarksData.map(new MapFunction<Event, Tuple2<String, Integer>>() {
            @Override
            public Tuple2<String, Integer> map(Event value) throws Exception {
                return Tuple2.of(value.name, 1);
            }
        }).keyBy(new KeySelector<Tuple2<String, Integer>, String>() {
            @Override
            public String getKey(Tuple2<String, Integer> value) throws Exception {
                return value.f0;
            }
        });

        //对于分组以后的数据进行开窗处理,滑动窗口的大小为10秒
        keyedStream.window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
                        .aggregate(new AggregateFunction<Tuple2<String, Integer>, Integer, Integer>() {
                            @Override
                            public Integer createAccumulator() {
                                return 0;
                            }

                            @Override
                            public Integer add(Tuple2<String, Integer> value, Integer accumulator) {
                                return accumulator + value.f1;
                            }

                            @Override
                            public Integer getResult(Integer accumulator) {
                                return accumulator;
                            }

                            @Override
                            public Integer merge(Integer a, Integer b) {
                                return a + b;
                            }
                            //第一个参数就是前面aggregate的增量聚合函数过来的值
                            //第二个参数就是返回的值
                            //第三个参数就是key
                            //第四个参数默认TimeWindow
                        }, new ProcessWindowFunction<Integer, String, String, TimeWindow>() {
                            @Override
                            public void process(String s, ProcessWindowFunction<Integer, String, String, TimeWindow>.Context context, Iterable<Integer> elements, Collector<String> out) throws Exception {
                                //因为窗口触发以后就是在aggregate的作用下,过来的数据就是一个那么我们直接next
                                Integer next = elements.iterator().next();

                                //得到窗口的开始时间和结束的时间
                                long start = context.window().getStart();
                                long end = context.window().getEnd();
                                String res="key: "+s+" start: "+start+" end: "+end+" "+"sum: "+next;
                                out.collect(res);
                            }
                        }).print();

        env.execute();

    }
}

处理迟到数据(加深窗口和水位线的认识)

应用测试

public class FlinkApp {
    public static void main(String[] args) throws Exception {
        //得到执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        DataStreamSource<String> socketTextStream = env.socketTextStream("master", 9999);

        //并行度设置为1才能看到效果,因为如果不为1,那么有些分区的水位线就是负无穷
        //由于自己的水位线是分区里面最小的水位线,那么自己的一直都是负无穷
        //就触发不了水位线的上升
        env.setParallelism(1);

        //第一个参数就一个名字,第二个参数用来表示事件时间
        SingleOutputStreamOperator<Tuple2<String, Long>> initData = socketTextStream.map(new MapFunction<String, Tuple2<String, Long>>() {
            @Override
            public Tuple2<String, Long> map(String value) throws Exception {
                String[] s = value.split(" ");
                //假设我们在控制台输入的参数是a 15s,那么我们要15*1000才能得到时间戳的毫秒时间
                return Tuple2.of(s[0], Long.parseLong(s[1]) * 1000L);
            }
        });

        //设置水位线
        SingleOutputStreamOperator<Tuple2<String, Long>> watermarks = initData.assignTimestampsAndWatermarks(
                // 针对乱序流插入水位线,延迟时间设置为 2s
                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;
                            }
                        })
        );

        //定义一个侧输出流的标识
        OutputTag<Tuple2<String, Long>> outputTag = new OutputTag<Tuple2<String, Long>>("late") {
        };

        SingleOutputStreamOperator<Tuple2<String, Long>> result = watermarks.keyBy(data -> data.f0)
                //窗口的大小为10s,注意这里是事件时间
                .window(TumblingEventTimeWindows.of(Time.seconds(10)))
                //定义窗口关闭延迟,就是允许最大的迟到数据,由于上面设置的最大延迟为2s,在加上这个2s那么就是
                //允许最大的迟到数据为4秒
                .allowedLateness(Time.seconds(2))
                //使用定义的标识
                .sideOutputLateData(outputTag)
                .reduce(new ReduceFunction<Tuple2<String, Long>>() {
                    @Override
                    public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception {
                        return Tuple2.of(value1.f0, value2.f1 + value1.f1);
                    }
                });
        //        .aggregate();下面就可以定义处理函数进行处理

        result.print("result");

        //得到侧输出流的数据
        DataStream<Tuple2<String, Long>> sideOutput = result.getSideOutput(outputTag);
        sideOutput.print("late");


        env.execute();
    }
}

在linux里面使用nc

nc -lk 9999

然后输入

a 1
a 15
a 2

得到的结果

result> (a,1000)
late> (a,2000)

原理图

flink timestamp 转 日期 flink时间类型_flink_18


总结:

WatermarkStrategy.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(2))

表示水位线延迟2秒,也就是说如果这个时候的时间时间是5,如果不延迟那么就是4999,那么延迟2秒的水位线就是2999 。


allowedLateness(Time.seconds(2))是窗口关闭以后还可以收集2秒的数据,如果2秒以后还没有过来,这个数据要么丢失,要么就是发送到侧输出流