問題起因
最近在做一個基于時間的統(tǒng)計功能,大體需求是統(tǒng)計按照 1min品山、10min胆建、2h、24h 為窗口大小進行數據統(tǒng)計肘交。原始數據的時間字段是 ms 時間戳笆载,思路很簡單就是直接用時間戳減去窗口大小余數,這種方式對 1min涯呻、10min、2h 的處理都沒有問題复罐,但是對 24h 的窗口處理就會有問題,可以見下面的測試市栗。
/**
* 獲取指定時間時間戳歸屬的時間窗口咳短,盡量刻度到當天
*
* @param timestamp 時間戳
* @param scale 刻度,ms
* @return {@link long}
*/
public static long getTimestampWindow(long timestamp, long scale) {
long remain = timestamp % scale;
return timestamp - remain ;
}
上面就是使用的時間戳窗口計算算法蛛淋。
public static void main(String[] args) throws ParseException {
long scale = TimeUnit.HOURS.toMillis(24);
for (int i = 0; i < 24; i++) {
Date date = DateUtils.addHours(DateUtils.parseDate("2022-06-24", DateUtil.DATE_PATTERN), i);
Date scaledDate = new Date(getTimestampWindow(date.getTime(), scale));
System.out.printf("當前時間:%s,窗口時間:%s%n",
DateFormatUtils.format(date, DateUtil.LONG_DATE_PATTERN),
DateFormatUtils.format(scaledDate, DateUtil.LONG_DATE_PATTERN));
}
}
當使用上面的測試程序進行測試的時候發(fā)現(xiàn)咙好,窗口并未預期的顯示是“2022-06-24 00:00:00”褐荷,而是輸出了“2022-06-23 08:00:00”及“2022-06-24 08:00:00”兩種窗口勾效,一天的數據出現(xiàn)了歸屬跨天問題叛甫。
當然如果使用 Java 里面的日期函數可以很簡單的解決這個問題层宫,但是我們返回時間數據是時間戳,直接做算術運算肯定是效率最高的其监。
當前時間:2022-06-24 00:00:00,窗口時間:2022-06-23 08:00:00
當前時間:2022-06-24 01:00:00,窗口時間:2022-06-23 08:00:00
當前時間:2022-06-24 02:00:00,窗口時間:2022-06-23 08:00:00
當前時間:2022-06-24 03:00:00,窗口時間:2022-06-23 08:00:00
當前時間:2022-06-24 04:00:00,窗口時間:2022-06-23 08:00:00
當前時間:2022-06-24 05:00:00,窗口時間:2022-06-23 08:00:00
當前時間:2022-06-24 06:00:00,窗口時間:2022-06-23 08:00:00
當前時間:2022-06-24 07:00:00,窗口時間:2022-06-23 08:00:00
當前時間:2022-06-24 08:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 09:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 10:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 11:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 12:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 13:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 14:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 15:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 16:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 17:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 18:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 19:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 20:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 21:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 22:00:00,窗口時間:2022-06-24 08:00:00
當前時間:2022-06-24 23:00:00,窗口時間:2022-06-24 08:00:00
問題分析
這是為什么呢萌腿?核心原因是因為我們處在的時區(qū)是東 8 區(qū)抖苦,在比格林尼治時間早 8 小時,時間戳 0 在零時區(qū)(UTC/GMT 0 )表示的是“1970-01-01 00:00:00”锌历,而在東 8 區(qū)(UTC/GMT +8.00)則表示的是“1970-01-01 08:00:00”。
public static void main(String[] args) {
System.out.println(TimeZone.getDefault());
System.out.println(DateFormatUtils.format(new Date(0), DateUtil.LONG_DATE_PATTERN));
System.out.println(TimeZone.getTimeZone("UTC"));
System.out.println(DateFormatUtils.format(new Date(0), DateUtil.LONG_DATE_PATTERN, TimeZone.getTimeZone("UTC")));
}
sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=19,lastRule=null]
1970-01-01 08:00:00
sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
1970-01-01 00:00:00
我們這里只討論能被 24 小時整除的窗口窗慎,也就是 2h卤材、12h 這樣的捉邢,而不討論 5h商膊,7h 這樣的窗口,因為對后面的窗口藐翎,勢必存在跨天問題。也就是說如果用取余的方式來計算時間窗口的話吝镣,當時間能被 24 整除但是如果大于 8 小時(12h昆庇、24h)或者不能被 8 整除(3h、6h)時候就會出現(xiàn)歸屬窗口跨天問題整吆。
如上圖所示,其中帶 - 號的代表上一天的時間拴测,大家可以想下是不是這樣?這個有點繞屿愚,一定要記得起始時間戳 0 代表的時間是 8 點。
如果能理解這個务荆,其實就會發(fā)現(xiàn)對一個 24h 的窗口來說妆距,今天 1 點的數據歸屬到昨天的 8 小時這個窗口是正常的函匕,但是這個的確看起來很怪。如果時間窗口是 24h浦箱,對我們的思維來說祠锣,今天所有產生的數據就應該是歸屬到今天。
問題解決
既然知道了問題的原因伴网,也知道了需求方式,也就比較容易解決這個問題沸伏。思想其實很簡單动分,就是先把時間戳向后拉 8 小時毅糟,讓“時間戳 0 代表的時間是 0 點”澜公。在算完窗口之后,再將時間窗口向前拉 8 小時迹辐,獲得真實的歸屬窗口。
/**
* 獲取指定時間時間戳歸屬的時間窗口明吩,盡量刻度到當天
*
* @param timestamp 時間戳
* @param scale 刻度殷费,ms
* @return {@link long}
*/
public static long getTimestampWindow(long timestamp, long scale) {
timestamp = timestamp + TIMESTAMP_8H;
long remain = timestamp % scale;
return timestamp - remain - TIMESTAMP_8H;
}
算法變成如上所示低葫,再運行測試程序躏鱼,就會發(fā)現(xiàn)時間窗口歸屬符合我們的期望了氮采。
當前時間:2022-06-24 00:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 01:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 02:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 03:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 04:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 05:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 06:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 07:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 08:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 09:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 10:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 11:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 12:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 13:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 14:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 15:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 16:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 17:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 18:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 19:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 20:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 21:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 22:00:00,窗口時間:2022-06-24 00:00:00
當前時間:2022-06-24 23:00:00,窗口時間:2022-06-24 00:00:00