この記事では、mysql のタイムスタンプの時刻型について説明します。mysql には、timestamp と datetime の 2 つの時刻型があります。お役に立てば幸いです。
ご存知のとおり、mysql には timestamp と datetime という 2 つの時刻型がありますが、timestamp と datetime の違いをオンラインで検索すると、次のことがわかります。インターネット上にはタイムゾーンに関する情報がたくさんありますが、主に 2 種類のまったく逆の結論があります:
timestamp にはタイムゾーンの問題はありませんが、datetime にはタイムゾーンの問題があります。タイムスタンプは UTC 形式で保存されますが、日時ストレージは時刻文字列に似ています。形式
タイムスタンプにもタイム ゾーンの問題があります。 #この 2 つのビューは混乱を招くため、タイムスタンプにはタイムゾーンの問題があるのでしょうか?
基本概念
タイムゾーン:
地理的な制約により、人々は時間を発明しました。ゾーン この概念は、人々の時間認識の違いに適応するために使用されます。たとえば、中国のタイムゾーンは東 8 ゾーンであり、8:00 または GMT 8 で表されますが、日本のタイムゾーンは東 9 ゾーンで、9:00 で表されます。または GMT 9 、中国で午前 8 時であるとき、日本では午前 9 時、つまり東 8 区では 8 時、東 9 区では 9 時です。これら 2 つの時間は等しいです。 さらに、時間には 2 つの概念があります:
絶対時間:
たとえば、UNIX 時間の接尾辞は 1970-01-01 00:00 です。 :00 から現在までの秒数 (例: 1582416000)。この表現は絶対時間であり、タイム ゾーンの影響を受けません。エポックとも呼ばれます。
現地時間:
特定のタイムゾーンに対する相対的な時間は現地時間です。たとえば、東 8 区の 2020-02-23 08:00:00 は中国の現地時間です。このとき、日本の現地時間は 2020-02-23 09:00:00 なので、現地時間は特定のタイムゾーンに関連付けられています。タイムゾーンなしで現地時間を見るのは意味がありません。これが具体的にどの時点を指すのかわかりません。
たとえば、Java では、Date オブジェクトは絶対時刻です。SimpleDateFormat でフォーマットされた yyyy-MM-dd HH:mm:ss 形式の時刻文字列が現地時間です。SimpleDateFormat が呼び出さない場合は、 setTimeZone() 指定されたタイム ゾーンが表示される場合、デフォルトのタイム ゾーンは、jvm が実行されているオペレーティング システムです。開発マシンのタイム ゾーンは、基本的に GMT 8 です。
timestamp と datetime の違い以下のように、time_stamp がタイムスタンプ型、date_time が日付時刻型、create_timestamp と create_datetime がタイムスタンプであるテーブルを作成しました。および datetime 型ですが、データベースによって自動的に生成できます。 CREATE TABLE `time_test` (
`id` bigint unsigned,
`time_stamp` timestamp,
`date_time` datetime,
`create_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_datetime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
)
1. まず、データベースのタイム ゾーンを 8:00 (中国東部 8 区) に設定します。
4. 上記のように、タイムスタンプ タイプとして定義された列 time_stamp と create_timestamp は手動で挿入されますまたは、now() 関数を使用します。地区 9 の時刻は、地区 8 東部の時刻より 1 時間大きいです。これは正しく、タイムスタンプのタイプがタイムゾーンに関連していることを示しています。ただし、date_time フィールドと create_datetime フィールドは datetime タイプとして定義されています時間に変化はありません。これは、datetime 型がタイムゾーンに依存しないことを意味します。
結論: timestamp にはストレージ内のタイム ゾーンが含まれていますが、datetime にはタイム ゾーンが含まれていません。これは、インターネット上の最初のステートメントが正しいことを示しています。別の例を見てください
東 8 区の 2020-02-23 08:00:00 を Unix 時間サフィックス (絶対時間) に変換して、試してみます。データベースに挿入しますか?
次のように、linux date コマンドを使用して、時刻文字列を Unix 時刻プレフィックスに変換します。$ "date" --date="2020-02-23 08:00:00 +08:00" +%s 1582416000次に、mysql from_unixtime() 関数を使用して、unix 時刻プレフィックスを mysql 時刻に変換します。データを挿入するタイプ。
#上記のように、クエリされた時刻も東 9 区の 9 時であり、時刻も正しいです。
タイムスタンプ タイプにはタイム ゾーンの問題があるとオンラインで言われているのはなぜですか?インターネットでタイムスタンプにタイムゾーンの問題があることがわかりました。アプリケーションはデータを挿入し、データベースにアクセスしてそれを確認します。時間が異なることが判明したため、 Java でデモを作成する予定です。試してみて、問題が再現できるかどうかを確認してください。
1. まず、上記の time_test テーブルに対応する Java のエンティティの定義は次のとおりです。ここでの時間属性はすべて、次のように Date 型で定義されていることに注意してください:
2. 次に、次のように、データを挿入およびクエリするための 2 つのインターフェイス /insert および /queryAll を作成しました。
3、然后我把数据库的时区设置为+09:00时区,即日本的东9区,如下:
4、然后调用/insert接口插入数据,注意我接口传入的时间是东8区的8点,如下:
5、插入完后,去数据库中查询一把,如下:
可以看到,time_stamp字段时间是9点,且我已将数据库时区设置为东9区,东9区的9点与东8区的8点,这两个时间实际是相等的,因此时间数据没错。
6、然后我使用/queryAll接口将数据查询出来,如下:
timeStamp属性是1582416000000,这是毫秒级的时间缀,秒级则是1582416000,对应是东8区的2020-02-23 08:00:00,时间数据也没错!
7、然后我又将mysql时区修改回+8:00,并重启我们的java应用,如下:
8、再查询一下数据,如下:
timeStamp属性还是1582416000000,时间没有变化,这也是正确的。
那为什么网上会说timestamp存在时区问题?
经过一翻查看,我发现他们都提到了jdbc的serverTimezone,会不会是这个配置错误导致的呢?就先试试吧!
1、如图,我把数据库时区修改回+9:00时区,然后故意把jdbc的url上的serverTimezone配置为与数据库不一致的GMT+8时区,然后重启java应用,如下:
url: jdbc:mysql://localhost:3306/testdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8
其中GMT%2B8就是GMT+8,因为在url上需要urlencode,所以就变成了GMT%2B8。
2、重新插入数据,注意插入的时间还是东8区的8点,如下:
3、然后,我再到数据库中查询一把,如下:
time_stamp中时间竟然是8点!要知道我们虽然插入的是东8区的8点,但当前会话可是东9区的,东8区的8点等于东9区的9点,所以正确显示应该为9点才对,时间差了1小时!
4、然后,我又调用/queryAll接口查询了一把,想看看mybatis查询出来的时间数据对不对,如下:
可以看到timeStamp是1582416000000,秒级是1582416000,这个时间就是东8区的8点,东9区的9点啊!查询出来的时间竟然是正确的,为什么???
serverTimezone的本质
为了找出问题所在,我调试了一下mysql的jdbc驱动代码,终于弄明白了原因,主要可以看看如下这几点:
1.mysql驱动创建连接后,会调用com.mysql.jdbc.ConnectionImpl#configureTimezone()来配置此连接的时区,如果配置了serverTimezone,则会使用serverTimezone配置的时区,没配置时会去取数据库中的time_zone变量,这就是为什么我们没有配置serverTimezone变量时,结果也是正确的。
//若使用普通驱动,使用此方法配置mysql连接的时区 com.mysql.jdbc.ConnectionImpl#configureTimezone() //若使用cj驱动,使用此方法配置mysql连接的时区 com.mysql.cj.protocol.a.NativeProtocol#configureTimezone()
2.调用jdbc的setTimestamp()方法时,实际调用的是com.mysql.cj.jdbc.ClientPreparedStatement#setTimestamp(),这里面会根据serverTimezone指定的时区,将对应的Timestamp对象转换为serverTimezone指定时区的本地时间字符串。
3.执行sql语句时,会执行com.mysql.cj.jdbc.ClientPreparedStatement#execute(),这里面sendPacket变量保存着真实会发送到mysql的sql语句。
注:看的是8.0.11版本mysql-connector-java驱动源码,不同版本代码会稍有差异,比如5.2.16版本驱动,jdbc url上需要同时配置这两个配置:useTimezone=true&serverTimezone=GMT%2B8,且setTimestamp()对应的是com.mysql.jdbc.PreparedStatement#setTimestampInternal方法。
原理总结如下:
mysql驱动在发送sql前,会将jdbc中的Date对象参数,根据serverTimeZone配置的时区转化为日期字符串后,再发送sql请求给mysql server,同样在mysql server返回查询结果后,结果中的日期值也是日期字符串,mysql驱动会根据serverTimeZone配置的时区,将日期字符串转化为Date对象。
因此,当serverTimeZone与数据库实际时区不一致时,会发生时区转换错误,导致时间偏差,如下:
a、比如sql参数是一个Date对象,时间值是东8区的2020-02-23 08:00:00,注意它里面存储的可不是2020-02-23 08:00:00这个字符串,它是Date对象(绝对时间),只是我用文字表达出来是东8区的2020-02-23 08:00:00。
b、然后,由于serverTimeZone配置的是东8区,mysql驱动会将这个Date对象转为2020-02-23 08:00:00,注意这时已经是字符串了,然后再将sql发送给mysql,注意这里的sql里面已经将Date参数替换为2020-02-23 08:00:00了,因为Date对象本身是无法走网络的。
c、然后mysql数据库接收到这个时间字符串2020-02-23 08:00:00后,由于数据库时区配置是东9区,它会认为这个时间是东9区的,它会以东9区解析这个时间字符串,这时数据库保存的时间是东9区的2020-02-23 08:00:00,也就是东8区的2020-02-23 07:00:00,保存的时间就偏差了1个小时。
d、查询结果里时间为什么又对了呢,因为查询结果返回了东9区的时间字符串,而java应用又将其理解为是东8区的时间,负负得正了!
将serverTimezone与mysql时区保持一致
so,那么如果我们将serverTimezone配置改正确,即与数据库保持一致时,应该查询到的时间就会是错的,会少1个小时。
1、jdbc url中使用与数据库一样的东9区GMT+9,如下:
url: jdbc:mysql://localhost:3306/testdb?serverTimezone=GMT%2B9&useUnicode=true&characterEncoding=utf8
其中的GMT%2B9,即是GMT+9。
2、然后重启Java应用,再查询一把看看,如下:
返回的是毫秒级时间缀1582412400000,秒级就是1582412400,使用linux的date命令转换为时间字符串形式:
$ "date" --date="@1582412400" +"%F %T %z" 2020-02-23 07:00:00 +0800
看到没,它是东8区的7点,刚好差了1个小时。
3、所以,使用mysql的timestamp类型时,对于java应用来说,一定要保证jdbc url中的serverTimezone与数据库中的时区配置是一致的。
另外一点是,当没有配置serverTimezone时,mysql驱动会自动读取mysql server中配置的时区,这里面也有坑!如下:
mysql驱动自动读取数据库时区的坑
3.1 mysql安装好后,默认时区是SYSTEM,而SYSTEM指的是system_time_zone变量的时区,如下:
3.2 当mysql驱动读到time_zone变量是SYSTEM时,会再去读取system_time_zone变量,而system_time_zone对于国内来说,默认是CST,这是一个混乱的时区,是4个不同时区的缩写,如下:
对于Linux或MySQL,会认为CST是中国标准时间(+8:00),但Java却认为CST是美国标准时间(-6:00)(注:可能和Java运行在Windows中有关):
如下,linux中CST等于+0800,即中国时区:
$ "date" +"%F %T %Z %z" 2021-09-12 18:35:49 CST +0800
如下,java中CST等于-06:00,美国时区:
3.3 因此mysql驱动取到CST这个时区值时,它会以为这是-6:00时区,但MySQL却理解为+8:00时区,因此MySQL时区一定不要配置为CST,而要配置为具体的时区,如+8:00,但如果MySQL时区为CST且不可修改的情况下,一定要配置jdbc的serverTimezone为清晰的时区(如:GMT+8)。
Entity中日期属性是String呢?
1、我们将Entity对象中的时间属性改为String(不推荐),如下:
2、然后也写两个接口,/insert2与/queryAll2,如下:
3、然后插入数据,注意这时我是直接将无时区的8点,作为参数给到sql的,如下:
4、然后再查询一把,如下:
如上所示,time_stamp字段值是8点,但此时数据库时区是东9区,所以这是东9区的8点。
5、然后我将数据库与jdbc中serverTimezone都改为东8区呢,改完后重启Java应用,如下:
url: jdbc:mysql://localhost:3306/testdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8
6、再次插入数据,参数还是无时区的8点,如下:
7、再查询一把,如下:
如上所示,time_stamp字段值是8点,但现在数据库时间是东8区,所以这是东8区的8点。
8、然后我再将jdbc url上的serverTimezone调整为东9区,然后重启Java应用,如下:
url: jdbc:mysql://localhost:3306/testdb?serverTimezone=GMT%2B9&useUnicode=true&characterEncoding=utf8
现在serverTimezone与数据库中不一致,数据库是东8区,serverTimezone是东9区。
9、我们再次插入无时区的8点,如下:
10、然后再查询一把,如下:
time_stamp字段值还是8点,数据库是东8区,所以这是东8区的8点,但我们serverTimezone与数据库的时区不一致啊,没看到时间有偏差,为什么?
解释一下
前面说过了,对于jdbc中的Date对象,在发送给mysql前,会先根据serverTimezone转换为相应时区的时间字符串,但现在Entity中时间属性是String类型,mysql驱动不会进行转换,所以不管serverTimezone怎么配置,对String类型的时间串都没影响。
这样的话,似乎java中日期类型用时间字符串来存还好些,不容易出错,但请再认真考虑一下,调用方传了一个无时区的8点,数据库自作主张,就将其认为是东9区的8点,但如果这个时间字符串实际是东8区的8点呢?这时如果保存到数据库中为东9区的8点,那数据就存错了!
那如果目前api接口就传的无时区的时间串,Entity中就定义的String,怎么解决呢?
1、询问接口定义人员,这个接口的时间串指的是哪个时区的,比如是东8区的2020-02-23 08:00:00。
2、然后接口接收到时间后,要以东8区将时间字符串转换为Date对象,如下:
SimpleDateFormat sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss'); sdf.setTimeZone(TimeZone.getTimeZone("GMT+8")); Date date = sdf.parse("2020-02-23 08:00:00");
3、然后如果Entity中时间属性定义的是String,那么我们要再将Date对象以数据库的时区格式化为对应的时间字符串,比如数据库时区是东9区,那么格式化后就是2020-02-23 09:00:00,如下:
SimpleDateFormat sdf = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss'); sdf.setTimeZone(TimeZone.getTimeZone("GMT+9")); String dateStr = sdf.format(date); entity.setTimeStamp(dateStr);
4、然后将Entity保存到mysql中的,就也会是东9区的2020-02-23 09:00:00,结果正确。
所以,使用String类型来存储时间数据,要想将时间值保存正确,超级麻烦,不建议在实际开发中这种使用。
最佳实践
1、大多数团队会规定api中传递时间要用unix时间缀,因为如果你传一个2020-02-23 08:00:00时间值,它到底是哪个时区的8点呢?对于unix时间缀,就不会有此问题,因为它是绝对时间。而如果某些特殊原因,一定要使用时间字符串,最好使用ISO8601规范那种带时区的时间串,比如:2020-02-23T08:00:00+08:00。
2、Mybatis中Entity定义要与数据库定义一致,数据库中是timestamp,那么Entity中要定义为Date对象,因为mysql驱动在执行sql时,会自动根据serverTimezone配置帮你转换为数据库时区的时间串,如果你自己来转换,你极有可能因为忘记调用setTimeZone()方法,而使用当前java应用所在机器的默认时区,一旦java应用所在机器的时区与数据库的时区不一致,就会出现时区问题。
3、jdbc的serverTimezone参数,要配置正确,当不配置时,mysql驱动会自动读取mysql server的时区,此时一定要将mysql server的时区指定为清晰的时区(如:+08:00),切勿使用CST。
4、如果数据库时区修改后,jdbc的serverTimezone也要跟着修改,并重启Java应用,就算没有配置serverTimezone,也需要重启,因为mysql驱动初始化连接时,会将当前数据库时区缓存到一个java变量中,不重启Java应用它不会变。
数据库中用timestamp还是int来存储时间?
如果用int型时间缀存储,不管数据库时区是啥,都不影响,因为存储的是绝对时间,看起来完美解决了时区问题。
しかし、いくつかの観点から見ると、この解決策はタイム ゾーンの問題をデータベース側からアプリケーション側にプッシュするだけです。タイム ゾーンの問題は、時刻文字列を時刻プレフィックスに変換するプロセスで発生します。たとえば、プログラマは次のように変換します。時刻文字列を時刻プレフィックスに変換します。インターフェイスで時刻文字列を取得した後、タイム ゾーンを考慮せずにそれを UNIX 時刻サフィックスに直接変換するため、タイム ゾーンの問題が発生する可能性があります。
したがって、タイム ゾーンを使用せずに時刻文字列を解析する場合は、必ずこれがどのタイム ゾーンであるかを確認し、それをコード内で明示的に指定してください。
さらに、時刻の保存に int を使用することには 3 つの欠点があります:
開発者はこのフィールドを見た後、時刻接尾辞を明確に理解できません。変換する必要がありますが、非常に面倒です。
update_time のようなフィールドの場合、データベースは DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP メカニズムを提供するため、フィールドが更新されると、update_time が自動的に更新されます。また、int ストレージが使用されている場合は、プログラムが必要です メンバーがテーブルを更新するたびに、このフィールドをリセットする必要がありますが、これは忘れがちです。
int には 4 バイトしかないため、時刻の保存に使用すると 2038 年以降オーバーフローします。タイムスタンプの場合、MySQL は基になるストレージを一律に 8 ワードに変更します。フェスティバルは比較的簡単です。
もちろん、int を使用しないことをお勧めするわけではありませんが、これは意見の問題であり、timestamp を使用しても int を使用しても致命的な問題はありません。
概要
timestamp 自体にはタイム ゾーンの問題はありません。タイム ゾーンの問題は、serverTimezone の設定が正しくなく、CST を使用する mysql などのタイム ゾーンの混乱が原因です。または、文字列型としてのエンティティ内の日付定義が原因です。
推奨学習: mysql ビデオ チュートリアル
以上がmysql のタイムスタンプにおけるタイムゾーンの問題について話しましょう。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。