本人前段時間經歷了一個全球化的報表項目(java+mysql),剛開始業務只在國內開展,所有報表用戶都是中國人,涉及時間/日期的數據,統一用北京時間即可。后來業務逐漸擴大到海外市場,很多國外用戶也會使用該系統,這樣默認用北京時間來顯示就不太友好了。
仔細分析一下,主要是幾個關鍵點:
一、數據查詢
當中國用戶來查看報表時,通常是在國內,查詢某張報表時,傳入的查詢日期參數 :比如 2020-04-06 00:00:00 ~ 2020-04-07 00:00:00,這2個字符串傳到服務端,應該理解為北京時間(GMT+08:00)。
而當海外用戶,比如"東京"的用戶來查看時,同樣還是 2020-04-06 00:00:00 ~ 2020-04-07 00:00:00,服務端收到這2個字符串時,應該理解為東京時間(GMT+09:00)時間。
所以,首先要改造的地方在於"查詢參數",必須新增一個額外的時區參數,類似 timeZone:"GMT+08:00"之類,這樣服務端才能知道用戶所在時區。
二、數據存儲
大多數公司的業務系統都是存儲在mysql之類的關系型數據庫中,通常在項目初期,全球化問題暫時不會考慮,部署在中國區的mysql實例,默認就是北京的東8區,即:GMT+08:00。
業務擴展到海外后,如果db性能還跟得上,仍然建議集中存儲到原來的實例上,即數據存儲仍然還是采用默認的GMT+08:00的北京時間存儲。海外用戶如果要訪問加速,可以在當地部署數據副本,把主庫的數據同步過去(方案有很多,大家可以自行網上查閱)。
這樣的好處是,數據寫入部分不用作任何修改。
三、時間的匹配及展示
有了前面2個前提,后面的事情就好做了,先來看日期字段的sql where 匹配:
3.1 根據查詢參數中的timeZone,把傳入的日期字符串,視為當地時間,統一轉換成北京時間(在java層做轉換即可,文章最后會給出轉換代碼),這樣就跟db中的時區一致,原來的sql語句不用任何調整.
3.2 在數據展示時,把db中查出來的時間(默認北京時間),根據timeZone轉換成當地時間顯示,仍然只需要在java層輸出數據時做轉換 。
四、一些按天匯總的job調整
有些報表,是按“自然天”跑定時job匯總統計,比如每天統計 當地時間0點到23:59:59的訂單總數。在只有中國業務的時期,這個統計的時間段范圍就是北京時間的每天00:00:00 ~ 23:59:59,但是有海外業務后,當地的自然天,就不再是北京時間的00:00:00 ~ 23:59:59了,思路還是類似的:先將當地自然天的00:00:00 ~ 23:59:59,轉換成北京時間對應的時間段.
比如:對於東京地區而言,2020-04-06 00:00:00 ~ 2020-04-06 23:59:59,其實對應北京時間的2020-04-05 23:00:00 ~ 2020-04-06 22:59:59. 仍然只需要在job計算的入口,統一換成北京時間的24小時區間段,再計算即可。
該方案理論上沒問題,但實際落地時會有些復雜,比如:原來的job,每天0點后,算前1天的即可,只要跑一次,現在海外用戶加進來后,比如有3個海外地區,job就要在額外的3個時間點,分別計算各個地區的自然天匯總數據。可能需要把原來的job部署多份(或配置多個啟動的時間點),然后在每個不同的時間點,要有各自的邏輯,計算指定地區的數據。
所以,還有另一個思路:把按天計算的報表,匯總的時間顆粒度細化,變成按小時計算,每個小時匯總前1個小時的數據,1個小時一條記錄,然后不同時區的用戶在查看時,根據當地自然天,查詢出對應匹配的24條記錄,最后做個簡單的sum即可。這樣job就不用區別對待各個地區,邏輯是統一的,對所有地區,只算上1個小時數據。
最后貼一段時區轉換的工具代碼:
import java.time.*; import java.time.format.DateTimeFormatter; import java.util.Date; public class DateTest { public static void main(String[] args) { Date now = new Date();//中國部署的服務器,通常時間即為北京時間GMT+08:00 String pattern = "yyyy-MM-dd HH:mm:ss.SSS"; System.out.println("北京時間(GMT+08:00):"); System.out.println(now); System.out.println("轉換成東京時間(GMT+09:00)字符串:"); System.out.println(toTargetDateTimeString(now, "GMT+9", pattern)); System.out.println("轉換成東京時間(GMT+09:00):"); System.out.println(toTargetDate(now, "GMT+9")); System.out.println("\n東京時間(GMT+09:00)字符串:"); String gmt9DateTimeString = "2020-04-06 14:32:52.534"; System.out.println(gmt9DateTimeString); System.out.println("轉換成北京時間(GMT+08:00)字符串:"); System.out.println(toTargetDateTimeString(gmt9DateTimeString, "GMT+9", pattern, "GMT+8")); System.out.println("轉換成北京時間(GMT+08:00):"); System.out.println(toTargetDate(gmt9DateTimeString, "GMT+9", pattern, "GMT+8")); } /** * @param date * @param targetGMT * @return */ public static Date toTargetDate(Date date, String targetGMT) { return toDate(LocalDateTime.ofInstant(date.toInstant(), ZoneId.of(targetGMT))); } /** * date -> 目標GMT時區字符串 * * @param date * @param targetGMT * @param pattern * @return */ public static String toTargetDateTimeString(Date date, String targetGMT, String pattern) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); return LocalDateTime.ofInstant(date.toInstant(), ZoneId.of(targetGMT)).format(formatter); } /** * 將原GMT時區的日期字符串->目標GMT時區的日期字符串 * * @param srcDateTimeString * @param srcGMT * @param pattern * @param targetGMT * @return */ public static String toTargetDateTimeString(String srcDateTimeString, String srcGMT, String pattern, String targetGMT) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); LocalDateTime srcLocalDateTime = LocalDateTime.parse(srcDateTimeString, formatter); ZonedDateTime srcZonedDateTime = srcLocalDateTime.atZone(ZoneId.of(srcGMT)); LocalDateTime targetLocalDateTime = LocalDateTime.ofInstant(srcZonedDateTime.toInstant(), ZoneId.of(targetGMT)); return targetLocalDateTime.format(formatter); } /** * 將原GMT時區的日期字符串->目標GMT時區的Date * * @param srcDateTimeString * @param srcGMT * @param pattern * @param targetGMT * @return */ public static Date toTargetDate(String srcDateTimeString, String srcGMT, String pattern, String targetGMT) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); LocalDateTime srcLocalDateTime = LocalDateTime.parse(srcDateTimeString, formatter); ZonedDateTime srcZonedDateTime = srcLocalDateTime.atZone(ZoneId.of(srcGMT)); LocalDateTime targetLocalDateTime = LocalDateTime.ofInstant(srcZonedDateTime.toInstant(), ZoneId.of(targetGMT)); return toDate(targetLocalDateTime); } /** * Date -> LocalDateTime * * @param date * @return */ public static LocalDateTime toLocalDateTime(Date date) { Instant instant = date.toInstant(); ZoneId zone = ZoneId.systemDefault(); return LocalDateTime.ofInstant(instant, zone); } /** * Date -> LocalDate * * @param date * @return */ public static LocalDate toLocalDate(Date date) { Instant instant = date.toInstant(); ZoneId zone = ZoneId.systemDefault(); LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone); return localDateTime.toLocalDate(); } /** * Date -> LocalTime * * @param date * @return */ public static LocalTime DateToLocalTime(Date date) { Instant instant = date.toInstant(); ZoneId zone = ZoneId.systemDefault(); LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone); return localDateTime.toLocalTime(); } /** * LocalDateTime -> Date * * @param localDateTime * @return */ public static Date toDate(LocalDateTime localDateTime) { ZoneId zone = ZoneId.systemDefault(); Instant instant = localDateTime.atZone(zone).toInstant(); return Date.from(instant); } /** * ZonedDateTime -> Date * * @param zonedDateTime * @return */ public static Date toDate(ZonedDateTime zonedDateTime) { Instant instant = zonedDateTime.toInstant(); return Date.from(instant); } /** * LocalDate -> Date * * @param localDate * @return */ public static Date toDate(LocalDate localDate) { ZoneId zone = ZoneId.systemDefault(); Instant instant = localDate.atStartOfDay().atZone(zone).toInstant(); return Date.from(instant); } /** * LocalDate,LocalTime -> LocalTimeToDate * * @param localDate * @param localTime */ public static Date toDate(LocalDate localDate, LocalTime localTime) { LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime); ZoneId zone = ZoneId.systemDefault(); Instant instant = localDateTime.atZone(zone).toInstant(); return Date.from(instant); } }
測試輸出結果 :
北京時間(GMT+08:00):
Mon Apr 06 15:27:56 CST 2020
轉換成東京時間(GMT+09:00)字符串:
2020-04-06 16:27:56.467
轉換成東京時間(GMT+09:00):
Mon Apr 06 16:27:56 CST 2020
東京時間(GMT+09:00)字符串:
2020-04-06 14:32:52.534
轉換成北京時間(GMT+08:00)字符串:
2020-04-06 13:32:52.534
轉換成北京時間(GMT+08:00):
Mon Apr 06 13:32:52 CST 2020