时区处理综述(java技术栈)

6 篇文章 0 订阅


通过这篇文章,澄清以下几个对时间数据进行序列化/反序列化的阶段:
mysql进程从数据文件读写时间字段(存储IO)、jdbc协议传输时间字段(网络IO)、http文本协议传输json序列化后的时间字段(网络IO);
让java开发员确信目前使用的技术栈,实际已经是在正确处理跨时区的时间信息传输

一、jdbc协议对时间的序列化(不含时区信息)

jdbc客户端需要正确获取到数据库运行时区,才能正确读写时间类型的列

数据库的两个时间类型

https://dev.mysql.com/doc/refman/8.0/en/datetime.html
datetime和timestamp在mysql内部的存储方式不同,且理论上timestamp类型的数据在数据库时区变更后依然能正确读取到存入时的数值。
但是,时间数据从本地数据文件反序列化到数据库内存后,还要经过jdbc协议(再一次进行序列化)传输到远程java应用;时间信息的错误处理,通常是发生在java应用(jdbc客户端)的反序列化阶段。

CREATE TABLE `test_datetime` (
`id` bigint(21) NOT NULL AUTO_INCREMENT COMMENT '流水号',
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`create_time` timestamp(3) DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`modify_time` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间(每次更新都记录)',
PRIMARY KEY (`id`),
UNIQUE KEY `index_create_by` (`create_by`)) 
ENGINE = InnoDB CHARSET = utf8mb4 COMMENT '验证'; 

中国数据库使用的CST时区无法被java正确识别

MySQL将来自操作系统的CST时区识别为中国标准时间;
jdbc客户端8.0.22及以下版本,虽然会主动获取数据库的时区配置,但会将CST认为是美国中部时间,这就导致反序列化后的Date与实际时间会相差13小时,如果处在冬令时还会相差14个小时;
无论是datetime还是timestamp数据,在通过jdbc协议发送前,会使用数据库运行时区进行序列化(序列化结果不含时区信息),而jdbc客户端认为收到的是美国中部时区的年月日、时分秒,于是转化为一个错误的java.util.Date。

mysql-connector-java的8.0.23版本开始对CST时区进行了解析修复,对应到了Asia/Shanghai。

jdbc协议跨时区传输方案

jdbc的url参数里,明确指定数据库服务器实际运行时区后,可避免jdbc协议序列化反序列化时间数据的异常

?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull

通过jdbc协议能否获取到正确的Date数据,和java进程本身的时区没有联系,通过修改java进程默认时区即可验证这点:-Duser.timezone=GMT+9

二、数字时间戳(可跨时区传输)

使用数字时间戳来传输时,发送方与接收方不需要知道对方时区,也能正确传递时间信息

定义

UNIX认为GMT时区的1970年1月1日零点是时间纪元起始;

大多数语言/系统的时间戳就是相对上述时间点的毫秒数,使用long类型记录和传输

用途

java.util.Date类型内部实际使用数字时间戳(private transient long fastTime)记录时间数据,所以java的Date数据是在时间轴上的确定值,与进程、服务器时区配置没有任何联系;java.sql.Timestamp继承自Date,扩展了对纳秒信息的记录。所以Date进行java序列化或hessian2序列化后,都可安全跨时区传输和还原。

json反序列化时(把json格式的字符串转换为内部对象),各框架(jackson、fastjson)都能自动把数字字段转换到java的Date字段;json序列化时,各框架(jackson、fastjson)都默认是把Date字段序列化为数字时间戳。

java和javascript的Date构造函数,都可用数字时间戳作为初始化参数;java和javascript的Date都有getTime方法来获取内部存储的数字时间戳。

浏览器端提交的数据字段,在服务端是时间类型时,在浏览器端提交数字时间戳即可自动正确转换,即使两端时区不一致。

三、ISO8601(可跨时区传输)

使用ISO8601格式的字符串来传输时,发送方与接收方不需要知道对方时区,也能正确传递时间信息

java的序列化方法

// 解析
OffsetDateTime dateTime = OffsetDateTime.parse("1997-07-16T19:20:30.010+01:00");
 
// 序列化(线程不安全),默认使用进程时区,可以另外指定
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
log.info(format.format(Date.from(dateTime.toInstant())));

controller层返回结果时,SpringMVC使用的jackson默认把Date字段序列化为ISO8601字符串,可通过配置项修改为数字时间戳:

spring.jackson.serialization.write-dates-as-timestamps=true

建议前端先转化为Date数据类型后,再根据浏览器本地时区展示为可阅读的字符串

js的序列化方法

// Date的构造函数既可传入时间戳数值,又可传入ISO8601字符串
new Date('1997-07-16T19:20:30+01:00');
 
// Date实例的toJSON、toISOString方法都能获取到ISO8601格式字符串
// JSON.stringify方法处理Date类型的字段时,就是用toJSON进行序列化
(new Date()).toJSON();
(new Date()).toISOString();

在浏览器端用JSON.stringify处理js对象的Date字段后(ISO8601序列化)再传输,即使两端时区不一致,服务端也能正确解析到java的Date字段。

所以XMLHttpRequest和SpringMVC的交互,默认就是可跨时区的,只要序列化前的时间信息字段是Date类型。

四、解析时间字符串(不含时区信息)

java服务端、sql里尽量避免直接处理不含时区信息的时间字符串;
无法避免时,比如处理csv文件里的时间字符串,就需要慎重确认是不是适合使用服务端java程序的启动时区,是否应该要求客户端明确指定一个时区。

夏令时

如果时区信息包含夏令时/冬令时处理规范,则解析不含时区信息的时间字符串时,会进行偏移处理;比如下面的例子,"GMT+8"时区不进行时间偏移,"Asia/Shanghai"时区进行时间偏移

SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
isoFormat.setTimeZone(TimeZone.getTimeZone("GMT+8"));     

String strDate = "1989-05-31 08:00:00"; //不含时区信息

SimpleDateFormat gmt8Format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
gmt8Format.setTimeZone(TimeZone.getTimeZone("GMT+8"));

SimpleDateFormat shanghaiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
shanghaiFormat.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
log.info("不使用中国夏令时:{},使用中国夏令时:{}", isoFormat.format(gmt8Format.parse(strDate)),
        isoFormat.format(shanghaiFormat.parse(strDate)));

sql函数

STR_TO_DATE
DATE_FORMAT
如果在sql执行时进行时间字符串转换,那么结果受当前session的时区信息影响;
jdbc建立的session,固定使用数据库运行时区(不受serverTimezone配置、客户端进程时区的影响),可通过获取select @@SESSION.time_zone的结果进行确认;
如果java应用非要把字符串写入数据表的datetime/timestamp字段,这个字符串必须表示的是数据库默认时区下的时间点(强烈不推荐)

# 时区是否支持夏令时规范、是否在经度上一致,影响获得的数字时间戳
SET time_zone = 'Asia/Shanghai';
SELECT UNIX_TIMESTAMP(STR_TO_DATE('1989-05-31 08:00:00','%Y-%m-%d %T'));
SET time_zone = '+08:00';

SELECT DATE_FORMAT(NOW(6), '%Y-%m-%d %T.%f');

五、跨时区binlog同步

master-slave原生同步

因为无法指定slave连接master时使用的时区,binlog里序列化的datetime数据无法正确反序列化,所以master、slave的时区配置不一致时,不能正确同步

otter同步

因为存在中间转换,只要otter连接到两个数据库节点的serverTimezone配置正确,即可正确反序列化源库binlog里的时间数据,并通过jdbc写入目标库

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值