本人前段時間經歷了一個全球化的報表項目(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
