前言
几乎每个后端都踩过这个坑:代码里 new Date() 明明是当前时间,存进数据库一看,时间差了整整 8 小时。或者反过来,从库里查出来的时间,展示到页面上又莫名早了 8 小时。
8 这个数字很有辨识度------它就是东八区(北京时间)与 UTC 的时差 。只要看到时间偏差是 8 小时(或它的整数倍),基本可以断定:某个环节的时区没对齐。
这篇文章讲清楚:8 小时到底是在哪一步冒出来的,涉及哪些容易被忽略的时区设置,以及怎么系统性地避开。
环境说明:以 Java + MySQL、部署在东八区为例,其他语言/数据库原理相通。
一、先复现:存进去和查出来对不上
一段最普通的存取代码:
java
// 存:当前时间
Date now = new Date();
System.out.println("Java 端:" + now); // Java 端:Wed Aug 05 14:00:00 CST 2026
// 假设通过 JDBC 存入 datetime 字段
存完直接去数据库查:
sql
SELECT create_time FROM t_order WHERE id = 1;
text
+---------------------+
| create_time |
+---------------------+
| 2026-08-05 06:00:00 | ← 少了 8 小时!
+---------------------+
Java 端是 14:00,数据库里却成了 06:00,整整差了 8 小时。反过来,从数据库读回来再展示,也可能变回来、或者错上加错。
这 8 小时不是数据丢了,而是时间在不同环节被按不同的时区解释了。要讲清楚,得先理解一个核心概念:时间的"值"和"时区"是两回事。
二、根因:一个时间点,在不同时区有不同的"读法"
2.1 时间戳是绝对的,"几点"是相对的
先建立一个关键认知:一个真正的"时间点",本质是一个绝对的时间戳(比如从 1970-01-01 UTC 至今的毫秒数),它全球统一、不含时区。
但当我们说"几点几分"时,就必须带上时区才有意义。同一个时间戳:
- 在东八区 读,是
2026-08-05 14:00:00; - 在**UTC(零时区)**读,是
2026-08-05 06:00:00; - 两者相差 8 小时,但指的是同一个瞬间。
所以"差 8 小时"往往不是时间错了,而是同一个时间戳,被两个环节用不同的时区翻译成了不同的'几点'。

2.2 8 小时是在哪冒出来的:链路上有多个时区
一条时间数据,从 Java 到 MySQL,至少经过这么几道"时区关卡",任何一道不一致就会出现偏差:
- JVM 时区 :Java 里
new Date()、LocalDateTime.now()依赖 JVM 的默认时区(user.timezone),通常取操作系统时区。 - JDBC 连接时区 :MySQL 驱动在传输时间时,会根据连接参数
serverTimezone(或服务器的时区设置)做转换。这是最常见的踩坑点。 - MySQL 服务器时区 :
time_zone变量,决定服务端如何解释和存储时间。 - 数据库字段类型 :
datetime和timestamp的行为还不一样(见下文)。
典型的 8 小时事故就是:JVM 是东八区(14:00),但 JDBC 连接或 MySQL 服务器被设成了 UTC,驱动把 14:00(东八区)换算成 UTC 存成了 06:00------数值上就少了 8 小时。
2.3 datetime 和 timestamp 的关键区别
MySQL 里两种时间类型对时区的态度完全不同,这也是偏差的一个来源:
datetime:不带时区 ,存的就是"字面上的年月日时分秒",你存14:00它就存14:00,与时区无关。偏差通常来自 JDBC 驱动在传输时做了时区转换。timestamp:与时区相关 ,MySQL 内部按 UTC 存储,读取时再根据服务器time_zone转换成对应时区显示。所以timestamp的显示值会随会话时区变化。
理解这个区别,就能解释很多"同一个库、有的字段偏、有的字段不偏"的诡异现象。
三、正解:让链路上的时区"全程对齐"
治本的思路只有一句:保证从 JVM → JDBC → 数据库,全链路时区一致,并且明确指定,不靠默认。
3.1 显式指定 JDBC 连接时区
MySQL 8 的驱动,连接串里务必带上 serverTimezone,和你的实际部署时区一致:
text
# 东八区部署,显式指定
jdbc:mysql://host:3306/db?serverTimezone=Asia/Shanghai&characterEncoding=utf8
# 如果全链路用 UTC(推荐的国际化方案),就统一成 UTC
jdbc:mysql://host:3306/db?serverTimezone=UTC
注意别用
serverTimezone=GMT%2B8这种偏移写法凑数,优先用Asia/Shanghai这样的具名时区,它能正确处理夏令时等规则。
3.2 统一 JVM、数据库的时区
-
JVM:启动参数显式指定,避免依赖服务器环境:
text-Duser.timezone=Asia/Shanghai -
MySQL 服务器 :确认
time_zone设置一致:sqlSHOW VARIABLES LIKE '%time_zone%'; -- 查看 SET GLOBAL time_zone = '+8:00'; -- 按需设置
三处(JVM、JDBC、MySQL)时区一致,8 小时偏差就消失了。

3.3 更现代的做法:用 java.time,并统一用 UTC 存储
如果做国际化系统(用户跨时区),业界更推荐的方案是:
- 代码里用
java.time包 (Instant、LocalDateTime、ZonedDateTime),语义清晰,替代老旧、易错、非线程安全的Date/Calendar/SimpleDateFormat。 - 存储统一用 UTC (数据库存 UTC,或用带时区的
Instant),只在展示层根据用户所在时区做转换。这样后端存储永远是"绝对时间戳",不受部署地点影响,跨时区也不会乱。
java
// 存:统一转成 UTC 的绝对时间点
Instant now = Instant.now(); // 不含时区,是绝对时间戳
// 展示:按用户时区转换
ZonedDateTime beijing = now.atZone(ZoneId.of("Asia/Shanghai"));
ZonedDateTime tokyo = now.atZone(ZoneId.of("Asia/Tokyo"));
四、常见误区与面试高频问答
Q:一看到差 8 小时,先怀疑哪里?
先查 JDBC 连接的 serverTimezone ,这是最高频的元凶。其次对比 JVM 时区 (TimeZone.getDefault())和 MySQL 的 time_zone。三者只要有一个不一致,就会出现偏差。8 小时对应东八区与 UTC,是最典型的信号。
Q:datetime 和 timestamp 到底用哪个?
看需求。timestamp 与时区相关、会自动按会话时区转换、范围到 2038 年(有溢出风险);datetime 存字面值、不受时区影响、范围更大。需要"跨时区自动转换"用 timestamp;只想存一个"确定的墙上时间"(如生日、排班)用 datetime。国际化系统更推荐统一存 UTC 时间戳,逻辑最清晰。
Q:为什么推荐 java.time 而不是 Date?
老的 Date/Calendar 设计有缺陷(月份从 0 开始等),而 SimpleDateFormat 非线程安全 ,在多线程/线程池下复用会解析出错乱的时间------这本身就是个经典坑(正好可以用 ThreadLocal 或改用线程安全的 DateTimeFormatter 解决)。java.time 的类都是不可变、线程安全的,且把"绝对时间点(Instant)"和"带时区时间(ZonedDateTime)"区分得很清楚,不容易出错。
Q:全链路都设成东八区,是不是就一劳永逸了?
对单一地区的系统够用。但如果系统要面向多个时区的用户,最佳实践仍是存储用 UTC、展示层按用户时区转换。把"存储"和"显示"解耦,才能从根上避免时区混乱。
总结
存进去差 8 小时,不是时间丢了,而是时区没对齐:
- 一个时间点 本质是绝对的时间戳,而"几点几分"必须带时区 才有意义;同一个时间戳,东八区读是
14:00,UTC 读是06:00,差 8 小时但是同一个瞬间。 - 偏差来自链路上的多个时区关卡不一致:JVM 时区、JDBC 的
serverTimezone、MySQL 的time_zone,其中 JDBC 连接时区是最常见的元凶 ;datetime(不带时区)和timestamp(按 UTC 存、按时区显示)行为不同,也会造成偏差。 - 正解:全链路时区显式对齐 ;国际化系统更推荐用
java.time+ UTC 存储、展示层转换。
一句话记忆: 差 8 小时 = 时区没对齐,先查 JDBC 的 serverTimezone;时间点是绝对的、"几点"是相对的,存 UTC、显示时再按时区转,最省心。