关于时区问题的来龙去脉

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 国际化的业务场景中经常会遇到时区转换的问题,处理不好就会产生各种各样的问题。本文详细介绍了时区问题产生的原因和常用解决方案

前言

国际化的业务场景中经常会遇到时区转换的问题,处理不好就会产生各种各样的问题。下面就产生时区问题的原因做个总结。时区的概念这里就不解释了

认识时区


因为时区的存在,同一时刻不同地区的时间是不一样的。Java中,这个“同一时刻”可以用java.util.Date#getTime表示,即距离"时间纪元"(1970年1月1日0时0分0秒)的毫秒数。如果在不同时区的国家部署多台服务器,服务器时区取当地行政时区(未指定情况下,Java默认使用系统时区)那么,同时在不同服务器上Date date = new Date(),那么date.getTime()的值是一样的

String format = "yyyy-MM-dd HH:mm:ss S";
//同一时刻不同时区的时间展示
Date date = new Date(1527405234768L);
SimpleDateFormat sdf1 = new SimpleDateFormat(format);
sdf1.setTimeZone(TimeZone.getTimeZone("GMT+8"));
System.out.println(sdf1.format(date));
SimpleDateFormat sdf2 = new SimpleDateFormat(format);
sdf2.setTimeZone(TimeZone.getTimeZone("GMT+7"));
System.out.println(sdf2.format(date));
//不同时区时间换算成同一时刻
String s1 = "2018-05-27 15:13:54 768";
String s2 = "2018-05-27 14:13:54 768";
SimpleDateFormat sdf3 = new SimpleDateFormat(format);
sdf3.setTimeZone(TimeZone.getTimeZone("GMT+8"));
System.out.println(sdf3.parse(s1).getTime());
SimpleDateFormat sdf4 = new SimpleDateFormat(format);
sdf4.setTimeZone(TimeZone.getTimeZone("GMT+7"));
System.out.println(sdf4.parse(s2).getTime());

运行结果:
2018-05-27 15:13:54 768
2018-05-27 14:13:54 768
1527405234768
1527405234768

时间的存在形式


JVM中同一时间可以用毫秒数long、字符串字面量+时区、java.util.Date对象来表示,3者可以等价转换。当一台服务器向不同时区的其他服务器传输时间时,可以使用以上任一方式传输:

  1. 毫秒数long:其他服务器根据自己的时区进行反序列化。时间可以正确处理
  2. 字符串字面量+时区:如果只传输字符串字面量str1,丢失时区信息timeZone1,那么其他服务器接收后就会以为字面量str1是基于自己的时区timeZone2的,反序列化后就得到了错误的时间。比如用json序列化一个Date对象进行传输而没有指定时区的场景
  3. Date对象:需要序列化成long,或者字符串字面量+时区

时区转换的场景

用户Browser <=> Web Server

Web Server => Browser

  1. 若二者时区相同(已将server服务器设置为当地时区),那么可以在server端将时间格式化为字符串字面量直接传输到Browser端展示。其中,若server端时间为其他时区的字符串字面量时,需转为当前时区;若为Date对象,直接format,Java默认取当前系统时区;若为毫秒long,转为Date再格式化
  2. 若时区不同,server端将时间转换成毫秒数long或者字面量+时区,传输到Browser,由browser端js基于browser的时区进行转换处理,最终展示。

Browser => Web Server

如用户通过前端时间控件选择的时间,需要转化为毫秒数long或者字面量+时区,传输到server端。

Server-n <=>Server-k

取决于时间的序列化和反序列方式,如HSF用hession将Date对象序列化为毫秒数、json将时间序列化为字符串(需要指定时区)

Server-n <=> DB-MySQL

时间序列化和反序列化

MySQL中时间可以用bigint(存毫秒数long),DateTime,TimeStamp表示。 使用bigint不存在时区问题,这里不写了。DateTime和TimeStamp详细区别参考官方文档。那么这里server和mysql的时间序列化方式是什么呢,可以看下MySQL的驱动包源码,下面贴下几个源码片段。代码有点多慢慢看,直接说结论吧:使用不带时区的时间字符串

public Timestamp getTimestamp(int columnIndex) throws SQLException {
checkRowPos();
checkColumnBounds(columnIndex);
return getDateOrTimestampValueFromRow(columnIndex, this.defaultTimestampValueFactory);
}
private 
 
   T getDateOrTimestampValueFromRow(int columnIndex, ValueFactory
  
  
    vf) throws SQLException {
   
   
Field f = this.columnDefinition.getFields()[columnIndex - 1];

// return YEAR values as Dates if necessary
if (f.getMysqlTypeId() == MysqlaConstants.FIELD_TYPE_YEAR && this.yearIsDateType) {
return getNonStringValueFromRow(columnIndex, new YearToDateValueFactory<>(vf));
}
return getNonStringValueFromRow(columnIndex, new YearToDateValueFactory<>(vf));
}
/**
* Get a non-string value from a row. All requests to obtain non-string values should use this method. This method implements the "indirect" conversion of
* values that are returned as strings from the server. This is an expensive conversion which first requires interpreting the value as a string in it's
* given character set and converting it to an ASCII string which can then be parsed as a numeric/date value.
*/
private T getNonStringValueFromRow(int columnIndex, ValueFactory vf) throws SQLException {
Field f = this.columnDefinition.getFields()[columnIndex - 1];

// interpret the string as necessary to create the a value of the requested type
String encoding = f.getEncoding();
StringConverter stringConverter = new StringConverter<>(encoding, vf);
stringConverter.setEventSink(this.eventSink);
stringConverter.setEmptyStringsConvertToZero(this.emptyStringsConvertToZero.getValue());
return this.thisRow.getValue(columnIndex - 1, stringConverter);
}

