• 业务系统-全球化多时区的解决思路


    本人前段时间经历了一个全球化的报表项目(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("
    东京时间(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

  • 相关阅读:
    二分查找 【数组的二分查找】
    二分答案 [TJOI2007]路标设置
    队测 逆序对 permut
    【线段树】 求和
    状压DP Sgu223 骑士
    状压DP Poj3311 Hie with the Pie
    状压DP入门 传球游戏之最小总代价
    状压DP 小W选书籍
    状压DP [Usaco2008 Nov]mixup2 混乱的奶牛
    __gcd()用法
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/multi-timezone-support.html
Copyright © 2020-2023  润新知