前言
这是一个 我们最近碰到的问题
在我们的一个 服务平台 查询到的时间字段 比 当前时区的当前时间多 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 {// Test06MysqlTimezonepublic 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);}}
确定客户端的时区
将字节序列转换为 Timestamp 的地方是在 SqlTimestampValueFactory 中进行处理的
这里传入的时间为 当前 session 缓存的 time_zone 为 “+00:00”, 然后传入的 time_zone 是来自于 session 的环境变量
session 的 serverTimeZone 的初始化如下, 有限获取的是客户端这边本地配置的 serverTimeZone, 其次获取的是 从服务器这边获取到的 time_zone 的值
所以 我们这边从 NativeServerSession 这边开始出发, 查看 它的 serverTimeZone
其值为 “GMT+00:00”, 这就是 上面转换 相比于本地时间 多了八个小时的原因
然后 我们再来看一下 它是获取的是 客户端这边本地配置的参数, 还是 服务器这边拿回来的 tz_zone 呢?
服务器这边 拿回来的配置 time_zone 如下, 可以看到的是 “+00:00”
查看一下 客户度这边的配置如下, 可以看到 值为 null
综上 客户端这边拿到的转换 Timestamp 使用的时区是 “GMT+00:00“
因此 我当时的做法是 直接重启了一下 服务, 解决了问题
重启服务是否能够复现问题?
回到 开始的地方, 是直接设置 全局变量, 还是说 改配置文件重启, 我最开始的理解是 改配置文件重启, 但是 后来怎么都发现不对, 原来是 直接设置的全局变量
假设 我们这里是 更新了配置之后, 重启 mysql 服务, 更新时区为 “GMT+08:00”
可以看到的是, 这里重新建立的连接, 然后 自然从服务器这边 拿到的是最新的时区配置 “GMT+08:00”
具体的处理是 客户端连接池这边的处理, 先是原有的连接已经关闭, 然后移除该连接
然后是 新建和数据库这边的连接
获取到的数据如下, 可以看到 中途数据同一个字段的时间更新了
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
更新服务器的全局变量是否能够复现问题?
执行 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
Navicate 这边展示的时间是否随着 serverTimeZone 变化?
呵呵 令我意外的是, navicate 这边展示的 时间字段的信息 貌似是直接是 服务器响应回来的时间字符串, 而没有根据服务器时区转换为时间戳, 然后再根据客户端时区转换为具体的时间
观察一下 session 和 全局 级别的 time_zone 的配置, 均是配置的 “GMT+00:00”
然后查询结果如下, field1 为 正确的 “GMT+08:00” 的时间
或者说 是最开始存放给 mysql 的那个 “时间字符串”
从服务器这边 拿到的数据如下
为了验证这个, 我们可以调整一下 服务器这边的输出
原始输出如下, 我们调整一下, 改一下 年月日 和 时分秒 中间的分隔符
调整之后 日期如下
客户端这边 展示如下, 可以看到 navicate 这边展示的服务器响应回来的字符串本身
完