UUID v1生成器

Generate time-based UUID v1 with embedded timestamp

格式化

数量:
Note:UUID v1 嵌入了主机 MAC 地址和生成时间戳,对于大多数现代应用程序,这会引发隐私问题。对于新项目,建议使用 UUID v4,除非你特别需要时间排序、可解码的标识符。

什么是 UUID v1?

UUID v1 是原始的 UUID 版本,在 RFC 4122(2005 年)中标准化。它通过将高精度时间戳与生成主机的 MAC 地址以及用于处理亚时间戳分辨率的短时钟序列组合来生成唯一标识符。

由于嵌入了时间戳,来自同一主机的 UUID v1 值随时间单调递增——使它们自然有序。这是为分布式系统设计的,其中每个节点可以独立生成 UUID 而无需协调。

今天 UUID v1 在很大程度上被 UUID v7(可排序、无 MAC 泄露)和 UUID v4(完全随机、私密)所取代。它仍然在 Apache Cassandra 和遗留分布式数据库等系统中使用。

UUID v1 的结构

550e8400-e29b-11d4-a716-446655440000 这样的 UUID v1 字符串编码了六个不同的字段:

字段大小描述
time_low32 bits60 位格里高利时间戳的 32 位低字段(自 1582 年 10 月 15 日以来的 100 纳秒间隔)
time_mid16 bits60 位时间戳的中间 16 位字段
time_hi_and_version16 bits60 位时间戳的最高 12 位加 4 位版本号(始终为 <code>1</code>)
clock_seq_hi_res8 bits6 位时钟序列高字段与 2 位 RFC 4122 变体标记组合
clock_seq_low8 bits时钟序列的低 8 位
node48 bits48 位节点标识符——通常是生成网络接口的 MAC 地址,如果没有 MAC 可用则为随机 48 位值

时钟序列字段(clock_seq_hi_res + clock_seq_low)是一个 14 位计数器。每当系统时钟向后移动(例如 NTP 调整)或系统在未持久化最后已知时间戳的情况下重启时,它就会递增。这防止了在时钟不单调前进时生成重复的 UUID。

解码 UUID v1 时间戳

60 位时间戳分散在 UUID 的三个字段中。要重建生成时间:

  1. 提取 time_low(字节 0–3)、time_mid(字节 4–5)和 time_hi(字节 6–7,减去版本半字节)
  2. 重新组装:(time_hi &lt;&lt; 48) | (time_mid &lt;&lt; 32) | time_low
  3. 结果是自 1582 年 10 月 15 日(格里高利历纪元)以来 100 纳秒间隔的 60 位计数
  4. 减去格里高利到 Unix 的偏移量:122,192,928,000,000,000(1582 年 10 月 15 日至 1970 年 1 月 1 日之间的 100 纳秒间隔)
  5. 除以 10,000 将 100 纳秒间隔转换为毫秒
  6. 将结果用作 Unix 毫秒时间戳来构造 Date 对象
  7. 格式化为 ISO 8601 以获得人类可读的输出

时间戳精度为 100 纳秒——远比 UUID v7 的毫秒精度更细。但是,在实践中大多数操作系统不提供亚毫秒时钟分辨率,所以较低的位通常为零或合成值。

隐私问题

UUID v1 最显著的缺点是它在节点字段中嵌入了生成主机的 MAC 地址。这意味着每个 UUID v1 都携带着生成它的机器的永久、全球唯一的指纹。

获得 UUID v1 的攻击者可以确定:(1) ID 生成的大致时间,(2) 生成主机的 MAC 地址,以及 (3) 通过分析多个 UUID,ID 的生成速率。

因此,UUID v1 不应作为面向公众的标识符使用(例如在 URL 或 API 响应中),除非你愿意公开这些信息。RFC 4122 本身指出,系统可以使用随机 48 位值代替 MAC 地址,但许多实现并不这样做。

UUID v1 仍然适用的场景

Apache Cassandra 主键
Cassandra 使用 UUID v1(通过 TimeUUID 类型)作为核心设计模式。时间戳排序自然映射到 Cassandra 的存储模型,实现高效的时间范围查询。
遗留分布式系统
在 UUID v7 出现之前(2024 年前)构建的系统依赖于时间戳排序的 UUID,且无法轻易迁移到新格式。
审计和事件日志
当生成主机身份已知且受信任时,嵌入 MAC 地址可为审计事件提供额外的可追溯性。
内部标识符
从未在受控内部系统之外暴露的 ID,在这种情况下 MAC 地址披露不是问题。
不使用单独时间戳列的时间范围查询
嵌入的时间戳可以被解码以按生成时间筛选行,充当组合 ID 和时间戳的作用。
与旧版 UUID v1 生成器的互操作性
在接收或处理来自生成它们的外部系统的 UUID v1 值时,解码嵌入的时间戳用于显示或分析。

UUID v1 vs UUID v7

UUID v7 是时间排序标识符的 UUID v1 的现代继任者。以下是直接对比:

