業務系統-全球化多時區的解決思路


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM