Flink实战 - 周窗口-week window实现

原文链接:https://blog.csdn.net/m0_49826240/article/details/117296559

结论:

先说结论吧:

Flink 使用 timeWindow,如何使用周级别的滚动窗口

不需要自定义窗口分配器!不需要自定义窗口分配器!

第一种:开始时间为周一 00:00:00,结束时间为 周日 23:59:59

.window(TumblingEventTimeWindows.of(Time.days(7), Time.hours(-80)));

第二种:开始时间为周天 00:00:00,结束时间为 周六 23:59:59

.window(TumblingEventTimeWindows.of(Time.days(7), Time.hours(-104)));

至于为什么,往下看。阅读源码来解释

 

数据需求

接到一个实时数据的需求,大致的内容就是实时统计每个店铺的一周GMV。

分析这个需求,明确的点:

       1.周期长度为1周,也就是7天,7 x 24 个小时。

       2.每个周期的开始时间:周一 00:00:00,结束时间:周日 00:00:00

按照日历中的划分,每周的开始是周日,结束是周六。这个我们不需要纠结这么多。

遇到的问题

这个需要可以用两种方式来做

1.使用 keyby-process,对商家ID做key by,然后在process中做每周的GMV统计即可。

2.使用 timeWindow,使用支付时间开窗,累计一周的时间。

此处我们主要探讨第二种方案。

本文提及的这个需求,更实用使用滚动窗口计算,下面这行代码就是平时实时的写法。

.window(TumblingEventTimeWindows.of(Time.days(7), Time.hours(-8)));

此时可以发现,Time.day() 已经是提供的最大时间单位了,并没有想要使用 Time.week()。那么使用 Time.day(7)呢?

行不行,试一下就知道了,做个小实验,读取端口 9910 数据,使用  TumblingEventTimeWindows.of(Time.days(7), Time.hours(-8))

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
DataStreamSource localhost = env.socketTextStream("localhost", 9910, "\n");
SingleOutputStreamOperator d1 = localhost
        .flatMap(new FlatMapFunction() {
            @Override
            public void flatMap(String value, Collector out) throws Exception {
                try {
                    DataEntity entity = new DataEntity();
                        entity.setKey(value);
                        entity.setCount(1L);
                        entity.setTime(System.currentTimeMillis());
                        out.collect(entity);
                } catch (Exception ignored) {
                }
            }
        });

SingleOutputStreamOperator reduce = d1
        .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor(Time.seconds(2)) {
            @Override
            public long extractTimestamp(DataEntity element) {
                return element.getTime();
            }
        })
        .keyBy(DataEntity::getKey)
        .window(TumblingEventTimeWindows.of(Time.days(7), Time.hours(-8)))
        .trigger(CountTrigger.of(1L))
        .process(new ProcessWindowFunction() {
            @Override
            public void process(String s, Context context, Iterable elements, Collector out) throws Exception {
                long start = context.window().getStart();
                long end = context.window().getEnd();
                String ss = TimeTranUtils.tranTimeToTime(start + "");
                String ee = TimeTranUtils.tranTimeToTime(end + "");
                for (DataEntity e: elements) {
                    System.out.println(e.getKey() + ", " + e.getCount() + ",        " + ss +" - " + ee);
                }
            }
        });
env.execute("tttttt");

查看控制台打印的数据,窗口开始时间是 2021-05-23 00:00:00,也就是今天的 00:00:00 时刻,结束时间是7日后。所以这个是不可行。周窗口不能以当前时间为开始时间。

Flink实战 - 周窗口-week window实现_第1张图片

这样不行,那我们想想看,如何改下窗口分配器了。(窗口分配器,就是将每条数据根据时间分配到对应的窗口上)

既然都是滚动窗口,那么看下官方的提供的分配器吧。找到 start 取值那一行,这个就是该数据所属窗口的开始时间。

public class TumblingEventTimeWindows extends WindowAssigner {
	private static final long serialVersionUID = 1L;

	private final long size;	// 窗口的大小
	private final long offset;	// 时间偏移量,最简单理解就是时间上的一个前进/后退

	protected TumblingEventTimeWindows(long size, long offset) {
		if (Math.abs(offset) >= size) {
			throw new IllegalArgumentException("TumblingEventTimeWindows parameters must satisfy abs(offset) < size");
		}

		this.size = size;
		this.offset = offset;
	}

