全球化部署-时区方案

方案总览

  1. 数据库时间统一 UTC

  2. JVM 强制 UTC

  3. JDBC URL 显式 serverTimezone=UTC

  4. DB 使用 DATETIME(3)

  5. **Java 参考以下,看场景。 BFF,应用层,领域层,基础设施层,不同场景有差异

    • BFF 负责做前端的时区转化。 BFF转换为UTC给前端, 前端给BFF需要带时区信息
    • 大部分场景使用LocalDateTime(包括DAO和普通对象)
      一旦涉及“时间间隔 / 是否过期 / 超时 / SLA”等判断,必须使用 Instant + Duration,不得使用 LocalDateTime。
  6. 前端按用户时区渲染

  7. **数据库字段禁止使用 TIMESTAMP。应该使用DateTime

  8. 定时任务执行时间需要根据不同部署区域时间做调整,比如日切等。

  9. 跨系统集成,以接口提供方的时区为基准。 调用方需要根据提供方的时区做转化。

JAVA四层时区规范

基础设施层

DAO的BO对象,使用LocalDateTime (因为JVM和DB都是UTC。如果使用带时区的对象反而让ORM,JDBC驱动更麻烦)

领域层

场景 类型
业务时间(下单时间等) LocalDateTime(UTC)
时间间隔 / 过期判断 Instant / Duration

一旦涉及“时间间隔 / 是否过期 / 超时 / SLA”等判断,必须使用 Instant + Duration,不得使用 LocalDateTime。

过期判断例子支付超时(15 分钟)

public class Order {

    private Instant createdAt;   // 绝对时间
    private OrderStatus status;

    public boolean isPaymentExpired(Instant now) {
        return now.isAfter(createdAt.plus(Duration.ofMinutes(15)));
    }
}

示例 2:订阅有效期(30 天)

public class Subscription {

    private Instant startAt;

    public boolean isExpired(Instant now) {
        return now.isAfter(startAt.plus(Duration.ofDays(30)));
    }
}

注意:
“30 天”是 Duration,不是日历月份

应用层

同领域层

应用层向 BFF 暴露时间字段的类型不是固定的
是否需要做时间计算 / 比较 / 过期判断 是关键决定因素

场景 暴露给 BFF 的时间类型 说明
业务逻辑需要做比较 / 过期判断 / 间隔计算 Instant 绝对时间点,方便跨时区计算,不依赖 JVM 默认时区
仅用于记录 / 展示 / 回传 DB LocalDateTime(约定 UTC) 只是存储或传输,BFF 可以再做时区转换

BFF

前端展示(用户)推荐的 userZone 决策优先级(可直接落地)

1.用户 Profile 中配置的 ZoneId(最高优先级)
2.请求 Header(前端显式传,如 X-Time-Zone)
3.登录态 / Session 中缓存
4.地区默认(国家 → ZoneId 映射)
5. UTC(兜底)

BFF返回日期属性给前端

BFF代码

@RestController
public class OrderController {

    public OrderResponse getOrder(...) {

        OrderDTO dto = orderAppService.getOrder(...);

        ZoneId userZone = timeZoneResolver.resolve();

        ZonedDateTime createdAt =
            dto.getCreatedAtUtc()
               .atZone(ZoneOffset.UTC)
               .withZoneSameInstant(userZone);

        return new OrderResponse(
            dto.getOrderId(),
            createdAt.toString()
        );
    }
}

前端传时间给BFF

前端用户输入的时间,必须同时携带「时间值 + 时区语义」,
BFF 不接受“裸时间字符串”。

最推荐的做法只有两种:

  1. ISO-8601 + offset(首选)
  2. 时间字符串 + ZoneId(可接受)

三、推荐方案一(最优):ISO-8601 + offset

1️⃣ 前端怎么传(日期控件)

用户在 Asia/Shanghai 选:

2025-03-10 10:00

前端生成:

{
  "deliveryTime": "2025-03-10T10:00:00+08:00"
}

2️⃣ BFF 怎么处理

OffsetDateTime odt = OffsetDateTime.parse(req.getDeliveryTime());
Instant instant = odt.toInstant(); // 绝对时间

3️⃣ 传给应用层

Instant deliveryAt; 或者 localDateTime ,主要看应用层的协议约定。

一、MySQL 设置为 UTC 后,JDBC URL 还需要特殊处理吗?

结论:需要,而且是强烈建议显式配置。

即便你已经把 MySQL Server 时区设置为 UTCJDBC URL 仍然必须显式声明时区与时间行为,否则在跨区域部署时非常容易踩坑。

