专栏首页菩提树下的杨过业务系统-全球化多时区的解决思路

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ExtJs学习笔记(2)_Basic GridPanel[基本网格]

    这一节,将学习如何使用网络上最常见的UI控件_Grid 1.静态示例: 静态示例其实官方下载包里,就有sample,这里只贴出代码,后面的如何跟WCF结合,做出...

    菩提树下的杨过
  • 打印机设置(PrintDialog)、页面设置(PageSetupDialog) 及 RDLC报表如何选择指定打印机

    如果一台电脑同时连接多个打印机,而且每个打印机使用的纸张大小各不相同(比如:票据打印钱用的小票专用张,办公打印机用的是A4标准纸),在处理打印类的需求时,如果不...

    菩提树下的杨过
  • oracle: job使用

    oracle的job,实际上就是数据库内置的定时任务,类似代码中的Timer功能。下面是使用过程: 这里我们模拟一个场景:定时调用存储过程P_TEST_JOB ...

    菩提树下的杨过
  • check cve

    今天想检查一下 Gitlab 11.9.0 产品受哪些 cve 的影响。其实网上已经有很多网站可以查询产品的相关 cve,但就是粒度比较粗。我想在 cve 列表...

    madneal
  • Geohash之范围搜索

    很多时候,我们都会遇到这样的需求:查找某个点周边多少距离的点。从本质来说,是一个缓冲区分析+空间查找,本文结合Geohash来实现类似的功能。

    lzugis
  • 业界难题-“跨库分页”的四种方案

    一、需求缘起 分页需求 互联网很多业务都有分页拉取数据的需求,例如: (1)微信消息过多时,拉取第N页消息 (2)京东下单过多时,拉取第N页订单 (3)浏览58...

    架构师之路
  • Delete Node in a Linked List

    参考了https://miafish.wordpress.com/2015/07/26/leetcode-ojc-delete-node-in-a-linked...

  • Flask-email 发送邮件的配置,发送附件的方法,以及os.environ.get('MAIL_USERNAME')为None的解决办法

    用户1214487
  • ASP.NET Core 开启后台任务

    本文告诉大家如何通过 Microsoft.Extensions.Hosting.BackgroundService 开启后台任务

    林德熙
  • Netty4.x整合SpringBoot2.x使用Protobuf3详解

    本篇文章主要介绍的是SpringBoot整合Netty以及使用Protobuf进行数据传输的相关内容。Protobuf会介绍下用法,至于Netty在netty ...

    sanshengshui

扫码关注云+社区

领取腾讯云代金券