方面UUID v1UUID v7
纪元 / 时间基准格里高利纪元(1582 年 10 月 15 日)Unix 纪元(1970 年 1 月 1 日)
精度100 纳秒1 毫秒
节点标识符MAC 地址(泄露主机身份)随机(私密)
隐私泄露 MAC 地址和生成时间戳不嵌入主机信息
数据库索引性能良好——每台主机顺序排列极佳——跨所有生成器 k 可排序
标准RFC 4122(2005 年)RFC 9562(2024 年)

对于新项目,UUID v7 是 UUID v1 的推荐替代品。它提供类似的时间排序保证,而不会有嵌入主机 MAC 地址的隐私影响。

代码示例

UUID v1 生成在浏览器或 Node.js 中原生不可用。使用 uuid npm 包:

JavaScript (browser)
// Generate a UUID v1 using the Web Crypto API
function generateUuidV1() {
  const buf = new Uint8Array(16)
  crypto.getRandomValues(buf)

  const ms = BigInt(Date.now())
  const gregorianOffset = 122192928000000000n
  const t = ms * 10000n + gregorianOffset

  const tLow   = Number(t & 0xFFFFFFFFn)
  const tMid   = Number((t >> 32n) & 0xFFFFn)
  const tHiVer = Number((t >> 48n) & 0x0FFFn) | 0x1000  // version 1

  const clockSeq    = (buf[8] & 0x3F) | 0x80  // variant 10xxxxxx
  const clockSeqLow = buf[9]

  const hex  = (n, pad) => n.toString(16).padStart(pad, '0')
  const node = [...buf.slice(10)].map(b => b.toString(16).padStart(2, '0')).join('')

  return `${hex(tLow,8)}-${hex(tMid,4)}-${hex(tHiVer,4)}-${hex(clockSeq,2)}${hex(clockSeqLow,2)}-${node}`
}

// Extract the embedded timestamp from a UUID v1
function extractTimestamp(uuid) {
  const parts = uuid.split('-')
  const tHex = parts[2].slice(1) + parts[1] + parts[0]
  const t = BigInt('0x' + tHex)
  const ms = (t - 122192928000000000n) / 10000n
  return new Date(Number(ms))
}

const id = generateUuidV1()
console.log(id)                      // e.g. "1eb5e8b0-6b4d-11ee-9c45-a1f2b3c4d5e6"
console.log(extractTimestamp(id))    // e.g. 2023-10-15T12:34:56.789Z
Python
import uuid
from datetime import datetime, timezone

# Generate UUID v1 (uses MAC address by default)
uid = uuid.uuid1()
print(uid)

# Extract embedded timestamp
# uuid.time is 100-ns intervals since Oct 15, 1582
GREGORIAN_OFFSET = 122192928000000000  # 100-ns intervals
ts_100ns = uid.time
ts_ms = (ts_100ns - GREGORIAN_OFFSET) // 10000
dt = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
print(dt.isoformat())   # e.g. "2023-10-15T12:34:56.789000+00:00"
Go
package main

import (
    "fmt"
    "time"

    "github.com/google/uuid"  // go get github.com/google/uuid
)

func main() {
    id, _ := uuid.NewUUID()  // UUID v1
    fmt.Println(id)

    // Extract timestamp from UUID v1
    // uuid.Time is 100-ns ticks since Oct 15, 1582
    t := id.Time()
    sec  := int64(t)/1e7 - 12219292800  // convert to Unix seconds
    nsec := (int64(t) % 1e7) * 100
    ts   := time.Unix(sec, nsec).UTC()
    fmt.Println(ts.Format(time.RFC3339Nano))
}

常见问题

我可以从 UUID v1 解码时间戳吗?
可以。生成时间戳可以从 UUID v1 字符串中完整恢复。此工具正是这样做的——粘贴任何 UUID v1,它将显示解码的 UTC 时间戳。有关算法,请参阅上面的解码步骤。
UUID v1 中始终存在 MAC 地址吗?
不一定。RFC 4122 允许实现在没有网络接口可用或需要隐私时,用随机生成的 48 位值替换 MAC 地址。实际上,许多服务器端实现确实嵌入了真实的 MAC 地址。浏览器生成的 UUID v1 值始终使用随机节点值,因为浏览器不暴露 MAC 地址。
为什么 UUID v1 时间戳使用 1582 年作为纪元?
格里高利历改革于 1582 年 10 月 15 日生效。UUID v1 时间戳被定义为相对于此日期,以提供一个早于 Unix 纪元(1970 年)的稳定、通用参考点。这给 60 位时间戳字段足够的范围,使其保持唯一直到大约公元 3400 年。
UUID v1 vs UUID v7——我什么时候仍应使用 v1?
今天使用 UUID v1 的主要原因是与现有系统的兼容性——特别是 Apache Cassandra,它使用 v1 作为其 TimeUUID 类型。对于所有新系统,UUID v7 严格更优:它使用更熟悉的 Unix 纪元,没有 MAC 地址泄露,并提供更好的 B 树索引性能。
UUID v1 值会碰撞吗?
理论上,如果相同的 MAC 地址在同一个 100 纳秒间隔内生成两个 UUID 且时钟序列相同,两个 UUID v1 值可能会碰撞。时钟序列的存在正是为了防止这种情况——它在快速连续调用时递增。实际上,在正确实现的系统中 UUID v1 碰撞极为罕见。