关于时区问题的来龙去脉

本文涉及的产品
RDS AI 助手,专业版
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
简介: 国际化的业务场景中经常会遇到时区转换的问题,处理不好就会产生各种各样的问题。本文详细介绍了时区问题产生的原因和常用解决方案

前言

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

认识时区


因为时区的存在,同一时刻不同地区的时间是不一样的。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与字符串之间的转换,丢失了时区信息,最终导致问题

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
SQL 前端开发 安全
详细介绍前后端分离必备的接口规范,包括命名规范、参数规范、错误处理规范等
详细介绍前后端分离必备的接口规范,包括命名规范、参数规范、错误处理规范等
3932 1
|
人工智能 网络协议 Java
RuoYi AI:1人搞定AI中台!开源全栈式AI开发平台,快速集成大模型+RAG+支付等模块
RuoYi AI 是一个全栈式 AI 开发平台,支持本地 RAG 方案,集成多种大语言模型和多媒体功能,适合企业和个人开发者快速搭建个性化 AI 应用。
2743 77
RuoYi AI:1人搞定AI中台!开源全栈式AI开发平台,快速集成大模型+RAG+支付等模块
|
存储 编解码 算法
视频编码格式和封装格式有什么关系?相机常见的编码格式有哪些?
视频编码格式与封装格式的关系类似于酒与酒瓶的关系。编码格式是视频的核心内容,如H.264、H.265等,而封装格式则是将视频、音频、字幕等集成在一起的外壳,如MP4、MKV等。不同的封装格式适应不同的播放需求,例如MP4兼容性最好,MKV适合网络传播。
|
存储 边缘计算 运维
光纤收发器:连接数字世界的桥梁
【10月更文挑战第22天】
1048 2
|
安全 应用服务中间件 网络安全
阿里云ssl证书简介和使用流程
了解如何在阿里云注册并实名账号,然后购买和部署SSL证书以增强网站安全性。阿里云SSL证书提供强大的加密、身份验证及SEO优势。通过简单流程购买适合的证书类型,如CFCA通配符OV证书,并在Nginx服务器上安装。遵循官方文档,下载证书,编辑Nginx配置并重启服务实现HTTPS。阿里云SSL证书是保障网站安全的高效解决方案。
1281 2
阿里云ssl证书简介和使用流程
|
机器学习/深度学习 分布式计算 算法
基于Spark中随机森林模型的天气预测系统
基于Spark中随机森林模型的天气预测系统
585 1
阿里云百炼大模型服务--模型训练指南
模型训练是通过Fine-tuning训练模式提高模型效果的功能模块,作为重要的大模型效果优化方式,用户可以通过构建符合业务场景任务的训练集,调整参数训练模型,训练模型学习业务数据和业务逻辑,最终提高在业务场景中的模型效果。
1239 0
|
前端开发 定位技术 数据格式
GeoServer使用CSS渲染地图
CSS Style是GeoServer的一个扩展插件,使用CSS写起来的地图渲染策略文件相比较SLD而言,非常的简洁,本文根据GeoServer用户手册,稍微改写,便于该知识点的推广。
1635 0
|
前端开发 网络协议 Dubbo
Netty - 回顾Netty高性能原理和框架架构解析
Netty - 回顾Netty高性能原理和框架架构解析
620 0
|
SQL Java 关系型数据库
Mybatis——动态SQL foreach批量操作
Mybatis——动态SQL foreach批量操作
1559 0