Недавно компания обновила версию Spring Boot (с 1.5 до 2.1), в ходе которой наткнулась на очень непонятную ловушку часового пояса MySQL, а именно: время чтения исторических данных базой данных отстает от реального времени на 14 часов, и новые пишут Входящие данные в норме. Если вы ранее также использовали настройку часового пояса MySQL по умолчанию, вы, скорее всего, столкнетесь с этой проблемой.Причина этого связана с множеством технических деталей, поэтому она разбирается и делится с вами.
Сначала рассмотрим причины. После обновления до Boot 2.1 версия MySQL Connect/J также обновляется до 8.0, и сначала будут использоваться параметры подключения (serverTimezone
) в указанном часовом поясе, если не указан, то используйте часовой пояс, настроенный базой данных, см.официальное объявление(Соответствующий исходный кодcom.mysql.cj.protocol.a.NativeProtocol#configureTimezone()
). Так как мы раньше не указывали часовой пояс в параметрах подключения к БД, а конфигурация БД стоит по умолчаниюCST
Часовой пояс (центральный часовой пояс США, т.е. -6:00), поэтому отображаемое время искажено.
Connector/J 8.0 always performs time offset adjustments on date-time values, and the adjustments require one of the following to be true:
- The MySQL server is configured with a canonical time zone that is recognizable by Java (for example, Europe/Paris, Etc/GMT-5, UTC, etc.)
- The server's time zone is overridden by setting the Connector/J connection property
serverTimezone
(for example,serverTimezone=Europe/Paris
).
После обнаружения причины решение относительно простое.
Способ 1: добавить параметры подключения в базу данныхserverTimezone=Asia/Shanghai
илиserverTimezone=GMT%2B8
. Этот параметр не нужно добавлять в Boot 1.5, но его можно добавить.
Способ 2. Измените конфигурацию time_zone базы данных MySQL на+8:00
(по умолчаниюSYSTEM
). При использовании этого метода нет необходимости изменять параметры подключения к базе данных.
Второй метод явно лучше, одна модификация, польза на всю жизнь. Но обратите внимание, что для вновь сгенерированного пакета данных после обновления до Boot 2.1, если он содержит поле типа time и значение поля указано приложением, а не сгенерировано базой данных (например,DEFAULT CURRENT_TIMESTAMP
), то требуется ручное исправление (плюс часы отклонения).
Два решения очень просты Некоторые студенты сразу спросят, почему этой проблемы нет в Boot 1.5? Почему 14-часовое отклонение при чтении исторических данных при загрузке 2.0, но вновь сгенерированные данные в порядке? Чтобы ответить на эти два вопроса, недостаточно прочитать официальный анонс, нужно прочитать исходный код MySQL Connect/J.
Головоломка 1, почему этой проблемы нет под Boot 1.5? Ответ скрыт вcom.mysql.jdbc.ResultSetImpl
иcom.mysql.jdbc.ConnectionImpl
в исходном коде обоих классов.
// 源代码:com.mysql.jdbc.ResultSetImpl
private TimeZone getDefaultTimeZone() {
// useLegacyDatetimeCode默认为true,因此使用connection的默认时区
return this.useLegacyDatetimeCode ? this.connection.getDefaultTimeZone() : this.serverTimeZoneTz;
}
// 源代码:com.mysql.jdbc.ConnectionImpl
public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
// connection的默认时区使用的是JVM的默认时区,一般为操作系统的时区
// We store this per-connection, due to static synchronization issues in Java's built-in TimeZone class...
this.defaultTimeZone = TimeUtil.getDefaultTimeZone(getCacheDefaultTimezone());
}
При загрузке 1.5 MySQL Connect/J по умолчанию использует часовой пояс операционной системы (Азия/Шанхай, т.е. +8:00), игнорируя параметры соединения или часовой пояс, указанный базой данных. , используется единый часовой пояс. , поэтому смещение времени отсутствует.
Головоломка 2, почему в Boot 2.0 при чтении исторических данных наблюдается 14-часовое отклонение, а вновь сгенерированные данные в порядке? После обновления до Boot 2.0 MySQL Connect/J изменился, чтобы использовать часовой пояс CST, настроенный базой данных, и исторические данные были сгенерированы в часовом поясе Азии/Шанхая под Boot 1.5, поэтому было прочитано, что было 14 (-6 :00 и +8:00 между) отклонение часов. Для вновь сгенерированных данных отклонений нет, поскольку они находятся в часовом поясе CST.
После решения этих двух головоломок у вас могут остаться некоторые сомнения. Затем, в сочетании с порядком потока данных, давайте проанализируем изменения часового пояса в процессе потока данных.
Установите Приложение-1 в качестве производителя данных, Приложение-2 в качестве потребителя данных, TZ-IN1 в качестве часового пояса, в котором находится Приложение-1, TZ-IN2 в качестве часового пояса, в котором Приложение-1 записывает в базу данных, и TZ- OUT1 as Application-2 Чтение часового пояса базы данных, TZ-OUT2 — это часовой пояс, в котором находится Application-2. Как упоминалось ранее, TZ-IN2 и TZ-OUT1 определяются параметрами соединения или конфигурацией базы данных.
Весь процесс потока данных включает 3 явных преобразования часовых поясов и 1 неявное преобразование часовых поясов.
- Преобразование ① (явное): TZ-IN1 в TZ-IN2, это преобразование выполняется MySQL Connect/J (см.
com.mysql.cj.ClientPreparedQueryBindings#setTimestamp()
, из-за нехватки места дальнейший анализ здесь проводиться не будет). - Преобразование ② (неявное): преобразование TZ-IN2 без часового пояса.Когда MySQL хранит поля типа времени внутри, либо игнорируйте часовой пояс (тип DateTime), либо используйте UTC (тип Timestamp), обратитесь к разделу типа времени, официально объявленному MySQL .
- Преобразование ③ (явное): передача в TZ-OUT1 без часового пояса и установка времени без часового пояса, считанного MySQL, в часовой пояс TZ-OUT1 (см.
com.mysql.cj.result.SqlTimestampValueFactory#localCreateFromTimestamp()
). - Преобразование ④ (явное): TZ-OUT1 в TZ-OUT2, за это преобразование отвечает Application-2, которое обычно выполняется на уровне DAO.
Внимательно проанализируйте эти 4 преобразования часовых поясов. Среди них ①, ② и ③ все выполняются MySQL. Нет сомнений в правильности. Однако, поскольку и TZ-IN2, и TZ-OUT1 указаны приложением, если два значения не совпадают, то в конечном результате будут отклонения (это яма, на которую мы наступили). Что касается ④, то вы должны полагаться на приложение, чтобы обеспечить правильность, и, как правило, ошибки нет. Кроме того, будь то преобразование часового пояса или другие типы преобразования данных (например, преобразование набора символов), мы можем обнаружить, что ключом к правильному преобразованию является то, что получатель данных должен использовать тот же формат, что и отправитель данных. Это может показаться бессмыслицей, но это основной ментальный метод решения таких проблем.
На данный момент ловушка часового пояса MySQL Connect/J 8.0 заполнена, и я надеюсь, что вы что-то из этого выиграете.