Демо: как ломается время при использовании TIMESTAMP в MySQL

Происходящее ниже — иллюстрация к этой статье.

Лабораторная работа

Нам понадобится Docker (по моему мнению самый простой способ провести эксперимент с системным часовым поясом без влияния на реальный часовой пояс используемой системы) и bash. Если с ними проблемы, то результат (полученный, правда немного другим путем: менялся не системный часовой пояс, а session.time_zone), можно увидеть на SQL Fiddle.

Запустим MySQL-сервер (и подождем, пока он запустится):

docker run --name time_test -e MYSQL_ROOT_PASSWORD=helloworld -d mysql:5.7 && \
docker exec -i -e MYSQL_PWD=helloworld time_test bash <<< 'while ! mysql -u root -e exit 2>/dev/null; do sleep 1; echo -n .; done; echo " MySQL seems to be ready"'

Убедимся, что на сервере UTC:

docker exec -it time_test date; \
docker exec -it time_test cat /etc/timezone; \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root <<< "SELECT NOW();"; \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root <<< "SHOW VARIABLES LIKE 'system_time_zone';"
Sat Sep 30 16:51:27 UTC 2017
Etc/UTC
NOW()
2017-09-30 16:51:27
Variable_name	Value
system_time_zone	UTC

Создадим таблицу следующей структуры:

+-------+------------------+------+-----+-------------------+-----------------------------+
| Field | Type             | Null | Key | Default           | Extra                       |
+-------+------------------+------+-----+-------------------+-----------------------------+
| id    | int(10) unsigned | NO   |     | NULL              |                             |
| ts    | timestamp        | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------+------------------+------+-----+-------------------+-----------------------------+
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root <<< "
    CREATE DATABASE test;
    USE test;
    CREATE TABLE timestamps (id int NOT NULL, ts timestamp NOT NULL);
"

Сделаем вставку с текущим временем:

docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "INSERT INTO timestamps VALUES (1, '$(date +%Y-%m-%d\ %H:%M:%S)');"

Подстановку $(date +%Y-%m-%d\ %H:%M:%S) осуществит bash, это текущее системное время в формате, требуемом MySQL.

Так, теперь поменяем системный часовой пояс и убедимся, что всё применилось:

docker exec -i time_test tee /etc/timezone <<< 'Europe/Minsk'; \
docker exec -i time_test dpkg-reconfigure -f noninteractive tzdata; \
docker exec -i time_test date
Europe/Minsk

Current default time zone: 'Europe/Minsk'
Local time is now:      Sat Sep 30 19:51:51 +03 2017.
Universal Time is now:  Sat Sep 30 16:51:51 UTC 2017.

Sat Sep 30 19:51:51 +03 2017

В результате выполнения date (последняя строка) UTC изменился на +03 – верный признак успеха.

Перезапустим MySQL, чтобы применилось новое значение системного часового пояса:

docker restart time_test && \
docker exec -i -e MYSQL_PWD=helloworld time_test bash <<< 'while ! mysql -u root -e exit 2>/dev/null; do sleep 1; echo -n .; done; echo " MySQL seems to be ready"' && \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "SELECT NOW();" && \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "SHOW VARIABLES LIKE 'system_time_zone';"
. MySQL seems to be ready
NOW()
2017-09-30 19:52:13
Variable_name	Value
system_time_zone	+03

Сделаем еще одну вставку и посмотрим на результат:

docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "INSERT INTO timestamps VALUES (2, '$(date +%Y-%m-%d\ %H:%M:%S)');" && \
docker exec -i -e MYSQL_PWD=helloworld time_test mysql -u root test <<< "SELECT * FROM timestamps;"
id	ts
1	2017-09-30 22:51:45
2	2017-09-30 19:52:24

Несмотря на то, что в моем случае между вставками прошло всего 39 секунд, по результатам выборки этого никогда не скажешь. Давайте еще раз разберемся в причинах. При первой вставке MySQL считал переданное значение временем с нулевым смещением относительно UTC и посчитал timestamp для этого времени. На PHP это бы выглядело примерно следующим образом:

<?php

$timeZone = new DateTimeZone('UTC');
$timeToInsert = new DateTime('2017-09-30T19:51:45', $timeZone);
$unixTimestamp = $timeToInsert->getTimestamp();

var_dump($unixTimestamp);
int(1506801105)

Во второй раз было передано время 2017-09-30 19:52:24:

<?php

$timeZone = new DateTimeZone('+03:00');
$timeToInsert = new DateTime('2017-09-30T19:52:24', $timeZone);
$unixTimestamp = $timeToInsert->getTimestamp();

var_dump($unixTimestamp);
int(1506790344)

После этого мы сделали запрос на выборку и MySQL вывел эти значения Unix Timestamp в текущем часовом поясе, т.е. +03:00:

<?php

$records = [
    ['id' => 1, 'ts' => 1506801105],
    ['id' => 2, 'ts' => 1506790344],
];

echo 'id', "\t", 'ts', "\n";
foreach ($records as $record) {
    $dateTime = new DateTime("@{$record['ts']}");
    $dateTime->setTimeZone(new DateTimeZone('+03:00'));
    echo $record['id'], "\t", $dateTime->format('Y-m-d H:i:s'), "\n";
}
id	ts
1	2017-09-30 22:51:45
2	2017-09-30 19:52:24

Таким образом, значения в базе начинают идти вразнос, если кто-то меняет значение часового пояса в MySQL любым из приведенных выше способов.

Назад к статье

Александр Курило
Системный архитектор