• 时区-Linux Java ConnectorJ MySQL-时间戳-EverNote


    问题描述

    我们最近遇到了一个问题,是时区导致的,测试代码往数据库里写了一个字符串时间,比如 2021-12-08 22:04:05 ,然后业务查出来的却是 2021-12-08 14:04:05,这个字段的定义是datetime;问题出在哪里了呢?
    谁谁转换时间的时候带上了时区?注意此时测试主机、MySQL、模块都是CST时区即北京时间。
    测试认为,我写入的啥时间字符串就应该返回啥字符串,就不应该存在时区转换问题。

    猜测

    很容易想到,直接原因肯定就是第一个访问数据库的模块与数据库时区不一致,造成数据库中DateTime在取出来的时候发生了转化,当然这是猜测的。
    当时另一个猜测是,第一个访问数据库的模块不是UTC时区,它在把DateTime转Timestamp时出了错。
    到底是哪个原因呢?

    跟踪请求调用

    请求流程是这样 请求代码–>Python模块A–>Java模块B–>MySQL,同时会原路返回
    核实到模块A收到模块B的回复中是个时间戳,这个时间戳T1转换成时间字符串并不是期望的时间,而是减了8h(错误的直接原因);手动转换T1,在UTC时区,这个模块A的转换本身是没错的【这里说的什么?】;模块A将时间戳不带时区信息地转换成了时间字符串。
    模块B中对entity的定义中,该字段为java.util.Date,那它返回给A的Json,是怎么变成时间戳的呢?我注意到模块B提供的Rest HTTP API是通过一个叫Jersey的包(它居然还是在javax包下),那这个框架是怎么把对象序列化到Json的呢?通过日志看到,模块B打出了请求内容到请求返回的所有字段,依此按图索骥查得为Jackson的ObjectMapper,这就好办了。

    Jackson与java.util.Date

    于是本地自己测试ObjectMapper,追踪到com.fasterxml.jackson.databind.ser.std.DateSerializer#_timestamp方法

    /**   protected long _timestamp(Date value) { 
    *       return value == null ? 0L : value.getTime(); 
    *     } 
    *  
    * 时间戳是没有时区概念的,它表示的就是UTC的1970年那个时间点距今的秒数 
    * 同样System.currentMillions 
    * 同样Date.getTtime-->getTimeImpl 
    *  
    *         private final long getTimeImpl() { 
    *         if (cdate != null && !cdate.isNormalized()) { 
    *             normalize(); 
    *         } 
    *         return fastTime; 
    *     } 
    *  
    * isNormalized 表示是否正常的时间,如果调用了什么addHour,addYear,就不是normalized的了 
    * 所以结论就是,Spring 等使用的反序列化工具是不考虑时区的
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    就是返回了Date#getTime方法,注意:时间戳是没有时区概念的
    所以结论就是,Spring 等使用的反序列化工具是不考虑时区的

    字符串时间与时间戳
    String dateStr = "2021-12-08 16:00:00";
            
    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date date = format.parse(dateStr);
    System.out.println(date.getTime());
    
    TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
    SimpleDateFormat format2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    date = format2.parse(dateStr);
    System.out.println(date.getTime());
    */*
    输出为
    1638979200000
    1638950400000
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    注意TimeZone的修改必须在定义DateFormat之前;必须重新定义衣蛾DateFormat,详情见DateFormat的构造方法初始化日历的时候用了默认时区
    可见时间戳与字符串时间之间相互转化是依赖时区的,Date实例本身是没有时区的

    Linux时区与Java进程

    Linux下时区判断可以用date看到,那date是依赖什么来判断时区的呢?答:TZ环境变量,这是POSIX标准指定的debugging-java-program-for-changing-timezone-configuration-file-on-ubuntu当没TZ环境变量时,则GNU系统库会使用文件 /etc/localtime(它其实是一个链接文件,指向/usr/share/zoneinfo中的一个文件)
    所有的时区信息定义在/usr/share/zoneinfo ,可以进行测试 TZ='Asia/Shanghai' date
    一些系统上可能需要 TZ=':Asia/Shanghai' date
    how-can-i-have-date-output-the-time-from-a-different-timezone
    Ubuntu与Debian系定义了个更友好的文件:/etc/timezone
    Java也更愿意使用文件/etc/timezone ,所以一般对单个应用程序,使用TZ环境变量即可
    改变系统时区sudo timedatectl set-timezone UTC

    ConnectorJ 时区

    ConnectJ主要有两个:5.1和8.0
    在这里插入图片描述

    8.0有篇文章描述了时间:Preserving Time Instants 保留时间片刻

    TIMESTAMP is the only MySQL data type designed to store instants. To preserve time instants, the server applies time zone conversions in incoming or outgoing time values when needed. Incoming values are converted by server from the connection session’s time zone to Coordinated Universal Time (UTC) for storage, and outgoing values are converted from UTC to the session time zone. Starting from MySQL 8.0.19, you can also specify a time zone offset when storing TIMESTAMP values (see The DATE, DATETIME, and TIMESTAMP Types for details), in which case the TIMESTAMP values are converted to the UTC from the specified offset instead of the session time zone. But, once stored, the original offset information is no longer preserved.

    TIMESTAMP是唯一的MySQL用来存储的时刻的数据类型。为了存储时刻,在需要的时候,MySQL server在存储、读取时间时进行了时区转换。存储时,服务器把进来的数据从session的时区转换到了UTC,取出的时候,则将UTC时间转化成session的时区。从MySQL 8.0.19版本开始,你可以在存储TIMESTAMP值时指定一个时区,这时进行时区转换时就使用了指定的时区而不是session的时区。但是一旦数据被存储,就不会再保存原始的时区信息。
    StackOverFlow sql.Date和util.Date
    MySQL文档中又有一句:

    Therefore, do not pass instant date-time types (java.util.Calendar, java.util.Date, java.time.OffsetDateTime, java.sql.Timestamp) to non-instant date-time types (for example, java.sql.DATE, java.time.LocalDate, java.time.LocalTime, java.time.OffsetTime) or vice versa, when working with the server.

    啊,因此不要把即时时间传给非即时时间;啥叫即时时间啊?
    似乎这个东西应该不叫即时时间,应该叫时刻。同一时刻,是和时区不相关的,比如不管美国现在几点,都是和我在同一时刻。而non-instant time就不行,它依赖时区,比如现在2021-12-9 18:00:00 不同时区的此LocalDateTime是不同的时刻。
    引用一个Stack Overflow回答whats-the-difference-between-instant-and-localdatetime;推荐这篇回答

    • Instant represents a moment, a specific point in the timeline.
      Instant是一个时刻,是时间线上的一个点
    • LocalDateTime represents a date and a time-of-day. But lacking a time zone or offset-from-UTC, this class cannot represent a moment. It represents potential moments along a range of about 26 to 27 hours, the range of all time zones around the globe. A LocalDateTime value is inherently ambiguous.
      LocalDateTime表示一天、和一天的时间,缺少时区或者是UTC偏移,这个类不能代表一个时刻

    So Local… means “not zoned, no offset”. Local表示没有时区,没有偏移。

    弄懂了instant概念,再回到connectJ,讲述当时区不同时的处理:

    可以看大MySQL本身就能很好得处理时区转换,根本不需要模块去操心。

    MySQL实验

    在这里插入图片描述

    看到了吗,TIMESTAMP随着时区自己在变,而DATETIME永远不会变,就像生日一样。

    总结

    python模块丢失了时区信息;因为数据库与模块B时区配置相同,没有暴露出datetime问题

    写入数据过程:测试模块向数据库datetime字段写入字符串时间
    读取数据过程:MySQL datetime --> java.util.date(东八区时间) --> 转换成时间戳,东八区-8h --> 时间戳转换成字符串格式丢失时区

    数据库设计时,分清到底是需要instant还是non-instant,instant就MySQL使用TIMSTAMP类型,否则DateTime,
    编写代码时,datetime本身是没有时区信息的,所以在模块中处理时也不应当涉及时区,不应该与java.util.Date匹配使用,而应该是LocalDateTime。
    而且如今已经Java8年代,要与时俱进:https://stackoverflow.com/a/61198758
    datetime: java.time.LocaleDateTime 曾经:java.sql.Date
    timestamp: java.time.Instant 曾经:java.util.Date
    可见我们在瞎使用。

  • 相关阅读:
    Spring的三种注入方式(为什么推荐构造器注入?)
    Unity3D学习笔记4——创建Mesh高级接口
    MySQL简单命令总结
    登录怎么实现的,密码加密了嘛?使用明文还是暗文,知道怎么加密嘛?
    Heartbleed Vulnerability-心脏滴血漏洞
    MySQL ——单行处理函数实例练习
    计算机毕业设计Javaweb唐院寻人表白系统(源码+系统+mysql数据库+lw文档)
    同态加密+区块链,在大健康数据隐私保护中的应用
    网络是怎样连接的--TCP大致控制流程
    在Eclipse 中使用 Maven 创建雅加达 EE 应用程序
  • 原文地址:https://blog.csdn.net/KHZ_222/article/details/126793309