前言

这是一个 我们最近碰到的问题

在我们的一个 服务平台 查询到的时间字段 比 当前时区的当前时间多 8 小时 

然后 这个问题 也是挺神奇的, navicate 上面查询到的 时间是在正常的时间

然后 查询环境变量 tz_zone 是 “+08:00”, 也没有问题, 但是 客户端这边 拿到的是 当前时间 + 8小时 

时间相关数据传输以及转换 请参见 mysql date/time/datetime/year 的数据存储 

 

从结果上来看 可以大致的推断是 

客户端这边的 serverTimeZone 拿到的应该是 “+00:00”, 然后 将服务器传回来的字符串 “2023-07-25 00:00:00” 以 “+00:00” 转换为时间戳, 然后在转换为客户端本地的时间, 客户端本地的时区为 “+08:00”, 因此 得到 “2023-07-25 08:00:00”

 

问题的上下文是

同事 查看了 mysql 的 timezone 为 “+00:00”, 然后 将其时区更新为了 “+08:00”

但是 我不清楚他是怎么更新的, 是直接设置 全局变量, 还是说 改配置文件重启, 我最开始的理解是 改配置文件重启, 但是 后来怎么都发现不对, 原来是 直接设置的全局变量

然后 之后发现 服务平台这边 查询到的数据 还是有问题, 比当前时间 多了 8 个小时 

 

 

测试用例

测试用例如下, 关键的地方 是需要使用连接池 来复用连接 

/**
 * Test06MysqlTimezone02
 *
 * @author Jerry.X.He
 * @version 1.0
 * @date 2023/7/26 9:58
 */
public class Test06MysqlTimezone02 {

    // Test06MysqlTimezone
    public static void main(String[] args) throws Exception {

        QueryRunner jdbcTemplate = initDbConnect();

        String sql = " select * from tz_zone; ";
        while (true) {
            Map<String, Object> entity = jdbcTemplate.query(sql, new MapHandler());
            System.out.println((entity.get("field1")).toString());
            Tools.sleep(10_000);
        }

    }