其中StringConverter中的createFromBytes方法部分代码如下

} else if (s.length() == MysqlTextValueDecoder.DATE_BUF_LEN && s.charAt(4) == '-' && s.charAt(7) == '-') {
return stringInterpreter.decodeDate(bytes, 0, bytes.length, vf);
} else if (s.length() >= MysqlTextValueDecoder.TIME_STR_LEN_MIN && s.length() <= MysqlTextValueDecoder.TIME_STR_LEN_MAX && s.charAt(2) == ':'
&& s.charAt(5) == ':') {
return stringInterpreter.decodeTime(bytes, 0, bytes.length, vf);
} else if (s.length() >= MysqlTextValueDecoder.TIMESTAMP_NOFRAC_STR_LEN
&& (s.length() <= MysqlTextValueDecoder.TIMESTAMP_STR_LEN_MAX || s.length() == MysqlTextValueDecoder.TIMESTAMP_STR_LEN_WITH_NANOS)
&& s.charAt(4) == '-' && s.charAt(7) == '-' && s.charAt(10) == ' ' && s.charAt(13) == ':' && s.charAt(16) == ':') {
return stringInterpreter.decodeTimestamp(bytes, 0, bytes.length, vf);
}

MySQL时间类型

  1. DateTime类型字段,MySQL存储时不存时区信息,并且怎么存就怎么取,不做任何处理和转换。所以时区timeZone1的server1插入MySQL一条记录后,时区timeZone2的server2读取出来的时间就不对了。这里只能将所有的server的时区设置为一样的,或者在数据库表中添加一个字段存储时区信息
  2. TimeStamp类型字段,这个比较特殊。当server创建connection时,可以在数据库URL中手动指定时区信息,即不同时区的server连接MySQL时,指定connection时区使用自己所在时区。当MySQL处理不同的connection时,就有了时间字符串和发出请求的时区,然后转换为UTC时间进行存储。从MySQL中读取时也是基于connection的时区设置进行转换。但是如果不指定connection时区,那么MySQL就将存储的UTC时间,按MySQL服务器所在时区进行转换和展示或者传输,此时若MySQL服务器和server的时区不一致,就会出现时区问题

由上可知,产生时区问题的根本原因在于不同时区的机器对时间进行序列化和反序列化时,Date对象或者毫秒数long与字符串之间的转换,丢失了时区信息,最终导致问题

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
监控 网络协议 Linux
Linux日期和时间管理指南:日期、时间、时区、定时任务和时间同步
Linux日期和时间管理指南:日期、时间、时区、定时任务和时间同步
196 0
|
存储 C语言 C++
软件开发入门教程网之C++ 日期 & 时间
软件开发入门教程网之C++ 日期 & 时间
|
存储 SQL 弹性计算
JVM时区配置-两行代码让我们一帮子人熬了一个通宵
不经意的两行代码让我们一帮子人熬了一个通宵
25546 10
|
存储 关系型数据库 MySQL
|
存储 前端开发 数据库
闲谈时间
闲谈时间
66 0
|
消息中间件 Dubbo NoSQL
老板,JDK8的日期、时间函数我不熟悉?
介绍JDK 8中的新日期工具类,及整理成PDF文档
77 0
|
安全 Java Linux
正确认识及掌握时间的用法
时间是一个相对地区而言的概念,因此有一个基准地区,就是本初子午线穿过的地区。了解世界时间相关的概念可以更好地协调全球人们的活动,便于跨越不同地区的时差。比如按照UTC时区划分算,洛杉矶和北京 之间的时间差异是16个小时, 但是一旦洛杉矶启用了夏令时两者之间的时间差异只有15个小时,神奇吗?
324 0
正确认识及掌握时间的用法
|
存储 开发工具
我花了一个星期,做出了公司的管理系统,只需几个步骤!
我是企业的管理人员,公司发展到现阶段,感觉进入到了瓶颈期,每个员工的工作都已经饱和,很难再挤出时间做其它的事情,需要一款合适的管理软件来协作我们的工作。本来打算买一套管理软件就行了,现实却并没有那么简单。
我花了一个星期,做出了公司的管理系统,只需几个步骤!
|
关系型数据库 MySQL Java
还在为时区问题发愁?获取的时间与中国时间差八小时怎么办?史上最全的解决方案总结
还在为时区问题发愁?获取的时间与中国时间差八小时怎么办?史上最全的解决方案总结