Dreamer Dreamer
首页
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)

lycpan233

白日梦想家
首页
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)
  • Mysql

  • Node

    • npm为什么父项目指定依赖版本后,可以影响到子项目的依赖
    • MacOS pnmp多个项目同个包,为什么没有共享存储空间?
    • pnpm 使用指南
    • pnpm 下载依赖更换源不生效
    • Commitizen + Commitlint + husky 实践
    • package.json 中波浪号~ 异或号^ 是什么意思?
    • 日志追踪
    • 详解UTC、Unix时间戳、时区问题
      • 问题背景
        • 概述
      • 问题定位与分析
        • 运行环境
        • 问题排查
        • 首先明确各个可能影响时间的因素。
        • 逐步排查
        • 结论与思考
      • UTC、GMT、Unix 时间戳、纪元时间是什么?
        • 常见时间格式
        • UTC、GMT
        • 纪元时间、Unix 时间戳
      • 时区问题
        • 实现细节
        • Mysql 类型选择
        • 服务端时区问题
        • 客户端处理
      • 结语
      • 参考文章
      • 附图
  • Go

  • Docker

  • 后端
  • Node
lycpan233
2025-09-18
目录

详解UTC、Unix时间戳、时区问题

# 问题背景

# 概述

在实际开发中发现,Mysql timestamp 存储的时间与本地机器的时间不一致(误)。因此引发了一系列思考。

看一下示例,我在2025-09-15 11:39:xx这个时间点插入一条数据,然后查询出来,发现时间变成了2025-09-15 03:40:54。

如图所示:

img-1

聪明的同学可能一下就想到了问题所在,是不是 ORM、 Mysql 没有指定正确的时区?

但先别急,更神奇的地方来了,当我们取出的原数据进行格式化以后,时间又仿佛又正常了。

// 伪代码
const info = db.findOne(); // {"id": 23, "gmtCreate": "2025-09-15T03:39:54.000Z"}

// 格式化时间
dayjs(info.gmtCreate).format("YYYY-MM-DD HH:mm:ss"); // 2025-09-15 11:39:54
1
2
3
4
5

??? 发生了什么,一瞬间小脑瓜里充满了疑问。接着我们尝试梳理一下,这里会产生问题的几个方面进行排查和定位。

  1. 写入数据的时候,创建时间 是由 ORM 的 hook 生成的,还是 Mysql 的行为?
  2. 和时区是否有关?这里要辨别服务器时区、Mysql 时区。
  3. 为什么格式化以后,时间又正常了?

# 问题定位与分析

# 运行环境

  1. 服务器运行在我本地。
  2. Mysql 8.0 运行在 docker 内。
  3. ORM 使用的是 drizzle + mysql2。
  4. 时间转换的库采用的 dayjs。

示例的代码逻辑:

/**
 * ORM的 schema
 * mysqlTable({
 *    id: int({ unsigned: true }).autoincrement().primaryKey(),
 *    gmtCreate: timestamp('gmt_create').notNull().default(sql`CURRENT_TIMESTAMP`),
 * })
 *
 * 表的 schema
 * CREATE TABLE `tb_review_record` (
 *   `id` int unsigned NOT NULL AUTO_INCREMENT,
 *   `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
 * }
 */

// 插入
const newInfo = db.create(); // 未传入任何时间

// 取到对应值
const info = db.findOne(); // {"id": 23, "gmtCreate": "2025-09-15T03:39:54.000Z"}

// 格式化时间
dayjs(info.gmtCreate).format("YYYY-MM-DD HH:mm:ss"); // 2025-09-15 11:39:54
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 问题排查

# 首先明确各个可能影响时间的因素。

  • 服务器时区,东八区。
  • Mysql 时区,未设定。
  • ORM 时区,未设定。(且不支持设定)
  • dayjs 未设定时区。

# 逐步排查

  1. 写入数据库的时候,ORM 未使用 hook,因此插入的时候是 Mysql 通过 CURRENT_TIMESTAMP 写入的时间。
  2. gmt_create 的字段类型为 timestamp。 timestamp 会将当前会话的时区转换为 UTC 时间写入到数据库中。当展示的时候,会根据当前会话的时区转换为本地时间展示。
  3. 因此写入数据的时候是通过,Mysql 的 CURRENT_TIMESTAMP 函数获取的当前时区时间写入字段的。
  4. 当从 ORM 中取出的时候,也没有经过 hook,其返回格式为 2025-09-15T03:39:54.000Z 是一个符合 ISO 8601 / RFC 3339 标准的日期时间格式。其中 Z 代表 UTC 时区(即 0 时区)。
  5. 当 dayjs 转换时间的时候,默认会取运行环境的本地时区进行格式化,因此 2025-09-15T03:39:54.000Z 被格式化为服务器(东八区)的 2025-09-15 11:39:54

# 结论与思考

结论