	@Override
	public Collection assignWindows(Object element, long timestamp, WindowAssignerContext context) {
		if (timestamp > Long.MIN_VALUE) {
			// Long.MIN_VALUE is currently assigned when no timestamp is present
			// 看这一步,start 这个就是窗口的开始时间,也就是用来分配窗口的一个凭证。
			long start = TimeWindow.getWindowStartWithOffset(timestamp, offset, size);
			return Collections.singletonList(new TimeWindow(start, start + size));
		} else {
			throw new RuntimeException("Record has Long.MIN_VALUE timestamp (= no timestamp marker). " +
					"Is the time characteristic set to 'ProcessingTime', or did you forget to call " +
					"'DataStream.assignTimestampsAndWatermarks(...)'?");
		}
	}

	@Override
	public Trigger getDefaultTrigger(StreamExecutionEnvironment env) {
		return EventTimeTrigger.create();
	}

	@Override
	public String toString() {
		return "TumblingEventTimeWindows(" + size + ")";
	}

	/**
	 * Creates a new {@code TumblingEventTimeWindows} {@link WindowAssigner} that assigns
	 * elements to time windows based on the element timestamp.
	 *
	 * @param size The size of the generated windows.
	 * @return The time policy.
	 */
	public static TumblingEventTimeWindows of(Time size) {
		return new TumblingEventTimeWindows(size.toMilliseconds(), 0);
	}

	/**
	 * Creates a new {@code TumblingEventTimeWindows} {@link WindowAssigner} that assigns
	 * elements to time windows based on the element timestamp and offset.
	 *
	 * 

For example, if you want window a stream by hour,but window begins at the 15th minutes * of each hour, you can use {@code of(Time.hours(1),Time.minutes(15))},then you will get * time windows start at 0:15:00,1:15:00,2:15:00,etc. * *

Rather than that,if you are living in somewhere which is not using UTC±00:00 time, * such as China which is using UTC+08:00,and you want a time window with size of one day, * and window begins at every 00:00:00 of local time,you may use {@code of(Time.days(1),Time.hours(-8))}. * The parameter of offset is {@code Time.hours(-8))} since UTC+08:00 is 8 hours earlier than UTC time. * * @param size The size of the generated windows. * @param offset The offset which window start would be shifted by. * @return The time policy. * * * 上面已经说的很明白,就是将时间一个整体的增加和减少,以达到一种偏移。最直白的一个例子就是 * 咱们国家属于 UTC +08:00 时区,比起 00:00 区,使用 day window,那么我们需要 减去 8 hour 来计算。 * */ public static TumblingEventTimeWindows of(Time size, Time offset) { return new TumblingEventTimeWindows(size.toMilliseconds(), offset.toMilliseconds()); } @Override public TypeSerializer getWindowSerializer(ExecutionConfig executionConfig) { return new TimeWindow.Serializer(); } @Override public boolean isEventTime() { return true; } }

找到了这个start,start + size 就是 endTime 了。

那,我们将我们自己的数据,每一条都计算到所属的周一 00:00:00 的时间戳,那么是不是就可以得到 startTime 了?

网上有很多自定义分配器的方式,思路都差不多,但是都是通过转换到 Date 对象一类来做的。有没有简捷方式呢?比如直接通过简单算术计算?

那肯定是有的,一周是 604800000 毫秒,时间戳 = 0 的时间是 1970-01-01 08:00:00,星期四早上8点,离本周一 凌晨00:00:00 的偏移量是 3 * 24 * 3600000 + 28800000 毫秒。

那么根据当前时间的时间戳计算本周一 00:00:00 的时间戳的公式为:

mon =(currentTime + (3 * 24 * 3600000 + 28800000) /  604800000 ) * 604800000,各位看官可以试下这个公式,currentTime 可以是任意时间,都可以得到本周的周一开始时间。

mon 就是 startTime,endTime = mon + 604800000;

 

如果这就完了,那就是没有认真思考。

官方提供了这个计算公式:

// 计算 startTime
long start = TimeWindow.getWindowStartWithOffset(timestamp, offset, size);

// 看下 getWindowStartWithOffset

/**
 * Method to get the window start for a timestamp.
 *
 * @param timestamp epoch millisecond to get the window start.
 * @param offset The offset which window start would be shifted by.
 * @param windowSize The size of the generated windows.
 * @return window start
 */
public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
	return timestamp - (timestamp - offset + windowSize) % windowSize;
}

其实和我的计算结果是一样的,只不过人家是可以根据提供的 offset 和 windwiSIze 来确定 startTime。

本想着是自己封住一个分配器,但是这个地方真的启示我了。

 

我的周窗口,size = 7day,offset = 3 * 86400000 + 28800000 = 80 hour

 

那么现在直接改 TumblingEventTimeWindows 参数就可以,

.window(TumblingEventTimeWindows.of(Time.days(7), Time.hours(-80)));

 

测试很完美。

 

还是需要看源码

 

 

 

 

 

你可能感兴趣的:(Flink实战,flink,window)