    public static QueryRunner initDbConnect() {
        //        String url = "jdbc:mysql://10.60.50.16:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&autoReconnectForPools=true&useSSL=false&serverTimezone=GMT%2B0";
//        String url = "jdbc:mysql://10.60.50.16:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&autoReconnectForPools=true&useSSL=false";
        String host = "10.60.50.16";
        String port = "3306";
        String db = "test";
        String username = "root";
        String password = "root";
        String URL_TEMPLATE = "jdbc:mysql://%s:%s/%s";

        Properties prop = new Properties();
        prop.put("url", String.format(URL_TEMPLATE, host, port, db));
        prop.put("username", username);
        prop.put("password", password);
        prop.put("driver", "com.mysql.jdbc.Driver");
        prop.put("validationQuery", "select 1");
        prop.put("initialSize", 10);
        prop.put("minIdle", 10);
        prop.put("maxIdle", 10);
        prop.put("maxActive", 10);
        prop.put("maxWait", 100 * 1000);
        prop.put("timeBetWeenEvictionRunsMillis", 100 * 1000);
        prop.put("minEvictableIdleTimeMillis", 300000);
        prop.put("poolPreparedStatements", true);
        prop.put("testWhileIdle", true);
        DataSource dataSource = null;

        try {
            dataSource = BasicDataSourceFactory.createDataSource(prop);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new QueryRunner(dataSource);
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.

 

 

确定客户端的时区

将字节序列转换为 Timestamp 的地方是在 SqlTimestampValueFactory 中进行处理的 

这里传入的时间为 当前 session 缓存的 time_zone 为 “+00:00”, 然后传入的 time_zone 是来自于 session 的环境变量 

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_服务器

 

session 的 serverTimeZone 的初始化如下, 有限获取的是客户端这边本地配置的 serverTimeZone, 其次获取的是 从服务器这边获取到的 time_zone 的值 

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_serverTimeZone_02

 

所以 我们这边从 NativeServerSession 这边开始出发, 查看 它的 serverTimeZone

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_客户端_03

 

其值为 “GMT+00:00”, 这就是 上面转换 相比于本地时间 多了八个小时的原因

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_服务器_04

 

然后 我们再来看一下 它是获取的是 客户端这边本地配置的参数, 还是 服务器这边拿回来的 tz_zone 呢? 

服务器这边 拿回来的配置 time_zone 如下, 可以看到的是 “+00:00”

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_timeZone_05

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_客户端_06

 

查看一下 客户度这边的配置如下, 可以看到 值为 null

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_mysql_07

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_timeZone_08

 

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_客户端_09

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_客户端_10

 

综上 客户端这边拿到的转换 Timestamp 使用的时区是 “GMT+00:00“

因此 我当时的做法是 直接重启了一下 服务, 解决了问题

 

 

重启服务是否能够复现问题?

回到 开始的地方, 是直接设置 全局变量, 还是说 改配置文件重启, 我最开始的理解是 改配置文件重启, 但是 后来怎么都发现不对, 原来是 直接设置的全局变量

假设 我们这里是 更新了配置之后, 重启 mysql 服务, 更新时区为 “GMT+08:00”

可以看到的是, 这里重新建立的连接, 然后 自然从服务器这边 拿到的是最新的时区配置 “GMT+08:00”

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_timeZone_11

 

具体的处理是 客户端连接池这边的处理, 先是原有的连接已经关闭, 然后移除该连接 

然后是 新建和数据库这边的连接

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_timeZone_12

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_服务器_13

 

获取到的数据如下, 可以看到 中途数据同一个字段的时间更新了

2023-07-25 08:00:00.0
2023-07-25 08:00:00.0
2023-07-25 08:00:00.0
2023-07-25 00:00:00.0
2023-07-25 00:00:00.0
2023-07-25 00:00:00.0
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

 

 

更新服务器的全局变量是否能够复现问题?

执行 sql 如下 “set global time_zone = '+08:00';”

然后 服务器这边 全局变量的时区为 “GMT+08:00”, 然后 客户端那边的时区为创建连接的时候获取到的 “GMT+00:00”

然后 和同事咨询了一下, 他确实是 直接在 服务器这边 直接设置的全局变量 time_zone

 

获取到的数据如下, 可以看到 该字段的时间在调整 time_zone 前后, 客户端这边的处理 没有任何变化

2023-07-25 08:00:00.0
2023-07-25 08:00:00.0
2023-07-25 08:00:00.0
2023-07-25 08:00:00.0
2023-07-25 08:00:00.0
2023-07-25 08:00:00.0
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

 

 

Navicate 这边展示的时间是否随着 serverTimeZone 变化? 

呵呵 令我意外的是, navicate 这边展示的 时间字段的信息 貌似是直接是 服务器响应回来的时间字符串, 而没有根据服务器时区转换为时间戳, 然后再根据客户端时区转换为具体的时间 

 

观察一下 session 和 全局 级别的 time_zone 的配置, 均是配置的 “GMT+00:00”

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_serverTimeZone_14

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_服务器_15

 

 

然后查询结果如下, field1 为 正确的 “GMT+08:00” 的时间 

或者说 是最开始存放给 mysql 的那个 “时间字符串”

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_服务器_16

 

从服务器这边 拿到的数据如下

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_客户端_17

 

为了验证这个, 我们可以调整一下 服务器这边的输出

原始输出如下, 我们调整一下, 改一下 年月日 和 时分秒 中间的分隔符

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_timeZone_18

 

调整之后 日期如下

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_mysql_19

 

客户端这边 展示如下, 可以看到 navicate 这边展示的服务器响应回来的字符串本身 

48 mysql 全局变量修改了时区, 客户端拿到的依然是旧时区_serverTimeZone_20