基于之前的排查,我们可以得出结论,Mysql 查到的数据是 UTC 时间,是符合预期的。

ORM 取时间的时候没有做转换,而 dayjs 格式化的时候将它转成了服务器时间。

因此之前感觉 Mysql 存储的时间是错误的,这个结论不正确。

我们可以进一步考证。 查 SQL 的时候,我们手动指定一个时区,如图所示:

img-3

冰果,符合预期。

思考

从这个小误会,我们发现在业务里处理时间涉及到很多环节,而且一不小心就容易搞混。

比如前文提到的 UTC 、timestamp 等等,它们到底是什么东东?我们应该如何分辨和应用?

如果涉及到国际化业务我们又应该如何处理? 客户端如何正确展示时间?服务端如何正确处理时间?时间格式又应该如何可靠的存储??

# UTC、GMT、Unix 时间戳、纪元时间是什么?

# 常见时间格式

在解决实际问题之前,我们先了解一下我们面对的“敌人”都有什么形态。

互联网常见时间格式参考表

格式名称 示例 说明与用途
ISO 8601 2025-09-15T15:27:00Z 国际标准,广泛用于 API、数据库、日志等
RFC 3339 2025-09-15T15:27:00+08:00 基于 ISO 8601,常用于 JSON、REST 接口
RFC 2822/5322 Mon, 15 Sep 2025 15:27:00 +0800 邮件头部时间格式,适用于电子邮件系统
Unix 时间戳 1757940420 自 1970 年起的秒数,适用于后端存储与计算
自然语言 September 15, 2025, 3:27 PM 人类可读,适合前端展示
中文格式 2025年9月15日 15:27 本地化展示,适合中文用户界面
美国格式 09/15/2025 3:27 PM MM/DD/YYYY,12 小时制,常见于美式网站
欧洲格式 15.09.2025 15:27 DD.MM.YYYY,24 小时制,常见于欧式系统
Excel 序号 45800 Excel 中的日期序号,自 1900 年起的天数
Julian 日期 JD 2460560.144 天文学与科学计算中使用
Swatch Internet Time @500 一天划分为 1000 beats,极少使用

里面有些是供人类可读性使用的,有些是专业领域使用的。我们主要关注前 4 种格式。

  • ISO 8601 是国际标准,也是最常用的时间格式。它是一种紧凑的、可扩展的、跨平台的时间格式,被广泛用于 API、数据库、日志等场景。
  • RFC 3339 是基于 ISO 8601 的扩展,常用于 JSON、REST 接口等场景。
  • RFC 2822/5322 是邮件头部时间格式,适用于电子邮件系统。
  • Unix 时间戳 是自 1970 年起的秒数,适用于后端存储与计算。

其中前三种格式直观感受大同小异,都是 时间格式+偏移量 。尤其是前两种我们业务中也经常有见到,如:

new Date(); // 2025-09-15T07:37:09.433Z

dayjs().tz("Asia/Shanghai").format(); // 2025-09-15T15:45:52+08:00
1
2
3

前者的 Z 代表 UTC 时区 ,后者的 +08:00 代表东八区。

还有一个格式是 Unix 时间戳,相信大家也不陌生,它是从 1970 年 1 月 1 日 00:00:00 UTC 开始计时的时间差值。

常见的分为 10 位的秒戳和 13 位的毫秒戳。

{
  "unix": 1757922548, // 对应时间:2025-09-15 15:49:08 CST(东八区)
  "valueOf": 1757922577000 // 对应时间:2025-09-15 15:49:37 CST(东八区)
}
1
2
3
4

# UTC、GMT

UTC,全称是 Coordinated Universal Time(协调世界时),是目前全球通用的时间标准。

GTM,全称是 Greenwich Mean Time(格林尼治标准时间),历史上最早被广泛采用的全球时间标准。它的定义和地位曾经非常重要,但现在已经被更精确的 UTC(协调世界时) 所取代。 目前仍用于航海、部分手表、历史文献中。

在我们开发中,首选的都是 UTC 时间, GMT 仅在一些欧洲用户偏好的地方有做 UI 相关的处理,这里不做赘述。

# 纪元时间、Unix 时间戳

纪元时间,全称是Epoch Time,指的是 1970 年 1 月 1 日 00:00:00 UTC 时间,它是 Unix 时间戳的起点。是 Unix 诞生之初,开发者团队为了简化时间计算定义的时间。(这个纪元时间的来历,网上有很多都市传说,感兴趣的可以课下了解一下...)

Unix 时间戳,就是从纪元时间开始,到现在的秒数。它是一个整数,通常以秒为单位,也可以用毫秒表示。

例如我们可以常常看到,1970 年 1 月 1 日 这个时间,往往就是对 "0" 进行时间戳格式化导致的。

dayjs.unix(0).format("YYYY-MM-DD HH:mm:ss"); // 1970-01-01 08:00:00 注意: 0 本身是纪元时间,这里格式化以后取了东八区
1

