一、背景:
最近有一個關(guān)于店鋪數(shù)據(jù)實時分析的需求,需要實時統(tǒng)計店鋪當(dāng)天的數(shù)據(jù):例如訪客數(shù),瀏覽量、商品排行榜等。由于店鋪可以自主選擇店鋪所在時區(qū)(全球二十四個時區(qū)),而數(shù)倉統(tǒng)計后落庫的時間是GMT+8時區(qū)對應(yīng)的UNIX時間戳。因此,在我們調(diào)用中臺的接口時,不能直接取服務(wù)器的UNIX時間戳作為傳參。
這么一聽,如果之前沒深入過 UNIX時間戳 與 時區(qū) 的概念,可能大家都會有點懵逼了;其實我也是,特別是我是昨天晚上十點多才接收到這個信息,內(nèi)心就更加慌張了,畢竟原本昨天就要提測了,現(xiàn)在因為這個時間戳的原因而推遲了。下面我們先來了解UNIX時間戳和時區(qū)的概念,然后再繼續(xù)討論這個問題。
二、概念:
UNIX時間戳:從1970年1月1日(UTC/GMT的午夜)開始所經(jīng)過的秒數(shù),不考慮閏秒。
也就是指格林威治時間1970年01月01日00時00分00秒開始到現(xiàn)在的總秒數(shù)。
對的,大家可以看到,其實UNIX時間戳說的是秒數(shù),而通常我們講的是毫秒~
時區(qū):為了克服時間上的混亂,1884年在華盛頓召開的一次國際經(jīng)度會議(又稱國際子午線會議)上,規(guī)定將全球劃分為24個時區(qū)(東、西各12個時區(qū))。規(guī)定英國(格林尼治天文臺舊址)為中時區(qū)(零時區(qū))、東1—12區(qū),西1—12區(qū)。每個時區(qū)橫跨經(jīng)度15度,時間正好是1小時。最后的東、西第12區(qū)各跨經(jīng)度7.5度,以東、西經(jīng)180度為界。每個時區(qū)的中央經(jīng)線上的時間就是這個時區(qū)內(nèi)統(tǒng)一采用的時間,稱為區(qū)時,相鄰兩個時區(qū)的時間相差1小時。
例如:中國所在東8區(qū)的時間總比莫斯科所在東3區(qū)的時間多5個小時。
看完上面兩個定義,我們可以得出結(jié)論:時間戳是沒有時區(qū)之分的,僅僅是日期展示和時區(qū)有關(guān)系;同一個時間戳,在不同時區(qū),顯示的日期是不一樣的。
所以上面的需求,如果直接用服務(wù)器所在的時間戳來作為查詢時的時間傳參,那么一般都是行不通的;除非店鋪的時區(qū)和我們服務(wù)器的時區(qū)是一樣的(容器中的時區(qū)都是GMT+8,也就是東八區(qū)),不然店鋪的時間和服務(wù)器的時間是有可能不一樣的。
例子:
假設(shè)我們現(xiàn)在根據(jù)北京時間 2021-01-12 03:00:00,獲取到對應(yīng)的時間戳是:1610391600000 (毫秒),
而這個時間戳對應(yīng)的莫斯科時間為:2021-01-11 22:00:00,這顯然和東八區(qū)與東三區(qū)的時差(五個小時)是對得上的。
很明顯,上面的例子中,同一UNIX時間戳,不同時區(qū)的時間時不一樣的,甚至存在兩時區(qū)不在同一日;那至于上面的問題也就隨之被解答了,查詢店鋪的實時數(shù)據(jù),那必須要拿到店鋪所在時區(qū)的當(dāng)前時間了。
關(guān)于展示同一UNIX時間戳兩個時區(qū)的時間區(qū)別,可以看下面代碼:
```
/***
* 對比同一時間戳,不同時區(qū)的時間顯示
* @author winfun
* @param sourceTimezone sourceTimezone
* @param targetTimezone targetTimezone
* @return {@link Void }
**/
public static void compareTimeByTimezone(String sourceTimezone,String targetTimezone){
? ? // 當(dāng)前時間服務(wù)器UNIX時間戳
? ? Long timestamp = System.currentTimeMillis();
? ? // 獲取源時區(qū)時間
? ? Instant instant = Instant.ofEpochMilli(timestamp);
? ? ZoneId sourceZoneId = ZoneId.of(sourceTimezone);
? ? LocalDateTime sourceDateTime = LocalDateTime.ofInstant(instant,sourceZoneId);
? ? // 獲取目標(biāo)時區(qū)時間
? ? ZoneId targetZoneId = ZoneId.of(targetTimezone);
? ? LocalDateTime targetDateTime = LocalDateTime.ofInstant(instant,targetZoneId);
? ? System.out.println("The timestamp is "+timestamp+",The DateTime of Timezone{"+sourceTimezone+"} is "+sourceDateTime+
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ",The " +
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "DateTime of Timezone{"+targetTimezone+"} is "+targetDateTime);
}
```
其中一次的執(zhí)行結(jié)果:
```
The timestamp is 1610594585422,The DateTime of Timezone{Europe/Moscow} is 2021-01-13 06:23:05.422,The DateTime of Timezone{Asia/Shanghai} is 2021-01-13 11:23:05.422
```
到此,我們應(yīng)該可以將時間戳和時區(qū)很好地區(qū)分出來了。
三、需求分析:
上面已經(jīng)很好地分析了UNIX時間戳與時區(qū)了,接下來繼續(xù)我們的需求分析~
如果只是拿店鋪所在時區(qū)的當(dāng)前時間,其實非常簡單,我們可以利用 LocalDateTime#now(ZoneId zone) 即可。
可是我上面的需求,到這一步還沒夠,因為數(shù)倉保存的是GMT+8時區(qū)對應(yīng)的UNIX時間戳;所以當(dāng)我們拿到店鋪所在時區(qū)的時間后,還需要轉(zhuǎn)為GMT+8時區(qū)對應(yīng)的UNIX時間戳。
由于我們是直接查詢當(dāng)天,我們可以簡化為獲取店鋪當(dāng)前的零點零分和23點59分,然后轉(zhuǎn)為GMT+8時區(qū)對應(yīng)的時間戳;最后,利用 JDK8 中的 LocalDateTime 可以非常簡單的完成。
雖然我們最后需要的是GMT+8時區(qū)的時間戳,但是為了使得方法更加通用,參數(shù)分別為源時區(qū)和目標(biāo)時區(qū),可以兼容更多的使用場景。
```
/***
* 獲取源時區(qū)的當(dāng)前日期的零點零分,轉(zhuǎn)為目標(biāo)時區(qū)對應(yīng)的時間戳
* @author winfun
* @param sourceTimezone 源時區(qū)
* @param targetTimezone 目標(biāo)時區(qū)
* @return {@link Void }
**/
public static void getStartTimeFromSourceTimezoneAndConvertTimestampToTargetTimezone(String sourceTimezone,String targetTimezone){
? ? // 獲取指定時區(qū)的當(dāng)前時間
? ? ZoneId sourceZoneId = ZoneId.of(sourceTimezone);
? ? LocalDateTime dateTime = LocalDateTime.now(sourceZoneId);
? ? LocalDate date = LocalDate.now(sourceZoneId);
? ? // 獲取上面時間的當(dāng)天0點0分
? ? LocalDateTime startTime = LocalDateTime.of(date, LocalTime.MIN);
? ? // 轉(zhuǎn)成目標(biāo)時區(qū)對應(yīng)的時間戳
? ? ZoneId targetZoneId = ZoneId.of(targetTimezone);
? ? ZoneOffset targetZoneOffset = targetZoneId.getRules().getOffset(dateTime);
? ? Long gmt8Timestamp = startTime.toInstant(targetZoneOffset).toEpochMilli();
? ? System.out.println("The Date of Timezone{"+sourceTimezone+"} is " + date + ",Thd StartTime of Timezone{"+sourceTimezone+
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "} is,"+ startTime +
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ",convert to Timezone{"+targetTimezone+"} timestamp is "+gmt8Timestamp);
}
/***
* 獲取源時區(qū)的當(dāng)前日期的23點59分,轉(zhuǎn)為目標(biāo)時區(qū)對應(yīng)的時間戳
* @author winfun
* @param sourceTimezone 源時區(qū)
* @param targetTimezone 目標(biāo)時區(qū)
* @return {@link Void }
**/
public static void getEndTimeFromSourceTimezoneAndConvertTimestampToTargetTimezone(String sourceTimezone,String targetTimezone){
? ? // 獲取指定時區(qū)的當(dāng)前時間
? ? ZoneId sourceZoneId = ZoneId.of(sourceTimezone);
? ? LocalDateTime dateTime = LocalDateTime.now(sourceZoneId);
? ? LocalDate date = LocalDate.now(sourceZoneId);
? ? // 獲取上面時間的當(dāng)天23點59分
? ? LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
? ? // 轉(zhuǎn)成目標(biāo)時區(qū)對應(yīng)的時間戳
? ? ZoneId targetZoneId = ZoneId.of(targetTimezone);
? ? ZoneOffset targetZoneOffset = targetZoneId.getRules().getOffset(dateTime);
? ? Long gmt8Timestamp = endTime.toInstant(targetZoneOffset).toEpochMilli();
? ? System.out.println("The Date of Timezone{"+sourceTimezone+"} is " + date + ",The EndTime of Timezone{"+sourceTimezone+
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? "} is"+ endTime +
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ", convert to Timezone{"+targetTimezone+"} timestamp is "+gmt8Timestamp);
}
```
其中一次執(zhí)行結(jié)果:
```
The Date of Timezone{Europe/Moscow} is 2021-01-14,Thd StartTime of Timezone{Europe/Moscow} is,2021-01-14T00:00,convert to Timezone{Asia/Shanghai} timestamp is 1610553600000
The Date of Timezone{Europe/Moscow} is 2021-01-14,The EndTime of Timezone{Europe/Moscow} is2021-01-14T23:59:59.999999999, convert to Timezone{Asia/Shanghai} timestamp is 1610639999999
```
補充:
當(dāng)然,其他場景不一定就是拿當(dāng)天的開始時間和結(jié)束時間,有可能僅僅是根據(jù)源時區(qū)當(dāng)前時間獲取目標(biāo)時區(qū)對應(yīng)的時間戳。
這個也是非常簡單,直接看下面代碼即可:
```
/***
* 獲取源時區(qū)的當(dāng)前時間,轉(zhuǎn)為目標(biāo)時區(qū)對應(yīng)的時間戳
* @author winfun
* @param sourceTimezone 源時區(qū)
* @param targetTimezone 目標(biāo)時區(qū)
* @return {@link Void }
**/
public static void getTimeFromSourceTimezoneAndConvertToTargetTimezoneToTargetTimezone(String sourceTimezone,String targetTimezone){
? ? // 獲取指定時區(qū)的當(dāng)前時間
? ? ZoneId sourceZoneId = ZoneId.of(sourceTimezone);
? ? LocalDateTime dateTime = LocalDateTime.now(sourceZoneId);
? ? /**
? ? * 轉(zhuǎn)成指定時區(qū)對應(yīng)的時間戳
? ? * 1、根據(jù)zoneId獲取zoneOffset
? ? * 2、利用zoneOffset轉(zhuǎn)成時間戳
? ? */
? ? ZoneId targetZoneId = ZoneId.of(targetTimezone);
? ? ZoneOffset offset = targetZoneId.getRules().getOffset(dateTime);
? ? Long timestamp = dateTime.toInstant(offset).toEpochMilli();
? ? System.out.println("The DateTime of Timezone{"+sourceTimezone+"} is " + dateTime + ",convert to Timezone{"+targetTimezone+"} timestamp is "+timestamp);
}
```
其中一次執(zhí)行結(jié)果:
```
The DateTime of Timezone{Europe/Moscow} is 2021-01-14T06:23:05.486,convert to Timezone{Asia/Shanghai} timestamp is 1610576585486
```
四、最后
到此,這次驚險的UNIX時間戳與時區(qū)的旅行就到此結(jié)束了,希望大家也能從這次分享中得到有用的信息~