[mysql] 混乱的时区

2022-11-17 10:06:48 +08:00
 GopherDaily

[mysql] 混乱的时区

Link: https://github.com/j2gg0s/j2gg0s/blob/main/20221116_mysql_%E6%B7%B7%E4%B9%B1%E7%9A%84%E6%97%B6%E5%8C%BA.md

MySQL 的时间类型, 无论是 TIMESTAMP 还是 DATETIME, 都是不带时区信息.

Binary Protocol 中对应的数据类型 MYSQL_TYPE_DATETIMEMYSQL_TYPE_TIMESTAMP 都没有任何字段来传递时区信息. go-sql-driver/mysql 中解析时间类型的 parseBinaryDateTimeparseDateTime 也没有从 server 返回的内容中解析出时间类型.

MySQL 将时区附加在链接上, 每个链接都有对应的时区, 在没有明确指定的情况下默认是 server 的时区.

链接的时区对 TIMESTAMP 和 DATETIME 的影响却又大不相同. 当插入或更新 TIMESTAMP 时, MySQL 会将更新的内容从链接的时区转换到 UTC 再存储. 当查询 TIMESTAMP 时, MySQL 会将读取的内容从 UTC 转换到链接的时区再展示. 而 DATETIME 的插入, 更新和查询却完全不受链接时区的影响.

go-sql-driver/mysql 类似的 driver 或者 orm, 一定程度上不区分 TIMESTAMP 和 DATETIME 又进一步加剧了混乱. 当然这不是 go-sql-driver/mysql 的问题, go-sql-driver/mysql 的实现质量还是非常又保证的.

上述的总总, 在写入和读取的代码不是同一套时, 格外的明显. examples/mysql-stamp 中构造了几个典型的例子验证理解.

mysql> SELECT * FROM visitor;
+----+--------+---------------------+---------------------+---------------------+
| id | name   | visited_timestamp   | visited_datetime    | created_at          |
+----+--------+---------------------+---------------------+---------------------+
|  1 | j2gg0s | 2022-11-16 22:17:00 | 2022-11-16 22:17:00 | 2022-11-16 14:18:29 |
+----+--------+---------------------+---------------------+---------------------+
1 row in set (0.01 sec)

mysql> SET @@session.time_zone='+08:00';
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM visitor;
+----+--------+---------------------+---------------------+---------------------+
| id | name   | visited_timestamp   | visited_datetime    | created_at          |
+----+--------+---------------------+---------------------+---------------------+
|  1 | j2gg0s | 2022-11-17 06:17:00 | 2022-11-16 22:17:00 | 2022-11-16 22:18:29 |
+----+--------+---------------------+---------------------+---------------------+
1 row in set (0.00 sec)
func ExampleStamp() {
	for i, dsn := range []string{
		"root:root@tcp(127.0.0.1:3306)/j2gg0s?parseTime=true",
		"root:root@tcp(127.0.0.1:3306)/j2gg0s?parseTime=true&loc=Asia%2FShanghai",
		"root:root@tcp(127.0.0.1:3306)/j2gg0s?parseTime=true",
	} {
		db, err := sql.Open("mysql", dsn)
		if err != nil {
			panic(err)
		}
		if i == 2 {
			db.Exec("SET @@session.time_zone='+08:00'")
		}

		rows, err := db.Query("SELECT * FROM visitor")
		if err != nil {
			panic(err)
		}
		for rows.Next() {
			var id int64
			var name string
			var visitedTimeStamp, visitedDateTime time.Time
			var createdAt time.Time
			err := rows.Scan(&id, &name, &visitedTimeStamp, &visitedDateTime, &createdAt)
			if err != nil {
				panic(err)
			}
			fmt.Println(id, name, visitedTimeStamp.Format(time.RFC3339), visitedDateTime.Format(time.RFC3339))
		}
		db.Close()
	}
	// Output:
	// 1 j2gg0s 2022-11-16T22:17:00Z 2022-11-16T22:17:00Z
	// 1 j2gg0s 2022-11-16T22:17:00+08:00 2022-11-16T22:17:00+08:00
	// 1 j2gg0s 2022-11-17T06:17:00Z 2022-11-16T22:17:00Z
}
3192 次点击
所在节点    MySQL
26 条回复
simonCN
2022-11-17 10:23:42 +08:00
TIMESTAMP 还能有时区信息?这个值不就是标准的是秒 /毫秒值么
awanabe
2022-11-17 10:26:14 +08:00
跨区就用 timestamp 展现的时候根据服务器的时区展现就行了
thinkershare
2022-11-17 10:31:48 +08:00
没啥好办法,单独用一个独立字段存储 timestamp 的实际时区好了。
maggch97
2022-11-17 10:34:41 +08:00
UTC 时间和 Unix timestamp 都不会有你这个问题,时区是展现时候才需要的,并且都是从 client 取得。先弄懂这几个概念再说吧
GopherDaily
2022-11-17 10:51:22 +08:00
@maggch97 mysql 支持在查询中传递 Unix Timestamp 吗?
hsfzxjy
2022-11-17 11:07:32 +08:00
入库一律转成 UTC 时间
julyclyde
2022-11-17 11:11:47 +08:00
timestamp 依法 UTC 啊
springz
2022-11-17 11:12:36 +08:00
时区统一存 Unix Timestamp ,或者 UTC 时间。前端去处理客户端时区。
springz
2022-11-17 11:13:53 +08:00
千万别自己发明,还会有其他国家夏令时等等一堆奇奇怪怪的问题。
timethinker
2022-11-17 11:13:59 +08:00
简单的理解一下:

