MySQL 和 PostgreSQL 中的时间戳:Go 和 Java 访问时的坑

MySQL 和 PostgreSQL 中的时间戳:Go 和 Java 访问时的坑

💡 原文中文,约15800字,阅读约需38分钟。
📝

内容提要

文章对比了MySQL与PostgreSQL的时间戳语义:MySQL的TIMESTAMP表示绝对时刻,DATETIME表示墙钟时间;PostgreSQL的timestamptz表示绝对时刻,timestamp表示墙钟时间,命名与MySQL相反。核心问题在于应用与数据库间的时区链路:MySQL的TIMESTAMP依赖客户端时区与会话time_zone,配置不一致会导致时间漂移,使用带夏令时的具名时区时漂移还会随时间变化。Go和Java驱动各有参数陷阱。建议全链路使用UTC,绝对时刻用带时区类型,墙钟时间用无时区类型。

🔎

延伸解读

命名相反:跨库迁移最容易踩的语义陷阱

MySQL 的 TIMESTAMP 是绝对时刻,DATETIME 是墙钟时间;PostgreSQL 恰好相反,timestamptz 是绝对时刻,timestamp 是墙钟时间。这意味着把 MySQL 的 TIMESTAMP 迁移到 PG 时,若按字面名字对应到 timestamp,语义就完全反了。写 ORM 映射或做跨库同步时,必须先确认列承载的是“唯一瞬间”还是“挂钟读数”,再选类型,不能只看类型名。

MySQL 时间漂移的根源:客户端时区与会话时区不配套

MySQL 的 TIMESTAMP 在写入时按会话 time_zone 转成 UTC 存储,读取时再转回会话时区;而驱动传输的是不带时区的字段串,其墙钟语义由客户端时区(Go 的 loc、Java 的 connectionTimeZone)决定。两端配置不一致就会产生漂移,公式为:存储 UTC = 真实 UTC −(会话时区 − 客户端时区)。同一条连接读写误差会抵消,所以自己看不出问题,坑全转嫁给 CLI、BI 工具和 SQL 层比较。

带夏令时的具名时区会让漂移随时间变化

如果会话时区使用 Europe/London 这类带 DST 的具名时区,而客户端时区是 America/New_York,两者夏令时切换日期不同,一年中有几周处于不同偏移状态,导致漂移量不是常数。例如 2026 年 3 月 8 日至 3 月 29 日、10 月 25 日至 11 月 1 日期间,漂移会在 −4h 和 −5h 之间跳变。这会让表内数据分裂成多套偏移,统一加减小时的修数脚本无法收敛,比固定偏移不一致更难排查。

落地建议:全链路 UTC 并显式声明精度

文章给出的核心解法是:库与连接全程 UTC,展示层再转本地时区。Go 的 MySQL DSN 加 parseTime=true&loc=UTC&time_zone='+00:00';Java 用 connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true,并加 -Duser.timezone=UTC。绝对时刻选 PG timestamptz 或 MySQL DATETIME(6) 写 UTC;墙钟时间用无时区类型。跨库统一微秒精度,MySQL 显式写 D

❓

Q&A

MySQL 和 PostgreSQL 中,TIMESTAMP 和 DATETIME 类型在语义上有什么根本区别?

MySQL 的 TIMESTAMP 表示绝对时刻,DATETIME 表示墙钟时间;PostgreSQL 的 timestamptz 表示绝对时刻,timestamp 表示墙钟时间。命名正好相反:MySQL 的 TIMESTAMP 对应 PG 的 timestamptz,而不是 PG 的 timestamp。

为什么 MySQL 的 TIMESTAMP 容易导致时间漂移?

MySQL 的 TIMESTAMP 在写入时按会话 time_zone 转成 UTC 存储,读取时再转回会话时区。如果客户端时区(如驱动配置的 loc)与会话 time_zone 不一致,就会产生漂移。漂移量 = 真实 UTC −(会话时区 − 客户端时区)。若任一端使用带夏令时的具名时区,漂移还会随时间变化。

在 Go 中访问 MySQL 时,如何正确配置驱动以避免时间问题?

推荐 DSN 配置:parseTime=true、loc=UTC、time_zone='+00:00'(URL 编码为 %27%2B00%3A00%27)。这样读写全链路使用 UTC,业务传入 time.Now() 也没关系,展示时再转换为本地时区。关键是要保证所有客户端的 loc 与会话 time_zone 配套一致。

使用 PostgreSQL 的 timestamptz 时,Go 的 pgx 驱动有什么需要注意的?

pgx 读取 timestamptz 时默认挂 time.Local 标签,时刻本身正确,但 Format、Hour、JSON 序列化会按 Local 渲染。要固定 UTC 输出,可在出参时调用 .UTC(),或通过 AfterConnect 设置 ScanLocation 为 time.UTC。写入时任何 Location 的 time.Time 都能正确存储。

Java 应用连接 MySQL 时,connectionTimeZone 和 preserveInstants 参数如何配合使用?

connectionTimeZone 指定客户端时区(默认 LOCAL 即 JVM 默认时区),preserveInstants 控制是否按时刻语义转换。推荐组合:connectionTimeZone=UTC 且 forceConnectionTimeZoneToSession=true,驱动会自动执行 SET time_zone='+00:00',实现全链路 UTC。若服务器未装时区表,可将 UTC 换成 %2B00%3A00。

在数据库建模时,如何选择时间类型来避免时区问题?

绝对时刻(如下单时间)用带时区类型:PG 用 timestamptz,MySQL 用 DATETIME(6) 并在应用层统一写 UTC,或 TIMESTAMP(6) 但需接受 2038 上限和会话时区转换。墙钟时间(如营业时间)用无时区类型:PG 的 timestamp、MySQL 的 DATETIME。精度显式声明为微秒,可空时间用 NULL 而非零值。

🏷️

标签

➡️

继续阅读