你遇到这么可怕的虫子会怎么办(你还能坚持多久)

某日吃过午饭,正准备小憩的我被测试小姐姐传唤了,传唤理由:某个服务突然报错(上午可以正常使用,并且期间没有做过任何变动),不管如何更换调用的姿势,都返回异常。本着负(怼)责(不)任(过)的工作态度,登录了测试服务器。

问题现象

先说一下现象吧:在对一张表执行insert操作时,mysql报错:

com.mysql.jdbc.MysqlDataTruncation: Data truncation: Incorrect datetime value: 2038-01-19 13:15:24 for column REQUEST_TIME at row 1

问题原因

仅看这个报错让人很摸不到头脑,时间明明是正确的,为毛就被Incorrect了。带着疑问,我查看了`REQUEST_TIME`字段的定义,如下图。

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(1)

表结构截图

原来,`REQUEST_TIME`被定义成了timestamp类型,而timestamp是有范围限制的。我查阅了timestamp的相关资料:

  • timestamp(N),N取值范围为0-6,默认值为0,N的不同代表了精度的不同,N越大精度越高,默认精度为精确到秒;
  • timestamp默认支持的的取值范围是:1970-01-01 00:00:01 UTC 至 2038-01-19 03:14:07 UTC。

这样,问题就查清楚啦,由于数据超过了timestamp允许的范围,才导致各种报错情况出现的。经过向测试小姐姐咨询,原来,最近服务器资源有限,这台服务器被分配给了另外一个项目组作为测试服务器,在测试过程中会不断切换日期,从而暴露了这个问题。

解决办法

既然timestamp只支持到2038-01-19 03:14:07 UTC,那么如何保存之后的时间呢?其实除了timestamp外,MySql还提供了datetime类型用来保存日期时间,datetime支持的时间范围为:1000-01-01 00:00:00 至 9999-12-31 23:59:59。

如果需要保存2038年1月19日之后的时间,将字段的数据类型更换成datetime就可以啦。在java中,对于日期时间的维护大多会通过PreparedStatement.setTimestamp()方法来实现,即使在MyBatis中,也是这样的(具体可以参见:org.apache.ibatis.type.DateTypeHandler.setNonNullParameter()方法)。所以,只需要将`REQUEST_TIME`字段的数据类型由timestamp修改为datetime即可!

与测试小姐姐的互动(追问)

问:timestamp的取值范围是1970-01-01 00:00:01 UTC 至 2038-01-19 03:14:07 UTC,为什么上午可以,上午的日期同样是2038-01-19。

答:UTC是国际标准时间(格林威治时间),而我们所在的时区是东8区,所以取值范围要加8个小时,这样我们可以测试一下,更新 2038-01-19 11:14:07 到数据库时可以正常更新,更新 2038-01-19 11:14:08 到数据库时就会报相同的错误


问:同样是保存时间,而且精度也一样,为什么timestamp和datetime的取值范围会不一样。

答:是因为默认情况下timestamp和datetime所占用的字节数不同,存储数据的方式也不相同。timestamp占用4字节,保存了从1970-01-01 00:00:00 UTC开始一共走过的秒;datetime占用了5个字节,其中包括了1位标志位,17位的年月,5位日期,5位小时,6位分钟,6位秒,正好5字节。而由于timestamp只使用了4字节来保存秒,也就是说最大只能保存Integer.MAX_VALUE秒,换算以后,也就是只能保存到 2038-01-19 03:14:07 UTC了。

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(2)

测试Integer.MAX_VALUE对应秒的deadLine


问:timestamp保存时间的方式我清楚了,datetime里日期、小时、分钟、秒也都好理解,17位的年月是什么鬼?保存9999年需要14位,保存12月需要4位,加一起应该是18位啊,怎么通过17位就保存了?

答:这里是通过除数/余数来分别表示年和月的,也就是说实际保存的数值=年*13 月。即使9999年12月,用这种算法计算得到的值为9999*13 12=129999,用17位就可以保存了(画外音:不服不行,而且datetime还预留了一位的标志位sign,这是要支持公元前的节奏啊)。

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(3)

datetime编码格式


问:既然datetime只比timestamp多占了1个字节,但是却能多保存好几千年,那为啥还要用timestamp呢?

答:我承认我在这个地方用timestamp确实是脑子抽抽了,但是不能完全否了timestamp的优势啊。timestamp直接保存秒值,相对datetime的结构要简单的多,遇到运算或者索引时效率会相对高一些,而且timestamp可以根据连接的时区显示对应时区的时间,此外,timestamp还提供了一些天然的非空能力(如果为空则使用当前时间戳进行保存)。


问:刚才提到timestamp和datetime默认是秒,但是可以支持到微秒的精度。本身的字节不是只保存了秒值么?毫秒、微秒存在哪里了?

答:咱们说的4字节、5字节都是在默认情况下说的,也就是精度为0的时候。当精度发生变化时,所占用的字节数也会相应的增加。咱们表达要严谨,4/5字节是默认的大小。

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(4)

日期/时间类型存储说明


问:既然timestamp只能支持到2038年,如果数据库安装服务器的时间变成了2038年会怎么样?

答:额~没试过。来来来,一起试一下。使用root用户设置系统时间:

date -s 2038-01-01

貌似数据库没啥反应,来执行一下now()

select now();

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(5)

select now() 的查询结果

看来是日子还没到,接下来吧时间修改到1月19日11时14分。

date 011911142038.00

奇怪,MySql怎么突然断掉了。查看mysql服务的error日志发现,mysql确实自己把自己搞死了。

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(6)

mysql shutting down

看来,mysql发现服务器的时间超过了他自己能承担的时间后就自杀了。不过,我觉得他还有抢救的空间,启动一下试一试。结果...

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(7)

mysql启动错误


对于亲手逼死MySql这件事,或许是让测试小姐姐感到些许内疚,一言不发的回到了自己的工位上,我终于可以喝杯水压压惊了。然而,我似乎成功的勾起了测试小姐姐对mysql的兴趣。半小时之后,测试小姐姐再次找到了我,说是找到了mysql的一个BUG。

问:datetime支持的时间不是1000-01-01 00:00:00 至 9999-12-31 23:59:59。为啥0000-00-00 00:00:00也能正常保存。

我:这个复现简单,我也来试一下。果然和测试小姐姐描述一致,下图是证据。

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(8)

mysql官方文档

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(9)

表结构.工具版本比较老,datetime看不到长度,实际长度是3

你遇到这么可怕的虫子会怎么办(你还能坚持多久)(10)

查询结果

看到我一脸大写的懵逼,测试小姐姐留下了一个鄙视的眼神便走了。剩下一脸迷茫我的不知所措,有大神能指点迷津么?

我是一名程序员,更多精彩请关注:空心小窝头

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。