时间:某一个时间点绝对值
时区:对时间点的修饰偏移

显然数据库存储的值不会根据你的时区是什么而发生变化,所以只能是在读取的时候根据当前已确定的时区进行不同的展示。
springz
2022-11-17 11:16:11 +08:00
时间这个问题是一个现实世界随时调整的一个变量,本质上是和计算机这个系统不兼容的,server 最好就是 Unix Timestamp 按需转现实时间,要不就是 UTC 。
masterclock
2022-11-17 11:22:12 +08:00
https://github.com/kdeldycke/awesome-falsehood#dates-and-time
时间处理非常复杂,基本方法是:
1. 通读上面的所以条目,去除脑子里的错误假设
2. 统一使用 UTC 直到最后给人看的时候
3. 用户输入的时间未必是某个特定时间
CRVV
2022-11-17 12:38:57 +08:00
如果已经注意到这种问题了,答案就是别用 MySQL ,这玩意遍地是坑。


@simonCN
@maggch97
op 说的问题不是怎么存时间,怎么用 timestamp.
他说的是 MySQL 的 timestamp ,不是通常说的 unix timestamp
(类似的还有 MySQL 的 utf8 也不是 UTF-8)

MySQL timestamp 的坑至少包含了:
1. 范围是 1970-2038 年
2. TIMESTAMP 直接把输入用本机的时区来理解(如果有冬夏令时,就不可能正常工作),MySQL 8.0.19 才能自己指定时区。
3. 默认自动更新,DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

这个类型根本就没考虑拿来存一个正常的时间,它的设计就是用来让你 ON UPDATE CURRENT_TIMESTAMP 的

DATETIME 的坑可能更多,一样很难把时区搞定。换个 SQLite 都没这么多坑
pengtdyd
2022-11-17 12:49:34 +08:00
DATETIME 没有时区
TIMESTAMP 有 2038 问题

我个人觉的最好的解决方案是存 Bigint(20) UTC+0
GopherDaily
2022-11-17 13:22:16 +08:00
@CRVV MySQL 的 utf-8 是 utf8mb3 吧
lisongeee
2022-11-17 13:48:46 +08:00
时间戳为啥不直接存 long 值?
CRVV
2022-11-17 13:52:43 +08:00
@GopherDaily

utf8mb3 是 MySQL 自己造出来的词,utf8mb4 也是
UTF-8 这个东西,本来是最多 6 bytes 的变长编码,根本没有什么 mb3 mb4
后面觉得 6 bytes 没用,4 bytes 就足够了,就把标准改成了最长 4 bytes

https://en.wikipedia.org/wiki/UTF-8#History
janxin
2022-11-17 14:08:58 +08:00
你不在这里出问题,其他地方也肯定会出问题的。如果涉及到时区问题,最好的方案就是统一 UTC ,展示转换时区即可。
GopherDaily
2022-11-17 14:11:15 +08:00
@CRVV

我值,MySQL 中的 utf-8 其实是 utf8mb3 ;
实际的 utf-8 对应的是 utf8mb4;
在加上 connector/j 5.1.46 之前的 characterEncoding 不生效

这几个名词能把刚写代码的人玩哭
unco020511
2022-11-17 15:11:54 +08:00
存 timestamp,展示的时候转为当地时间

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/895852

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX