时间的基础知识

前言

时间处理是所有程序员都必须面对的难题,如果你遇过以下问题:

  • 时间显示时总是差八个小时时差
  • 存储在数据库的时间看起来没问题,就是程序读出来的时候差了八个小时时差
  • 浏览器传过到后台的时间,差了八个小时时差

八个小时时差这个事,好像总是跟程序员过不去,而程序员总是糊里糊涂的把时差补上去完事,到底真正原因是什么却不知晓。

时间的本质

假如你身在中国,需要和一个在英国的同事进行一个电话会议。首先约个通话时间吧。早上9时吧。算一下不行,英国GMT+0时区,他们那边是午夜1时,还没上班呢。那约下午2时吧,等于他们的上午9时,可以了。

时间点,是一个绝对概念,好比约个电话会议:不管你在哪里,一个时间点,就是同一个时刻,电话会议若是错过了就是错过了。北京时间14时,和英国时间09时,就是同一个时间点。(这里不会考虑相对论中对时空的理解可能带来的问题)

一个时间点所衍生的,是不同观察者对这个时间点的解析。同一个时间点,我在我的时区是14时,你在你的时区是09时,其他人在不同时区是05时,具体体现的时间取决于你在那个时区,但都是相同的时间点。所以时间点是绝对的概念,时间是相对的概念,取决于你当时所在的时区。

时间的表达方式

运行结构

一个时间点,在计算机中不同语言或环境会用不同的表达方法。一个特殊时间点叫Epoch (t=0),代表着一个特殊时间点(在Wikipedia的解析:The “epoch” serves as a reference point from which time is measured)在Unix中,Epoch就是 1970-01-01 00:00:00.000 GMT。Epoch标志着0在哪里,作为其他时间点的参考点:比如说如果 t=86400秒, 代表的就是1970-01-02 00:00:00.000 GMT 这个时间 (或者在不同时区的观察者而言,这个点是 1970-01-02 08:00:00 GMT+8)

在Java中,时间点会用一个long表示,表示自Epoch后过了多少毫秒的意思。大家比较熟悉的是:System.currentTimeMillis() 就是当前时间点。按这样算,一个long最大值可以到9223372036854775807,换算一下,可以表达2亿多年后的时间了。

long表达时间点,两个相同的时间点就是两个long值的相等,实现起来非常简单。

在此强调一下,Java中的java.util.Date仅仅是一个long的具体形象化的表现,大家可以把Date对象等同与一个long来看待,所以Date的构造函数,用一个long就可以了,而Date对象并没有包含时区概念。

当我们需要把一个Date打印出来看看的时候(或者toString()的时候),Java会默认通过本机操作系统的时区来打印(或者变成时间字符串)。

	// 用默认时区格式化时间
	SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

	Date d = new Date(86400000L);
		
	System.out.println("Date (Default TZ): " + d);
	System.out.println("Formatted (Default TZ): " + df.format(d));

	// 设置全局时区
	TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
	// 修改格式化所用的时区
	df.setTimeZone(TimeZone.getTimeZone("GMT"));

	System.out.println("Date (in GMT): " + d);		
	System.out.println("Formatted (in GMT): " + df.format(d));

输出如下:

Date (Default TZ): Fri Jan 02 08:00:00 HKT 1970
Formatted (Default TZ): 1970-01-02 08:00:00
Date (in GMT): Fri Jan 02 00:00:00 GMT 1970
Formatted (in GMT): 1970-01-02 00:00:00

大家编程的时候,不要随便更改系统默认(全局)时区,因为这个配置是全局的,万一默认时区在一个地方改了,其他地方并不知道默认时区被改了,影响可大了。

上面说了基础Java对时间的表达方式,当然也有其他比较完善的工具包如JodaTime,让可以对时间进行更细致的定义和操作,这里不继续展开了。

重点是:观察者,观察者,观察者。是观察者去理解时间点,而不是时间点本身包括了时区信息(针对Date对象而言)。

存储方式

数据库存储结构

我们看看MySQL数据库如何存储时间类型的:

https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html

在内部格式中,除了TIMESTAMP的描述有关时区以外 (seconds UTC since the epoch),都看不到有时区的存储结构的描述。所以MySQL中的时间类型的存储,是没有【时区】存储在数据库中的。

假如你存下了时间 2018-01-01 10:23:45到数据库中,这时间在理解上是有歧义的:到底你这个时间是英国时间,还是中国时间?单凭存储的这个时间值不能下定论,因为这个时间不能代表一个时间点。

这问题通常需要通过系统设计者的定义来解决。设计师会说明数据库存储的时间是什么时区,而且会用统一时区。比较普遍的做法是使用GMT/UTC时区(0时区),这个做法适合多时区的应用。而另一个做法是存储本地时区的,但这个定义可能不太适合需要支持多时区的应用。按上面例子:2018-01-01 10:23:45 这个时间就能明确下来了, 它代表着 2018-01-01 10:23:45 GMT 这个时间点。

如果你的数据结构文档中,没有定义时间存储的时区,大概你需要先定义一下了。

JDBC的影响

几乎所有Java框架存储结构,最终都会转化成JDBC的调用,而JDBC在时间存储的接口有:ResultSet.getTimestamp(), PreparedStatement.setTimestamp() 等。我们知道数据库没有在时间字段中存储时区,JDBC驱动怎么知道从数据库读出来的时间 2018-01-01 10:23:45 是什么时间点呢?

在MySQL的JDBC驱动里,有个关于时区的配置有十几个,看到有点头晕:

https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html

简单点说,加一个连接参数 serverTimezone=UTC 或者你想用数据库存储时间的时区,就可以避免数据库操作系统时区配置的差异导致问题。

传递方式

跨系统的时间传递,也可能因为定义不清楚,导致传来的时间存在解析歧义。如果你接收一个JSON格式:

{
	"username": "hello",
	"loginTime: "2018-04-07 10:24:14"
}

从内容上看,loginTime字段存在理解歧义,因为这里的时间是什么时区的,没有明确定义。所以在交互协议层的API定义中,如果出现这种情况,必须说明这个字符串有没有隐含什么时区,是不是默认中国时间?

消灭歧义的唯一方式,都需要通过准确定义来解决,没有其他办法。

在线格式定义好以后,发送方和接收方双方面就可以做相应的工作。发送的时候,按格式输出;接收的时候,按格式解读。

如果使用框架自带的格式化/序列化功能,如Jackson,Date类型会变成一个long,而long本身代表Epoch后多少微秒,时间点不存在歧义。

倘若你自己处理时间格式,就需要理解到底序列化的过程中,有没有失真。如果Date转成 “yyyy-MM-dd HH:mm:ss”,内容就有所失真了,因为这样的格式,不足以代表一个时间点,因为存在理解歧义,如果真的需要定义这个格式,必须配合文档说明这个字符串写的时间的是什么时区,才能消除理解歧义。而且你在序列化过程中不做精准控制(如没配置好默认时区,或SimpleDateFormat没有配置时区),有可能你设计的格式,和真实的输出格式,因为操作系统的配置差异而不同,尤其是生产系统通常是UTC时区的,当你按照开发时的桌面操作系统来验收,结果生产系统出来的是另外一个时间,到时候才排查问题就不是太理想了。

总结

我们需要从不同层面的时间定义上理解透彻,才能彻底解决这类问题,而不是随便调整过来,看上去还可以,过几天连自己做过什么都不太记得了。这样的调整对自身学习毫无价值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值