基于 PostgreSQL 的多时区应用开发最佳实践

详解如何在 PostgreSQL 中处理多时区数据,包括时区设置、时间戳转换、最佳实践,帮助你构建健壮的多时区应用。

1. 核心原则

系统内部(数据库、后端代码、API 传输)统一使用 UTC,只有在数据真正呈现在用户眼前时,才转换成用户的当地时间。


2. 数据库设计

✅ 推荐方案:使用 timestamptz

在建表时,时间字段必须使用 timestamp with time zone (简写为 timestamptz)。

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMPTZ DEFAULT NOW() -- ✅ 正确
);
  • 原理:Postgres 在存储 timestamptz 时,会自动将输入的任何时区时间转换为 UTC 格式存储。它内部不保存时区名(如 ‘Asia/Shanghai’),只保存 UTC 时间点。
  • 优势
    1. 语义明确:告诉所有连接数据库的客户端(代码、BI 工具、SQL 客户端),这是一个绝对时间点。
    2. 计算强大:支持直接在 SQL 中跨时区查询(如 AT TIME ZONE 语法)。
    3. 防呆:无论误传了哪个时区的时间,存入后在物理时间轴上都是同一个点。

❌ 反模式:使用 timestamp (without time zone) 存 UTC

不要为了“只存 UTC”而使用 timestamp without time zone

  • 风险:这会让数据变成“裸奔”状态(没有元数据)。第三方工具(如 BI 报表)或不同的数据库驱动会根据自己的配置猜测这个时间的含义,极易导致 8 小时偏差的 Bug。

3. 数据流转架构

阶段一:数据写入 (Write)

  1. 前端 (浏览器/App)
    • 用户选择/产生时间(本地时间)。
    • 提交前将时间转换为 UTC 的 ISO 8601 字符串
    • 例如: "2023-12-18T10:00:00Z" (带 Z 代表 UTC)。
  2. 后端 (API)
    • 透传该时间对象。
  3. 数据库 (Postgres)
    • 接收到带 Z 的时间字符串。
    • 字段为 timestamptz
    • 处理:由于输入已是 UTC,且存储目标也是 UTC,数据库不进行任何时区加减计算,直接落库。
    • 效率最高,路径最短。

阶段二:数据读取 (Read)

  1. 数据库配置
    • 后端服务连接 Postgres 时,Session 时区设置为 UTC (SET TIME ZONE 'UTC')。
  2. 数据库 (Postgres)
    • 读取数据,原样吐出 UTC 时间。
  3. 后端 (API)
    • 序列化为 JSON,格式统一为 "2023-12-18T10:00:00Z"
  4. 前端 (浏览器/App)
    • 接收 UTC 字符串。
    • 根据用户浏览器当前时区(或用户设置的时区)进行格式化展示。
    • 显示结果:2023-12-18 18:00:00 (如果是北京用户)。

4. 为什么这个方案是最好的?

  1. 数据库算力节省:前端直接提交 UTC,Postgres 仅做存储,不做时区偏移计算。
  2. 避免配置漂移:不依赖服务器(OS)时区,也不依赖 Postgres 配置文件(postgresql.conf)里的时区。所有时间自带上下文。