1. 推荐 JDBC URL 配置(MySQL 8.x)

jdbc:mysql://host:3306/db
?useUnicode=true
&characterEncoding=utf8
&useSSL=false
&serverTimezone=UTC

2. 为什么 serverTimezone=UTC 仍然必须?

原因在于 MySQL JDBC Driver 的时间解析机制

  • JDBC 驱动在 反序列化 DATETIME / TIMESTAMP

  • 并不总是信任 MySQL server 的 global time_zone

  • 如果不显式指定:

    • 可能使用 JVM 默认时区
    • 或触发 The server time zone value ... is unrecognized 警告
    • 不同节点(东南亚 / 欧洲)行为可能不一致

结论一句话

MySQL 是 UTC ≠ JDBC 一定按 UTC 解析


二、Java 进程需要做哪些统一时区配置?

1. JVM 层:强烈建议统一 UTC

-Duser.timezone=UTC

或者在容器 / 启动脚本中:

export TZ=UTC

2. 为什么 JVM 时区必须统一?

如果 JVM 使用本地时区(如 Asia/Singapore):

  • new Date()
  • LocalDateTime.now()
  • ORM(Hibernate / MyBatis)默认行为

都会隐式依赖 JVM 时区

即使 DB 是 UTC,JVM 不是 UTC,照样会发生时间漂移


三、时间戳 / DATETIME / TIMESTAMP,到底用哪个?

这是全球化系统的关键设计点


1. MySQL 的三种时间类型对比

类型 是否带时区 存储含义 风险
TIMESTAMP UTC → 会随 session time_zone 转换 多区域易混乱
DATETIME 纯字面值 推荐
BIGINT(epoch) 隐式 UTC 时间戳 可用,但可读性差

2. 推荐方案(企业级主流)

方案 A:DATETIME + 全链路 UTC(最推荐)

created_at DATETIME(3) NOT NULL
updated_at DATETIME(3) NOT NULL

约束:

  • DB:UTC
  • JVM:UTC
  • JDBC:UTC
  • 前端展示:按用户时区转换

优点:

  • 可读性好
  • 无 MySQL session time_zone 副作用
  • 和 Java LocalDateTime 完美匹配
  • 多区域部署稳定

⚠️ 方案 B:BIGINT 时间戳(可接受)

created_at BIGINT NOT NULL -- milliseconds since epoch

适用场景:

  • 高并发写入
  • 对时间排序 / 比较极端敏感
  • 事件流、日志、埋点系统

缺点:

  • SQL 可读性差
  • BI / 报表友好度低

❌ 不推荐:TIMESTAMP

TIMESTAMP 最大的问题不是 UTC,而是“自动时区转换”

在以下场景极易出错:

  • 不同 MySQL Session time_zone
  • CDC / binlog
  • 数据迁移
  • 多语言客户端

四、Java 实体层的正确写法(非常关键)

1. 推荐映射

DB 类型 Java 类型
DATETIME LocalDateTime
BIGINT Instant / long

不要混用 Date / Calendar(历史包袱)


2. 示例(MyBatis / JPA 通用)

@Column(name = "created_at")
private LocalDateTime createdAt;

生成时间:

LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);

五、前端 & 业务层的时区策略(很多系统忽略)

核心原则:

存储 / 计算统一 UTC,展示按用户时区

示例:

  • 数据库存:2025-03-10 12:00:00(UTC)

  • 用户在:

    • 新加坡 → +8 → 20:00
    • 巴西 → -3 → 09:00

六、最终推荐的“全球化时间规范”(可写入架构文档)

强烈建议你在 B 端全球销售系统中明确以下约束:

  1. 数据库时间统一 UTC
  2. JVM 强制 UTC
  3. JDBC URL 显式 serverTimezone=UTC
  4. DB 使用 DATETIME(3)
  5. Java 使用 LocalDateTime
  6. 前端按用户时区渲染
  7. 禁止使用 TIMESTAMP

七、一句话总结

MySQL 设为 UTC 只是第一步,JDBC 和 JVM 不统一,时间照样会出问题。
企业级全球系统:DATETIME + 全链路 UTC,是目前最稳妥、最低心智负担的方案。

如果你愿意,我可以直接帮你:

  • 输出一份《全球化时间规范(Architecture Decision Record)》
  • 或评审你现有表结构 / ORM 映射是否有风险
posted @ 2025-12-25 20:12  向着朝阳  阅读(5)  评论(0)    收藏  举报