只有同时说明单位、纪元、正负号规则、时钟来源和精度契约,时间戳才有意义。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_nstimeout_ms)或类型化 Schema。不要让每个消费者根据数值大小猜单位。

单位与整数转换

Unix 时间是从纪元开始计数的单位数:

text
1 秒      = 1,000 毫秒
1 毫秒    = 1,000 微秒
1 微秒    = 1,000 纳秒

把整数 n 转为瞬时点时,先选择单位,再拆分为完整秒和非负的小数余数。对于纳秒:

text
seconds = floor_div(n, 1_000_000_000)
nanos   = floor_mod(n, 1_000_000_000)

负时间戳必须使用向下除法。直接使用向零截断的语言运算符可能得到负余数和错误瞬时点。

为什么位数判断会失败

以下值在不同契约下都可能有效:

  • 在所有单位中 0 都是 Unix 纪元;
  • -1 可能代表纪元前一个单位;
  • 1960 年日期的位数少于当前纳秒时间戳;
  • 自定义纪元或设备 tick 计数可以有任意量级;
  • 十进制字符串可能含前导零;
  • 64 位计数器可能是经过时长,而不是墙上时间。

如果遗留接口没有单位,只能把有界启发式作为迁移辅助;多个解释都可能成立时应报歧义错误,新接口必须要求显式单位元数据。

墙上时钟、单调时钟与准确度

墙上时钟把瞬时点映射到纪元,可能因同步、管理员修改、闰秒处理、虚拟化而跳变。单调时钟用于经过时间测量,不能映射到 UTC。

javascript
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 能表示的部分:

javascript
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:避免浮点纪元转换

使用整数秒和纳秒,再明确附加时区:

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 瞬时点:

python
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) 接收秒和纳秒调整值;负计数应使用向下语义:

go
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 同样接收秒和非负纳秒调整值:

java
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:

python
import time

wall_epoch_ns = time.time_ns()       # 基于纪元的墙上时钟
monotonic_ns = time.monotonic_ns()   # 经过时间领域

两者不可互换。超时、基准、重试预算或 span 时长应使用单调时钟;事件的大致发生时间使用纪元墙上时钟。两者都不保证物理纳秒准确度。

数据库与 API 表示

数据库的存储精度和范围会随引擎与版本变化。只保存微秒的时间列不能往返保存纳秒;BIGINT 能保留选定范围,但团队必须定义有符号、范围、单位、索引和展示转换。

JSON 没有原生 BigInt。超过消费者安全整数范围的纪元纳秒应使用带 Schema 的十进制字符串:

json
{
  "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 和操作系统通常忽略、平滑或按其时间尺度处理闰秒。需要标注闰秒时,应记录时间尺度并使用支持该语义的领域库。

一手来源

总结

高精度时间戳是契约和时钟设计问题,不只是连续除以 1,000。应声明纪元和单位,使用整数运算,分离墙上时间与单调时长,只在展示时应用具名时区,在序列化中保留精度,并独立测量准确度。这样可以避免看似合理的日期掩盖错误瞬时点或无效性能结论。