全球化部署-时区方案
方案总览
-
数据库时间统一 UTC
-
JVM 强制 UTC
-
JDBC URL 显式
serverTimezone=UTC -
DB 使用
DATETIME(3) -
**Java 参考以下,看场景。 BFF,应用层,领域层,基础设施层,不同场景有差异
- BFF 负责做前端的时区转化。 BFF转换为UTC给前端, 前端给BFF需要带时区信息
- 大部分场景使用LocalDateTime(包括DAO和普通对象)
一旦涉及“时间间隔 / 是否过期 / 超时 / SLA”等判断,必须使用 Instant + Duration,不得使用 LocalDateTime。
-
前端按用户时区渲染
-
**数据库字段禁止使用 TIMESTAMP。应该使用DateTime
-
定时任务执行时间需要根据不同部署区域时间做调整,比如日切等。
-
跨系统集成,以接口提供方的时区为基准。 调用方需要根据提供方的时区做转化。
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 不接受“裸时间字符串”。
最推荐的做法只有两种:
- ISO-8601 + offset(首选)
- 时间字符串 + 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 时区设置为 UTC,JDBC 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
- 新加坡 → +8 →
六、最终推荐的“全球化时间规范”(可写入架构文档)
强烈建议你在 B 端全球销售系统中明确以下约束:
- 数据库时间统一 UTC
- JVM 强制 UTC
- JDBC URL 显式
serverTimezone=UTC - DB 使用
DATETIME(3) - Java 使用
LocalDateTime - 前端按用户时区渲染
- 禁止使用 TIMESTAMP
七、一句话总结
MySQL 设为 UTC 只是第一步,JDBC 和 JVM 不统一,时间照样会出问题。
企业级全球系统:DATETIME + 全链路 UTC,是目前最稳妥、最低心智负担的方案。
如果你愿意,我可以直接帮你:
- 输出一份《全球化时间规范(Architecture Decision Record)》
- 或评审你现有表结构 / ORM 映射是否有风险

浙公网安备 33010602011771号