MySQL 时区处理完全指南
目录
一、前言
1.1 问题背景
当 JVM 时区和数据库时区不一致时,会发生什么?这个问题也许你从来没有注意过,但是当把 Java 程序容器化的时候,问题就浮现出来了,因为目前几乎所有的 Docker Image 的时区都是 UTC。
典型场景:
1 | Java 应用(Asia/Shanghai) → MySQL(UTC) |
1.2 为什么需要关注时区
- 🌍 全球化应用:用户分布在不同时区
- 🐳 容器化部署:Docker 默认使用 UTC 时区
- ⏰ 时间敏感业务:订单、日志、定时任务等
- 🔄 数据迁移:跨时区数据库迁移
本文探究了 MySQL 及其 JDBC 驱动对于时区的处理方式,并尝试给出最佳实践。
二、核心结论
2.1 关键要点总结
✅ 推荐做法:
DATE 和 TIME 类型不支持时区转换
- 存储什么值,查询就是什么值
- 不受 connection 时区影响
TIMESTAMP 类型自动进行时区转换
- 插入时:从 connection 时区 → UTC 存储
- 查询时:从 UTC → connection 时区返回
- JDBC 程序无需特别处理,只需保证 JVM 时区与用户所在时区一致
CURRENT_TIMESTAMP() 等函数安全可用
- 返回结果会自动转换为 connection 时区
- 可以安全使用
❌ 避免做法:
不要在服务器端做日期时间字符串格式化
DATE_FORMAT()返回的是服务端时区,不是 connection 时区- 应该在应用层进行格式化
不要混用 TIMESTAMP 和 DATETIME
- 同一业务场景应统一使用一种类型
- 避免时区转换混乱
2.2 快速决策表
| 数据类型 | 时区转换 | 适用场景 | 注意事项 |
|---|---|---|---|
TIMESTAMP |
✅ 自动转换 | 需要时区敏感的时间 | 范围:1970-2038 |
DATETIME |
❌ 不转换 | 固定时间点(如生日) | 范围:1000-9999 |
DATE |
❌ 不转换 | 仅日期(不含时间) | 无时区概念 |
TIME |
❌ 不转换 | 仅时间(不含日期) | 无时区概念 |
三、日期时间类型详解
3.1 TIMESTAMP vs DATETIME
MySQL 官方文档说明:
MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.) By default, the current time zone for each connection is the server’s time. The time zone can be set on a per-connection basis. As long as the time zone setting remains constant, you get back the same value you store. If you store a TIMESTAMP value, and then change the time zone and retrieve the value, the retrieved value is different from the value you stored. This occurs because the same time zone was not used for conversion in both directions.
核心要点:
- TIMESTAMP:存储时从当前时区转换为 UTC,查询时从 UTC 转换回当前时区
- DATETIME:不进行任何时区转换,存储什么值就返回什么值
- 默认时区:每个连接的默认时区是服务器时区,可以按连接设置
3.2 时区转换机制
1 | 插入流程(TIMESTAMP): |
关键点:
- ✅ 只要 connection 时区不变,存入和取出的值一致
- ⚠️ 如果改变时区后查询,返回的值会不同
- ❌ DATETIME 类型不会进行任何转换
3.3 实验验证
为了验证这个结论,我写了一段程序来实验,这个程序做了四件事情:
- 使用 Asia/Shanghai 时区构造一个日期
java.util.Date:2018-09-14 10:00:00,然后插入到数据库里(表:test,列:timestamp 类型) - 使用 Asia/Shanghai 时区把这个值再查出来,看看结果
- 使用 Asia/Shanghai 时区,获得这个字段的格式化字符串(使用 DATE_FORMAT() 函数)
- 使用 Europe/Paris 时区重复第 2-3 步的动作
实验结果:
1 | Insert data, Time Zone : 中国标准时间 |
结果分析:
| 项目 | Asia/Shanghai | Europe/Paris | 说明 |
|---|---|---|---|
| 插入时间 | 10:00:00 | - | 原始时间 |
| 查询 Date 对象 | 10:00:00 | 04:00:00 | ✅ 自动转换 |
| DATE_FORMAT 字符串 | 02:00:00 | 02:00:00 | ❌ 固定 UTC |
结论:
- ✅
Retrieve java.util.Date返回的结果根据 JVM 时区做了转换 - ❌
Retrieve formatted string返回的结果则是 UTC 时间(不受 connection 时区影响)
四、当前日期时间函数
4.1 常用函数列表
MySQL 与”当前日期时间”相关的函数:
| 函数 | 返回值类型 | 时区行为 |
|---|---|---|
CURRENT_TIMESTAMP() |
DATETIME/TIMESTAMP | ✅ 转换为 connection 时区 |
CURRENT_TIME() |
TIME | ✅ 转换为 connection 时区 |
CURRENT_DATE() |
DATE | ✅ 转换为 connection 时区 |
NOW() |
DATETIME | ✅ 转换为 connection 时区 |
FROM_UNIXTIME() |
DATETIME | ✅ 转换为 connection 时区 |
UTC_TIMESTAMP() |
DATETIME | ❌ 始终返回 UTC |
UTC_TIME() |
TIME | ❌ 始终返回 UTC |
UTC_DATE() |
DATE | ❌ 始终返回 UTC |
官方文档说明:
The CURRENT_TIMESTAMP(), CURRENT_TIME(), CURRENT_DATE(), and FROM_UNIXTIME() functions return values in the connection’s current time zone, which is available as the value of the time_zone system variable.
4.2 时区行为验证
为了验证这个结论,分别使用 Asia/Shanghai 和 Europe/Paris 来调用 CURRENT_TIMESTAMP()、CURRENT_TIME()、CURRENT_DATE()。
运行结果:
1 | JVM Time Zone : 中国标准时间 |
结果分析:
- ✅
CURRENT_DATE():两个时区返回相同日期(符合预期) - ✅
CURRENT_TIME():相差约 7 小时(上海 10:55 vs 巴黎 03:56) - ✅
CURRENT_TIMESTAMP():相差约 7 小时(上海 10:55 vs 巴黎 04:56)
4.3 DST 夏令时问题
发现的问题:
在 Europe/Paris 时区,CURRENT_TIME() 和 CURRENT_TIMESTAMP() 的时间部分相差一小时:
CURRENT_TIME(): 03:56:02CURRENT_TIMESTAMP(): 04:56:02
原因分析:
CURRENT_TIMESTAMP()返回的是 UTC + DST offset 结果CURRENT_TIME()返回的是 UTC offset 结果(未考虑夏令时)
这看上去是一个 Bug,已登记 Bug #92453。
关于 Europe/Paris 的 DST 信息:
可以在 Wiki - List of tz database time zones 找到详细信息。
建议:
- ⚠️ 在涉及夏令时的时区,谨慎使用
CURRENT_TIME() - ✅ 优先使用
CURRENT_TIMESTAMP()获取完整时间戳 - ✅ 在应用层进行时区转换和格式化
五、时区配置与管理
5.1 查看时区设置
1 | -- 查询系统时区和 session 时区 |
输出示例:
1 | +--------------------+---------------------+ |
时区值说明:
| 值 | 含义 | 示例 |
|---|---|---|
SYSTEM |
使用操作系统时区 | - |
+00:00 |
UTC 偏移量 | +08:00 (北京时间) |
Asia/Shanghai |
时区名称 | Europe/Paris, America/New_York |
5.2 修改时区配置
方法一:修改 Session 时区(仅当前连接有效)
1 | -- 设置为指定时区 |
方法二:修改 Global 时区(新连接生效)
1 | -- 需要 SUPER 权限 |
方法三:修改配置文件(永久生效)
编辑 my.cnf 或 mysqld.cnf:
1 | [mysqld] |
重启 MySQL 服务:
1 | sudo systemctl restart mysql |
5.3 Docker 环境特殊处理
问题:Docker 容器默认使用 UTC 时区
解决方案:
方案 1:挂载时区文件(推荐)
1 | # Dockerfile |
1 | # docker-compose.yml |
方案 2:设置环境变量
1 | docker run -d \ |
方案 3:在容器中安装时区数据
1 | FROM mysql:8.0 |
方案 4:JDBC URL 中指定时区
1 | # application.properties |
最佳实践:
- ✅ 推荐方案 1 + 方案 4 组合使用
- ✅ MySQL 服务器使用 UTC,JDBC 连接指定时区
- ❌ 避免在多个层面同时设置时区(容易混乱)
六、最佳实践
6.1 Java 应用开发建议
✅ 推荐做法:
统一使用 TIMESTAMP 类型
1
2
3
4
5// ✅ 好:使用 java.time 包
LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
// ✅ 好:JDBC 4.2+ 支持
preparedStatement.setObject(1, now);JVM 时区与用户时区保持一致
1
2
3
4
5// 启动参数设置
// -Duser.timezone=Asia/Shanghai
// 或在代码中设置(不推荐)
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));使用 java.time 替代 Date/Calendar
1
2
3
4
5
6
7// ✅ 推荐:Java 8+
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
Instant instant = zonedDateTime.toInstant();
// ❌ 不推荐:旧 API
Date date = new Date();
Calendar calendar = Calendar.getInstance();在应用层进行格式化
1
2
3
4
5
6// ✅ 好:应用层格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = zonedDateTime.format(formatter);
// ❌ 不好:数据库层格式化
// SELECT DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') FROM table
❌ 避免做法:
- ❌ 不要混用
TIMESTAMP和DATETIME - ❌ 不要在 SQL 中使用
DATE_FORMAT()返回给前端 - ❌ 不要硬编码时区偏移量(如
+08:00),应使用时区名称 - ❌ 不要忽略 DST 夏令时问题
6.2 SQL 编写规范
✅ 推荐写法:
1 | -- ✅ 好:使用 CURRENT_TIMESTAMP 默认值 |
❌ 避免写法:
1 | -- ❌ 不好:使用 DATE_FORMAT 返回字符串 |
6.3 常见问题排查
Q1: 查询时间与插入时间不一致?
排查步骤:
1 | -- 1. 检查当前会话时区 |
解决方案:
1 | -- 统一设置为相同时区 |
Q2: Docker 环境中时区不正确?
排查步骤:
1 | # 1. 检查容器时区 |
解决方案:
1 | # 重新加载时区数据 |
Q3: Java 应用获取的时间差 8 小时?
原因:JVM 时区与 MySQL 时区不一致
解决方案:
1 | # 方案 1:JDBC URL 指定时区 |
Q4: 如何处理历史数据的时区问题?
场景:数据库中存在大量 DATETIME 类型数据,需要迁移到 TIMESTAMP
步骤:
1 | -- 1. 备份数据 |