跳到主要内容

关于时间数据类型的时区问题

对于使用 timestamp 的场景,MySQL 在访问 timestamp 字段时会做时区转换,当 time_zone 设置为 system 时,MySQL 访问每一行的 timestamp 字段时,都会通过 libc 的时区函数,获取 Linux 设置的时区,在这个函数中会持有mutex,当大量并发SQL需要访问 timestamp 字段时,会出现 mutex 竞争。

MySQL 访问每一行都会做这个时区转换,转换完后释放mutex,所有等待这个 mutex 的线程全部唤醒,结果又会只有一个线程会成功持有 mutex,其余又会再次sleep,这样就会导致 context switch 非常高但 qps 很低,系统吞吐量急剧下降。

查看当前数据库时区

mysql> SHOW VARIABLES LIKE "%time_zone%";
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| system_time_zone | CST |
| time_zone | SYSTEM |
+------------------+--------+

查看表结构

mysql> DESC timezone_test;
+-----------------+-----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+-----------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| datetime_field | datetime | YES | | NULL | |
| timestamp_field | timestamp | YES | | NULL | |
| int_field | int(10) | YES | | NULL | |
+-----------------+-----------+------+-----+---------+----------------+

插入数据

mysql> INSERT INTO timezone_test(datetime_field, timestamp_field, int_field) VALUES('2021-01-01 08:00:00', '2021-01-01 08:00:00', 1609459200);

mysql> SELECT * FROM timezone_test;
+----+---------------------+---------------------+------------+
| id | datetime_field | timestamp_field | int_field |
+----+---------------------+---------------------+------------+
| 1 | 2021-01-01 08:00:00 | 2021-01-01 08:00:00 | 1609459200 |
+----+---------------------+---------------------+------------+

这个时间看起来是没问题的, 那么我们尝试修改时区再插入数据

# 修改时区
mysql> SET time_zone = "+00:00";

mysql> INSERT INTO timezone_test(datetime_field, timestamp_field, int_field) VALUES('2021-01-01 08:00:00', '2021-01-01 08:00:00', 1609459200);
Query OK, 1 row affected (0.01 sec)

# 再恢复到东8区
mysql> SET time_zone = "+08:00";

# 这时候再查看数据, 两条插入的SQL是一样的,但是发现查询的结果是不一样的,这两条数据相差正好是时区的时间差
mysql> SELECT * FROM timezone_test;
+----+---------------------+---------------------+------------+
| id | datetime_field | timestamp_field | int_field |
+----+---------------------+---------------------+------------+
| 1 | 2021-01-01 08:00:00 | 2021-01-01 08:00:00 | 1609459200 |
| 2 | 2021-01-01 08:00:00 | 2021-01-01 16:00:00 | 1609459200 |
+----+---------------------+---------------------+------------+

再看一下实际存储的时间戳, 然后我们变化时区

# 当前东8区时,查看时间戳
mysql> SELECT *,UNIX_TIMESTAMP(datetime_field),UNIX_TIMESTAMP(timestamp_field) FROM timezone_test;
+----+---------------------+---------------------+------------+--------------------------------+---------------------------------+
| id | datetime_field | timestamp_field | int_field | UNIX_TIMESTAMP(datetime_field) | UNIX_TIMESTAMP(timestamp_field) |
+----+---------------------+---------------------+------------+--------------------------------+---------------------------------+
| 1 | 2021-01-01 08:00:00 | 2021-01-01 08:00:00 | 1609459200 | 1609459200 | 1609459200 |
| 2 | 2021-01-01 08:00:00 | 2021-01-01 16:00:00 | 1609459200 | 1609459200 | 1609488000 |
+----+---------------------+---------------------+------------+--------------------------------+---------------------------------+

# 修改时区,再次查看时间戳
mysql> SET time_zone = "+00:00";

## 对比上面
# 发现`datetime_field `字段原始数据没变,但调用`UNIX_TIMESTAMP()`转为时间戳时发生了变化。
# 发现`timestamp_field `字段原始数据发生了变化, 但是它的时间戳没变。
mysql> SELECT *,UNIX_TIMESTAMP(datetime_field),UNIX_TIMESTAMP(timestamp_field) FROM timezone_test;
+----+---------------------+---------------------+------------+--------------------------------+---------------------------------+
| id | datetime_field | timestamp_field | int_field | UNIX_TIMESTAMP(datetime_field) | UNIX_TIMESTAMP(timestamp_field) |
+----+---------------------+---------------------+------------+--------------------------------+---------------------------------+
| 1 | 2021-01-01 08:00:00 | 2021-01-01 00:00:00 | 1609459200 | 1609488000 | 1609459200 |
| 2 | 2021-01-01 08:00:00 | 2021-01-01 08:00:00 | 1609459200 | 1609488000 | 1609488000 |
+----+---------------------+---------------------+------------+--------------------------------+---------------------------------+

因为这一切都是 MySQL 隐式的帮我们转换了,就是数据库实际上会保存 UTC 时间戳,写入的时候先按 Session 时区转成 UTC 时间,读出的时候再按 Session 时区转成当前时区的时间,这些转换都是透明的。

假如: 我们在东八区存储了 2020-12-09 08:00:00 时间的一条数据。我们在东八区取出这一条数据, 时间依然是 2020-12-09 08:00:00。我们在零时区的服务器上,把当前的时区设置为+00:00,再去查数据库这条记录,查到的数据是 2020-12-09 00:00:00, 正好对应零时区的时间。

参考文献