只有同时说明单位、纪元、正负号规则、时钟来源和精度契约,时间戳才有意义。19 位整数经常是当代 Unix 纳秒时间戳,但位数只能作为启发式判断:纪元前日期、遥远未来、前导零、负值和自定义纪元都会破坏它。纳秒字段也不证明时钟以一纳秒的准确度测量了时间。
核心要点
- 在 API 或 Schema 中明确写出单位,例如
created_at_ns,或使用规定小数精度的 RFC 3339 字符串。 - 使用整数商和余数转换,避免对 64 位或 128 位纪元值使用浮点数。
- Unix 墙上时钟与单调时长属于不同领域。
process.hrtime.bigint()和System.nanoTime()不能转换成日历日期。 - 单位分辨率、时钟精度、准确度和事件顺序保证是不同属性。
- 交换时使用 UTC 瞬时点,展示时才使用具名 IANA 时区。不要把固定加 8 小时当作通用时区转换。
- 在边界校验范围、正负、溢出、小数余数、闰秒规则和数据库/JSON 表示。
定义时间戳契约
应记录以下信息:
| 问题 | 示例决策 |
|---|---|
| 纪元 | Unix 纪元 1970-01-01T00:00:00Z |
| 单位 | 整数纳秒 |
| 正负 | 允许纪元前负值 |
| 时间尺度 | UTC/Unix time,记录闰秒处理方式 |
| 时钟 | 日期使用墙上时钟,时长使用单调时钟 |
| 精度 | 存储位数与时钟分辨率分开 |
| 范围 | 接受的最小和最大瞬时点 |
| 序列化 | 超过消费者整数精度时使用十进制字符串 |
优先使用带单位字段名(event_time_ns、timeout_ms)或类型化 Schema。不要让每个消费者根据数值大小猜单位。
单位与整数转换
Unix 时间是从纪元开始计数的单位数:
1 秒 = 1,000 毫秒
1 毫秒 = 1,000 微秒
1 微秒 = 1,000 纳秒
把整数 n 转为瞬时点时,先选择单位,再拆分为完整秒和非负的小数余数。对于纳秒:
seconds = floor_div(n, 1_000_000_000)
nanos = floor_mod(n, 1_000_000_000)
负时间戳必须使用向下除法。直接使用向零截断的语言运算符可能得到负余数和错误瞬时点。
为什么位数判断会失败
以下值在不同契约下都可能有效:
- 在所有单位中
0都是 Unix 纪元; -1可能代表纪元前一个单位;- 1960 年日期的位数少于当前纳秒时间戳;
- 自定义纪元或设备 tick 计数可以有任意量级;
- 十进制字符串可能含前导零;
- 64 位计数器可能是经过时长,而不是墙上时间。
如果遗留接口没有单位,只能把有界启发式作为迁移辅助;多个解释都可能成立时应报歧义错误,新接口必须要求显式单位元数据。
墙上时钟、单调时钟与准确度
墙上时钟把瞬时点映射到纪元,可能因同步、管理员修改、闰秒处理、虚拟化而跳变。单调时钟用于经过时间测量,不能映射到 UTC。
const started = process.hrtime.bigint();
// 要测量的工作
const elapsedNs = process.hrtime.bigint() - started;
elapsedNs 是时长,不能传给 new Date() 或当作 Unix 时间戳展示。事件时间使用 Date.now() 或注入的墙上时钟;超时和延迟使用单调时钟。
分辨率是可表示的最小增量,精度描述重复性,准确度描述与参考时钟的接近程度。系统可以存储纳秒而物理时钟更新频率低得多。分布式事件排序还需要序列号、逻辑时钟或 trace ID,因为墙上时间可能相同或有偏差。
JavaScript:保留整数精度
IEEE 754 Number 只能安全表示到 2^53 - 1 的整数,当代纪元纳秒超过这个范围。应保留 BigInt 或十进制字符串,只转换目标 API 能表示的部分:
const NS_PER_MILLISECOND = 1_000_000n;
function epochNanosecondsToDate(ns) {
const value = typeof ns === "bigint" ? ns : BigInt(ns);
const milliseconds = value / NS_PER_MILLISECOND;
const numericMilliseconds = Number(milliseconds);
if (!Number.isSafeInteger(numericMilliseconds)) {
throw new RangeError("timestamp is outside JavaScript Date range");
}
const date = new Date(numericMilliseconds);
if (Number.isNaN(date.getTime())) {
throw new RangeError("timestamp is outside JavaScript Date range");
}
return date;
}
const instant = epochNanosecondsToDate("1706140800000000000");
console.log(instant.toISOString());
Date 只保存毫秒,会丢弃亚毫秒余数。需要保留余数时,返回 { milliseconds, nanosecondsRemainder },或保存具有规定小数位的 RFC 3339 字符串。
不要把 Date.now() * 1_000_000 称为精确当前纳秒时间;它只是把毫秒墙上时钟读数换了单位。
Python:避免浮点纪元转换
使用整数秒和纳秒,再明确附加时区:
from datetime import datetime, timezone
NANOS_PER_SECOND = 1_000_000_000
def split_epoch_nanos(value: int) -> tuple[int, int]:
seconds, nanos = divmod(value, NANOS_PER_SECOND)
return seconds, nanos
def epoch_nanos_to_utc(value: int) -> datetime:
seconds, nanos = split_epoch_nanos(value)
return datetime.fromtimestamp(seconds, tz=timezone.utc).replace(
microsecond=nanos // 1_000
)
Python datetime 只保存微秒,因此最后 0–999 纳秒不会保留。需要精确交换时应保存整数或十进制字符串。time.time_ns() 返回整数墙上时钟读数,但操作系统的分辨率和准确度可能低于一纳秒。
展示时使用 IANA 时区转换 UTC 瞬时点:
from zoneinfo import ZoneInfo
shanghai = epoch_nanos_to_utc(1706140800123456789).astimezone(
ZoneInfo("Asia/Shanghai")
)
print(shanghai.isoformat())
不要用固定偏移解决带夏令时或历史规则变化的通用时区问题。
Go 与 Java:使用原生纪元 API
Go 的 time.Unix(seconds, nanos) 接收秒和纳秒调整值;负计数应使用向下语义:
package timestamp
import "time"
const nanosPerSecond int64 = 1_000_000_000
func EpochNanosToTime(value int64) time.Time {
seconds := value / nanosPerSecond
nanos := value % nanosPerSecond
if nanos < 0 {
seconds--
nanos += nanosPerSecond
}
return time.Unix(seconds, nanos).UTC()
}
转换前应检查范围。UnixNano() 在超出 int64 表示范围时可能溢出,time.Now().UnixNano() 是墙上时间值,不是单调时长。
Java 的 Instant 同样接收秒和非负纳秒调整值:
import java.time.Instant;
public final class Timestamps {
private static final long NANOS_PER_SECOND = 1_000_000_000L;
public static Instant fromEpochNanos(long value) {
long seconds = Math.floorDiv(value, NANOS_PER_SECOND);
long nanos = Math.floorMod(value, NANOS_PER_SECOND);
return Instant.ofEpochSecond(seconds, nanos);
}
}
仅在面向人类展示时使用 ZoneId.of("Asia/Shanghai") 等 IANA 时区。不要用 String.length() 猜单位,也不要忽略负余数。
生成当前时间值
根据目的选择 API:
import time
wall_epoch_ns = time.time_ns() # 基于纪元的墙上时钟
monotonic_ns = time.monotonic_ns() # 经过时间领域
两者不可互换。超时、基准、重试预算或 span 时长应使用单调时钟;事件的大致发生时间使用纪元墙上时钟。两者都不保证物理纳秒准确度。
数据库与 API 表示
数据库的存储精度和范围会随引擎与版本变化。只保存微秒的时间列不能往返保存纳秒;BIGINT 能保留选定范围,但团队必须定义有符号、范围、单位、索引和展示转换。
JSON 没有原生 BigInt。超过消费者安全整数范围的纪元纳秒应使用带 Schema 的十进制字符串:
{
"event": "user_login",
"timestamp_ns": "1706140800123456789",
"timestamp_ms": 1706140800123
}
除非校验一致性,否则不要同时传输多个单位。边界应拒绝格式错误、越界、歧义或静默舍入的值,并记录输入是舍入、截断还是拒绝。
使用场景与边界
性能分析
使用单调时钟测量时长,并报告时钟来源、预热、采样和测量开销。墙上时间的纳秒字段不能证明函数耗时达到某个纳秒数。
日志与追踪
使用 RFC 3339 UTC 时间、trace/span ID,并在支持时记录单调时长。时钟偏差和相同时间值会使单独按墙上时间排序不可靠。
金融与科学系统
纳秒字段不等于交易顺序、结算正确性或测量准确度。应定义时间源、同步、误差、序列、校正和审计规则。金额应使用十进制定点表示,而不是二进制浮点。
数据库事件
需要排序时分开存储瞬时点和领域序列。不要只用时间戳生成唯一 ID;并发进程可能共享时间值,时钟也可能回拨。
常见问题
可以用位数识别时间戳精度吗?
不能可靠识别。它只适合有界的当代数据启发式判断。新 API 应要求显式单位,歧义时拒绝。
纳秒时间戳意味着纳秒准确度吗?
不意味着。存储分辨率、时钟分辨率、精度、准确度、同步状态和事件顺序保证是不同概念。
JavaScript Date 能保存纳秒吗?
不能。Date 保存毫秒。应使用 BigInt 或十进制字符串保存原值,并单独保留亚毫秒余数。
System.nanoTime() 和 process.hrtime.bigint() 是 Unix 时间戳吗?
不是。它们是单调时长来源,应测量耗时,不能转换为日历日期。
应如何处理时区?
传输和存储 UTC 瞬时点或明确偏移;展示时使用 IANA 具名时区。不要假设“北京时间”对所有历史或未来规则都只是固定偏移。
Unix 时间包含闰秒吗?
Unix 时间 API 和操作系统通常忽略、平滑或按其时间尺度处理闰秒。需要标注闰秒时,应记录时间尺度并使用支持该语义的领域库。
一手来源
- POSIX:Seconds Since the Epoch
- RFC 3339:Internet Date and Time
- IANA 时区数据库
- W3C Trace Context
- ECMA-262:BigInt 与 Date
- Python
time文档
总结
高精度时间戳是契约和时钟设计问题,不只是连续除以 1,000。应声明纪元和单位,使用整数运算,分离墙上时间与单调时长,只在展示时应用具名时区,在序列化中保留精度,并独立测量准确度。这样可以避免看似合理的日期掩盖错误瞬时点或无效性能结论。