# 时区问题

时区这个概念,在我们日常交流的语境里经常会被淡化,比如几点和朋友约好吃饭、几点上课、几点放学等等,我们往往不会去考虑时区的问题。

但是在涉外业务中,时区问题就变得非常关键。例如 apple 公司想要举办一个全球性的活动,那么这个活动的时间应该如何通知到不同地区的用户?

我们现在打开 apple 官网的一个近期活动,看一下他们的实现。

  1. apple.com

img-2

  1. apple.com/cn

img-4

通过对比我们可以看到,apple 返回了两种时间格式,一个是 UTC 时间,一个是本地时间。

结合 Mysql timestamp 的设计,不难想到,我们也可以采用存储 UTC 时间,然后交由客户端处理为本地时间的方案。

# 实现细节

# Mysql 类型选择

类型 存储大小 时间范围(有效值) 格式 是否受时区影响 小数秒支持 用途说明
DATE 3 字节 1000-01-01 到 9999-12-31 YYYY-MM-DD ❌ ❌ 仅表示日期
TIME 3 字节 -838:59:59 到 838:59:59 HH:MM:SS ❌ ✅ 表示时间或持续时间
YEAR 1 字节 1901 到 2155 YYYY ❌ ❌ 表示年份
DATETIME 8 字节 1000-01-01 00:00:00 到 9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS ❌ ✅ 日期和时间组合,绝对时间
TIMESTAMP 4–7 字节 1970-01-01 00:00:01 到 2038-01-19 03:14:07 UTC YYYY-MM-DD HH:MM:SS ✅ ✅ 日期和时间组合,受时区影响

Mysql 的时间类型中只有 TIMESTAMP 支持时区的转换,其他几个类型,如果传入什么时区,则会存储什么时区。而 TIMESTAMP的值是从当前时区转换为 UTC,并在检索时从 UTC 转换回当前时区。如果连接的时区保持不变,则得到的时间值就会和存储的相同。但是如果存储以后,变更了时区则再次取出来,时间值会根据新时区进行变更。

因此,在连接数据库时,要保证时区的一致性。

# 服务端时区问题

服务端的时区,一般和运行环境相关,如果部署在 Docker 中,那么默认就是 UTC 时区。

如果部署在不同的区域的服务商里,可能还需要额外指定时区。

因此在业务里,我们要保持时区的一致性,避免取到偏移值。

常见的时间处理方式有两种: 1. Date 对象。 2. 时间处理库,如 dayjs。

Date 对象

Date 对象是基于 Unix 时间戳实现的,因此它处理时间的方式是基于 UTC 时间进行处理的。

若我们想获取当前的 UTC 时间只需要 new Date() 即可。

dayjs

dayjs 默认会使用运行环境的时区进行转换,因此我们在业务里使用 dayjs 需要先进行拓展。

import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
/**
 * 拓展 Dayjs
 */
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault("UTC");

// 处理时间时采用 dayjs.utc 或者 dayjs.tz
console.log(dayjs.utc().format()); // 2025-09-15T10:18:39Z
console.log(dayjs().format()); // 2025-09-15T18:18:39+08:00

console.log(dayjs.tz(dayjs().format(), "UTC").format()); // 2025-09-15T10:18:39Z
console.log(dayjs.tz(dayjs().format(), "Asia/Shanghai").format()); // 2025-09-15T10:18:39+08:00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

ORM

ORM 的连接一般可以指定时区,默认指定 Z 即可。

注: drizzle ORM 并不支持设置 Session 时区。

# 客户端处理

服务端经过统一处理后,返回的时间正常应为 UTC 时间,客户端即可根据用户的运行环境转换为对应的本地时间。

# 结语

写道这里,我们大概对于时间格式、时间存储等方面有了一个比较清晰的认识。

但实际上,针对当前的实现方案仍有几个问题值得思考:

  1. 当用户 bios 没电了,时间存在问题,这时候服务端和客户端可以修正时间差嘛? 应该通过什么方式进行交互?
  2. 基于上面的问题,我们在接口校验的时候,常常会引入时间差的概念,当前模式是否支持?
  3. 当我们涉及到字符串 YYYY-MM-DD HH:mm:ss 的时候又应该如何处理?

# 参考文章

  • Mysql 8.0 - date (opens new window)
  • Mysql 8.0 function_current-timestamp (opens new window)
  • Date - MDN (opens new window)

# 附图

编辑 (opens new window)
上次更新: 2025/09/18, 01:27:45
日志追踪
Wire 依赖注入

← 日志追踪 Wire 依赖注入→

最近更新
01
vscode pwsh 输入时突然一片空白
09-01
02
日志追踪
08-07
03
docker基础概念
02-26
更多文章>
Theme by Vdoing | Copyright © 2023-2025 Dreamer | MIT License
粤ICP备2025379918号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式