国际化项目对时间的处理

时间戳&时区的理解

Posted by zhida on December 12, 2018

最近在做一个中美的项目,涉及对时间的转换处理,做了一些知识点的整理,防止后人采坑。

前面部分主要对时间戳的相关知识点普及,后面部分是项目中的采坑经历。

时间戳

Unix时间戳,是一种时间表示方式,定义为从格林威治时间1970年01月01日00时00分00秒起至现在的总秒数,是一个绝对值。因此,严格来说,不管你处在地球上的哪个地方,任意时间点的时间戳都是相同的,与时区没有任何关系。这点有利于线上和客户端分布式应用统一追踪时间信息。

时区

时间戳全世界都一样,但是在不同的地方对于时间的表达是不一样的,简单来说就是各个地区看到太阳升起的时间不一致,所以会有时区的概念,不同的时区,会对时间戳进行不同的处理,以适应当地的环境。

但是。每个时区获取的时间戳肯定是一样的。

中国时区

中国土地辽阔,横跨五个时区,但是为了统一管理,都是使用东八区,比标准时间快8小时。

需要额外注意的是: Asia/Beijing 时区是不存在的,请使用Asia/Shanghai or Asia/Chongqing代替

夏令时

夏令时(Daylight Saving Time:DST),是一种为节约能源而人为规定地方时间的制度,在这一制度实行期间所采用的统一时间称为“夏令时间”。

一般在天亮早的夏季人为将时间调快一小时,以充分利用光照资源,从而节约照明用电。各个采纳夏时制的国家具体规定不同。目前全世界有近110个国家每年要实行夏令时。

美国是采取夏令时的国家,中国不是,这样就会造成一个问题:夏令时中美时差会增加一小时。

一般在时间处理中不需要额外判断夏令时这一条件,当你要考虑夏令时的时候,就要理一理你的思路是否正确了。

Date对象

JAVA中Date对象,并没有时区的概念,他只有一个核心属性,就是时间戳

public Date() {
    this(System.currentTimeMillis());
}

格式化

格式化是对一个时间,按照不同地区的表达格式展现出来,并没有涉及时区的处理

public static void main(String[] args) {

    Date date = new Date();

    Locale locale = Locale.CHINA;
    DateFormat shortDf = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM, locale);
    System.out.println("中国格式:"+shortDf.format(date));

    locale = Locale.US;
    shortDf = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM, locale);
    System.out.println("美国格式:"+shortDf.format(date));
}

输出结果:可以看到时间是一致的,只是格式有差异

中国格式:2019-2-18 14:12:13
美国格式:Feb 18, 2019 2:12:13 PM

时区转换

Date date = new Date();
DateFormat shortDf = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM);

shortDf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
System.out.println("中国当前日期时间:" + shortDf.format(date));

shortDf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println("美国当前日期时间:"+shortDf.format(date));

输出结果:

中国当前日期时间:2019-2-18 14:22:07
美国当前日期时间:2019-2-17 22:22:07

数据库时间保存

这是最容易采坑的地方,数据库对时间的储存主要有两种数据结构:

DateTime类型:

默认情况下,DATETIME的值范围为1000-01-01 00:00:00至9999-12-31 23:59:59

和java的Date对象一样,没有时区(timezone)的概念,存入的就是日期字符串,丢失了时区。

潜在的风险点:不同时区的应用服务器获取相同时间字符串的时候,会自动根据当前的时区进行时间戳转换,于是造成不同区域的两个应用服务器获取的时间戳不一致。

Timestamp类型存储:

TIMESTAMP值范围从1970-01-01 00:00:01 UTC到2038-01-19 03:14:07 UTC

Timestamp 是带时区的。修改 time_zone时,再次查询,会发现时间的显示发生的变化。Timestamp会根据不同的时区自动进行时间的更新,不同版本的数据库处理策略不一样,很容易就采坑。

潜在的风险点:对不同版本的数据库策略了解不清楚,出现日期误判处理。

数据库的时区参数:

时区设置顺序:

  • 数据库实例启动,从my.cnf配置参数timezone=timezone_name获取,若my.cnf未设置则从操作系统获取环境变量TZ获取
  • 数据库当前实际使用的时区,默认为system,即系统时区,其设置可以通过set global time_zone=''设置,其中参数值有以下三种形式:’SYSTEM’ 、’+10:00’ 、’Europe/Helsinki’
解决方案:
  • 1: 遵循设计原则:将数据的储存和展示分开,把表示绝对时间的时间戳存入数据库,在显示的时候根据用户设置的时区格式化为正确的字符串
  • 2: 储存日期字符串 + 时区的形式

两种方案各有好坏:第一种方式对于聚合查询统计,会存在挑战。

实际项目中采的坑:

假设中美时区相差16个小时。

A应用,中国的服务器,向美国服务器的数据库中插入一条数据: 2018-01-01 18:00:00

B应用,美国的服务器,向美国服务器的数据库取出这条数据: 2018-01-01 18:00:00

要求展示正确的美国时间,正确的应该是 2018-01-01 02:00:00, 实际展示的时间是 2018-01-01 18:00:00

我们看一下过程发生了什么:

时间 场景 时间戳
2018-01-01 18:00:00 中国应用服务器 x
2018-01-01 18:00:00 美国数据库 null
2018-01-01 18:00:00 美国应用服务器 x+16*60*60*1000
2018-01-02 10:00:00 美国应用服务器.setTimeZone(“Asia/Shanghai”) x+16*60*60*1000

通过表格可以发现,存入的时间戳和 数据取出的时间戳 是不一致的。时间已然对不上。

  • 如果直接展示时间:就是2018-01-01 18:00:00
  • 如果在美国服务器设置中国的时区进行转换,就会发现时间变成 2018-01-02 10:00:00

解决方案是:

先把时间戳(x+16*60*60*1000)转化成日期字符串: 2018-01-01 18:00:00

创建DateFormat ,设置中国时区,解析成Date对象,得到正确的时间戳 x

DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
try {
    long time = df.parse(formatStr).getTime();

    return time;

} catch (ParseException e) {
    e.printStackTrace();
}

总结

在国际化的项目中,要注意以下几个点:

  • 前后端的交互使用时间戳传递,不要做日期的转换。
  • 数据库的日期储存,要优先使用时间戳的方式,如果非得储存日期字符串,请加上时区标识
  • 最差的情况下:数据库储存日期字符串,请先调研清楚需求要怎么展示,了解数据库的数据来源。
  • 注意正确的中国时区 Asia/Chongqing or Asia/Shanghai

参考网站

附录:

public static void main(String[] args) {

    Date date = new Date();
    System.out.println("中国当前时间戳: " + date.getTime());

    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String formatStr = simpleDateFormat.format(date);
    System.out.println("中国当前日期显示:" + formatStr);

    DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    df.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
    long japanTime = 0L;
    try {
        japanTime = df.parse(formatStr).getTime();
        System.out.println("当前日期-日本时区时间戳:" + japanTime);
    } catch (ParseException e) {
        e.printStackTrace();
    }

    DateFormat shortDf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    shortDf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
    Date japanDate = new Date(japanTime);
    System.out.println("日本时区时间戳-日期显示:" + shortDf.format(japanDate));

    DateFormat chinadf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    chinadf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
    System.out.println("中国时区时间戳-日期显示:" + chinadf.format(japanDate));

}
中国当前时间戳: 1550570735560
中国当前日期显示:2019-02-19 18:05:35
当前日期-日本时区时间戳:1550567135000
日本时区时间戳-日期显示:2019-02-19 18:05:35
中国时区时间戳-日期显示:2019-02-19 17:05:35