时间不是一个字段:生产系统中的 UTC、时区与单调时钟
把所有时间都存成 UTC,常被当成一条万能规则。它只解决了“某件事何时发生”的一部分问题。生日不是瞬时点,“每天上午九点”不是固定秒数,30 秒超时也不该被系统时钟校准影响。生产系统首先要判断业务面对的是哪一种时间。
五种概念不能混用
- Instant:时间线上的唯一瞬时点,例如支付完成时间。
- Local date:没有时区的一天,例如生日或账单日期。
- Local date-time:墙上时钟读数,本身可能不存在或重复。
- Zoned schedule:本地时间加 IANA 时区与重复规则,例如每周一 09:00
Asia/Shanghai。 - Duration:两个动作之间经过的时间,例如超时与性能耗时。
字段名也应体现语义:created_at、birthday、starts_local_at、time_zone、timeout_ms,不要用一个模糊的 time 装下所有概念。
已发生事件用瞬时点交换
数据库可以保存 UTC instant 或 Unix 时间,API 则使用带偏移的 RFC 3339 字符串,例如:
{
"completed_at": "2026-10-20T02:32:11.418Z"
}
接收端必须拒绝没有偏移的日期时间,不能猜测它属于服务器、本机还是用户时区。输出精度也要固定:若协议约定毫秒,就不要有的服务发送三位小数、有的发送九位。内部比较使用解析后的 instant,不依赖字符串排序或展示文本。
记录原始偏移有助于审计“用户当时看到什么”,但偏移不是时区。+08:00 无法表达未来夏令时规则;CST 之类缩写还可能代表多个地区。需要本地语义时保存 Asia/Shanghai、Europe/Paris 这样的 IANA 标识。
日期不要伪装成午夜
生日、到期日和营业日应保存为日期类型或 YYYY-MM-DD。把生日转成 2000-01-01T00:00:00Z,在美洲显示时可能退回前一天。日期范围也应采用半开区间:查询 10 月 20 日的数据,用本地时区计算当天开始和次日开始对应的 instant,再执行 start <= t < end,不要拼接 23:59:59.999。
周期计划要保存意图
“每周一上午九点”意味着用户所在时区的墙上时间,不是每隔 604800 秒。保存本地时间、IANA zone、重复规则和业务对 DST 的策略,并在接近执行时计算下一次 instant。政府可能修改时区规则,IANA tzdb 也会随之更新;把未来一年全部永久展开成 UTC,规则变化后就会在错误时刻运行。
DST 向前跳会产生不存在的本地时间,向后拨会让某个时间出现两次。产品必须选择并记录策略:跳到下一个有效时间、跳过本次、选择较早或较晚的重复 instant,或者要求用户修改。库的默认值不是产品决策。RFC 9557 可以在交换格式中同时携带偏移与时区名,但服务仍要定义二者不一致时拒绝、采用哪一方或重新计算。
耗时测量使用单调时钟
墙上时钟可能被 NTP 校准、管理员修改或虚拟机恢复而跳动。请求 deadline、锁租约倒计时、重试退避和性能测量应基于单调时钟:它不应倒退,且不代表日历日期。
started = monotonic_now()
deadline = started + 30 seconds
remaining = deadline - monotonic_now()
单调时钟值只在当前系统和运行上下文内有意义,不能写进数据库后跨机器比较。持久化过期时间仍保存 wall-clock instant;进程运行期间再把它转换为剩余 duration,并处理系统休眠是否计入的产品语义。
时区数据也是依赖
IANA tzdb 描述历史与预测的民用时间规则,而未来预测会因法律变化失效。运行环境应能报告 tzdb 版本,移动端与服务端需要更新策略。发布后若规则改变,重新计算尚未执行的周期实例,并通知受影响用户;已发生事件则保留原 instant 与必要的原始时区上下文,不能用新规则改写历史事实。
跨服务排序也不能只相信时间戳。机器存在时钟偏差,两个事件还可能拥有相同精度值。业务顺序应使用数据库序列、版本号、消息 offset 或因果标识;时间戳适合展示与粗略检索,不是全局一致性协议。
围绕边界建立测试
测试样本至少覆盖 DST 跳过与重复时刻、月底和闰日、负偏移与非整点偏移、跨午夜区间、tzdb 更新、系统时钟向前与向后跳、休眠后 deadline,以及客户端发送无偏移时间。固定测试用时区和时钟,避免依赖执行机器的当前设置。
上线前逐项确认:每个字段属于 instant、日期、本地时间、计划还是 duration;瞬时点使用带偏移的标准格式;未来本地规则保存 IANA zone;DST 歧义有产品策略;耗时用单调时钟;tzdb 可更新且可观测;业务顺序不依赖墙上时钟;边界测试使用可控时钟。
正确的时间设计不是统一成一种类型,而是保留业务意图。UTC 表达已经发生的瞬时点,时区解释人类日历,单调时钟测量经过时间,序列标识负责一致性顺序。边界清楚之后,时间才不会在跨地区和长期运行中悄悄改变含义。