ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

oklog/ulid v2 演进实录:从变更日志读懂 ULID 的时间、熵与单调性设计

oklog/ulid v2 演进实录:从变更日志读懂 ULID 的时间、熵与单调性设计 人工智能AI AgentAgent 沙箱云原生容器运行时零信任【免费下载链接】substrateAgent Substrate: the core system项目地址https://gitcode.com/GitHub_Trending/substrate7/substrate点击查看免费下载本仓库 vendor/github.com/oklog/ulid/v2 目录下内置了 Go 语言 ULIDUniversally Unique Lexicographically Sortable Identifier通用唯一字典序可排序标识符库的完整源码与文档。本文以其 CHANGELOG.md 为骨架逐条拆解每个版本变更背后的设计动机与实现原理并结合 README.md 规范与 ulid.go 源码讲透 ULID 的 128 位二进制布局、48 位毫秒时间戳、80 位熵、Crockford Base32 编码以及单调熵Monotonic Entropy机制。读完本文你将掌握 ULID 与 UUID 的本质差异、如何用ulid.Make/ulid.New生成 ID、如何用ParseStrict严格解析、如何用MonotonicEntropy保证同一毫秒内的排序性以及如何在数据库中直接存取 ULID。ULID 是什么为什么 UUID 在某些场景下不够好ULID 是一个 16 字节128 位的标识符设计目标是在保留 UUID 兼容性的同时提供字典序可排序和时间可回溯的能力。根据 README.md 的说明GUID/UUID 在不少场景下是次优选择它不是编码 128 位信息最高效的字符方式36 字符 vs ULID 的 26 字符UUID v1/v2 依赖唯一且稳定的 MAC 地址在很多环境不实用UUID v3/v5 需要唯一种子且产生随机分布 ID容易造成数据结构碎片化UUID v4 除随机性外不携带任何信息同样容易造成索引与数据结构碎片化。ULID 则具备如下特性兼容 UUID/GUID每个毫秒内理论上有 1.21e24 个唯一 ULID字典序可排序规范编码为 26 字符对比 UUID 的 36 字符使用 Crockford Base32每字符 5 比特效率与可读性更高大小写不敏感无特殊字符URL 安全具有单调排序保证能正确处理同一毫秒内的并发生成。从 ulid.go 源码中的类型注释可以看到其整体布局ULID 本质上就是type ULID [16]byte即 16 个字节的网络字节序MSB first二进制块。CHANGELOG 全景v0.1.0 到 v1.3.1 的版本脉络仓库内置的 CHANGELOG.md 记录了从 2016 年首发到 2018 年 1.3.1 的全部演进。整体脉络如下版本日期核心变更1.3.12018-10-02单调递增使用底层熵源生成随机增量#321.3.02018-09-29单调熵支持#311.2.02018-09-09新增把毫秒 Unix 时间转回time.Time的函数#301.1.02018-08-15确保随机部分总是从熵源完整读取#281.0.02018-07-29新增ParseStrict与MustParseStrict#26解析时强制溢出检查#200.3.02017-01-03实现ULID.Compare方法0.2.02016-12-13移除 2262 年时间戳 bug#1解析时优雅处理非法编码0.1.02016-12-06首个 ULID 版本下面逐节展开每一个变更的技术内涵。二进制布局与时间戳设计v0.2.0移除 2262 年 bug48 位时间戳为什么能用到公元 10889 年ULID 的前 6 字节48 位存放 Unix 毫秒时间戳。根据 README.md 的规范Timestamp48 位Unix 毫秒时间直到公元 10889 年都不会耗尽Entropy80 位用户定义的熵源。v0.2.0 的变更 Remove year 2262 Timestamp bug (#1) 是早期最重要的修复之一初版实现中时间戳只能表示到 2262 年原因是时间计算方式存在缺陷修复后正确支持到 10889 年。这一点在 ulid.go 中体现为maxTime常量var maxTime ULID{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}.Time()即 6 字节全 1 时对应的时间MaxTime()返回该值。时间戳相关的四个核心函数在 ulid.goNow()返回当前 UTC 时间的 Unix 毫秒数等价于Timestamp(time.Now().UTC())Timestamp(t time.Time) uint64将time.Time转为毫秒时间戳实现为t.Unix()*1000 t.Nanosecond()/int(time.Millisecond)Time(ms uint64) time.Time将毫秒时间戳转回time.Time这正是 v1.2.0 变更 Add a function to convert Unix time in milliseconds back to time.Time (#30) 引入的能力ULID.Time()与ULID.Timestamp()分别返回 ULID 中编码的毫秒值与其对应的time.Time。时间/熵的字节划分按 README.md 中的二进制布局图16 字节按网络字节序划分0 1 2 3 -------------------------------- | 32_bit_uint_time_high | -------------------------------- | 16_bit_uint_time_low | 16_bit_uint_random | -------------------------------- | 32_bit_uint_random | -------------------------------- | 32_bit_uint_random | --------------------------------即前 6 字节是时间后 10 字节是熵。SetTimeulid.go通过右移 40/32/24/16/8/0 位把毫秒值逐字节写入前 6 字节并在ms maxTime时返回ErrBigTime。字符串表示26 字符与 Crockford Base32 编码ULID 的规范字符串形式为 26 个字符前 10 个字符是时间戳48 位后 16 个字符是熵80 位01AN4Z07BY 79KA1307SR9X4MV3 |----------| |----------------| Timestamp Entropy 10 chars 16 chars 48bits 80bits base32 base32编码使用Crockfords Base32字母表刻意排除了字母 I、L、O、U 以避免混淆和滥用0123456789ABCDEFGHJKMNPQRSTVWXYZ这一字母表在 ulid.go 中定义为常量Encoding。编码实现采用来自 NUlid 的优化展开循环ulid.goMarshalTextTo用位掩码逐字符生成String()即调用它解码侧parse则配合 256 字节的dec查表实现 O(1) 字符到索引的映射0xFF 作为非法字符哨兵值ulid.go。26 个字符长度由常量EncodedSize 26定义ulid.go。解析与校验v1.0.0 的 ParseStrict 与溢出检查v1.0.0 是首个正式版本带来了两个关键能力对应 CHANGELOG.mdAdd ParseStrict and MustParseStrict functions (#26)Enforce overflow checking when parsing (#20)。宽松解析与严格解析Parseulid.go只做长度校验长度不等于 26 就返回ErrDataSize非法编码会产生未定义的 ULID不报错。ParseStrictulid.go则在parse内部额外检查 26 个字符是否全部落在合法 base32 字符集内任一字符查表为 0xFF 即返回ErrInvalidCharacters由于多一轮字符校验它比Parse略慢。对应的MustParse/MustParseStrict是解析失败即 panic 的便捷版本。溢出检查的原理parse中有一段关键逻辑ulid.go// Check if the first character in a base32 encoded ULID will overflow. This // happens because the base32 representation encodes 130 bits, while the // ULID is only 128 bits. if v[0] 7 { return ErrOverflow }由于 26 个 base32 字符实际编码了 130 位信息26 × 5而 ULID 只有 128 位若首字符大于 7 就会超出有效位深。v1.0.0 之前该情况未被检测v1.0.0 起解析时强制检查并返回ErrOverflow。这一细节对依赖解析结果的系统很重要宽松解析下越界输入会产生无法预期的字节内容严格模式则保证解析结果一定合法。此外 v0.2.0 的 Gracefully handle invalid encodings when parsing 已先行保证了解析遇到非法字符时不会 panic而是返回错误。熵源与 APINew / MustNew / Make 的取舍熵源是 io.Reader自由选择安全性与性能ULID 的时间戳是毫秒级的同毫秒内的唯一性完全依赖 80 位熵。库的设计将熵源抽象为io.Reader让调用方自己权衡README.mdentropy : rand.New(rand.NewSource(time.Now().UnixNano())) ms : ulid.Timestamp(time.Now()) fmt.Println(ulid.New(ms, entropy)) // 01G65Z755AFWAKHE12NY0CQ9FH安全敏感场景应使用crypto/rand提供的密码学安全熵源性能敏感场景应避免生成时的锁竞争可为每个并发 goroutine 分配独立熵源或用sync.Pool池化熵源换取无锁、高吞吐但牺牲强随机性保证与毫秒内单调性math/rand.Rand不保证并发安全可考虑golang.org/x/exp/rand的LockedSource。New(ms uint64, entropy io.Reader)ulid.go会先SetTime然后分三种情况处理熵nil直接返回仅时间戳实现了MonotonicReader接口则调用MonotonicRead(ms, id[6:])否则用io.ReadFull从熵源完整读取 10 字节。v1.1.0 的变更 Ensure random part is always read from the entropy reader in full (#28) 正是针对这一环节此前若底层io.Reader的Read未一次读满 10 字节短读随机部分就会残缺。修复后统一使用io.ReadFull保证 80 位熵总是被完整填充——这与New中的io.ReadFull(e, id[6:])一一对应。Make开箱即用的进程级默认熵ulid.Make()ulid.go是快速上手入口fmt.Println(ulid.Make()) // 01G65Z755AFWAKHE12NY0CQ9FH其实现为MustNew(Now(), defaultEntropy)。defaultEntropyulid.go是包初始化时构建的进程全局熵源由math/rand播种time.Now().UnixNano()生成Monotonic单调熵再用LockedMonotonicReader加互斥锁包装因此Make()是进程级线程安全的底层通过sync.Mutex串行化同一毫秒内的生成。注释也说明底层利用了sync.Pool思想保持低竞争。单调熵v1.3.0 / v1.3.1 的核心机制问题同一毫秒内的 ULID 天然无序ULID 天然具备毫秒级时间顺序时间戳大的 ULID 字典序更大。但同一毫秒内生成的多个 ULID 只按随机熵排序默认是无序的。对于数据库主键、日志 ID、事件流等强排序场景需要同一毫秒内也递增的保证。v1.3.0 的 Monotonic entropy support (#31) 正是为此引入ulid.MonotonicEntropy与ulid.LockedMonotonicEntropyv1.3.1 的 Use underlying entropy source for random increments in Monotonic (#32) 进一步优化了增量生成的随机性来源。实现原理uint80 大数递增Monotonic(entropy io.Reader, inc uint64)ulid.go构造一个MonotonicEntropy内部用 80 位大数类型uint80Hi 为 uint16Lo 为 uint64见 ulid.go保存上一次的熵值。其工作流程MonotonicReadulid.go若当前 ULID 时间戳ms与上一次相同且已有熵值则对熵做一次随机增量递增increment并把新熵写入目标字节若ms变化进入新的毫秒则直接从底层熵源读取全新的 10 字节随机熵并缓存为新的基准。incrementulid.go调用random()生成一个[1, inc]区间的随机增量加到uint80上若加法溢出80 位空间耗尽则返回ErrMonotonicOverflow。uint80.Add用Lo n; if Lo lo { Hi }处理 64 位进位并返回Hi hi判断整体溢出。随机增量与 inc 参数的权衡v1.3.1 之前增量生成完全依赖包内的math/rand逻辑v1.3.1 之后如果底层熵源本身就是math/rand.Rand实现了Int63n接口即代码中的rng接口则直接用底层熵源生成增量减少一层包装ulid.go。inc参数控制单次递增的上限ulid.goinc 0时取默认值math.MaxUint32这是推荐的安全默认单毫秒内容纳的单调序列最长、熵的可猜测性最低inc越小同一毫秒内可用单调熵的步数越多但 ULID 熵更易被猜测若依赖 ULID 熵的保密性请保持默认值。并发安全LockedMonotonicReaderMonotonicEntropy本身不并发安全。LockedMonotonicReaderulid.go用sync.Mutex包装使Make()/DefaultEntropy()在并发下安全——这正是ulid.Make()可直接多 goroutine 使用的底层保证。排序与比较v0.3.0 的 Compare 与数据库集成Compare 与零值判断v0.3.0 实现ULID.Compare方法ulid.go基于bytes.Compare逐字节比较返回 0 表示相等、-1 表示小于、1 表示大于。由于时间戳在高位这一比较天然等价于时间先后 熵大小的字典序。IsZero()通过Compare(Zero) 0判断 ULID 是否为零值ulid.go。sql.Scanner 与 driver.Valuerulid.go 实现了数据库接口Scan(src interface{})支持从nil、string走UnmarshalText和[]byte读取字节切片同时兼容 16 字节二进制形式与 26 字符文本形式其余长度返回ErrDataSize非字符串/字节类型返回ErrScanValueValue()默认返回MarshalBinary()的 16 字节二进制形式若需文本形式README 建议用包装类型调用String()。此外MarshalBinary/UnmarshalBinary、MarshalText/UnmarshalText使 ULID 可直接用于各类 Go 序列化框架JSON 等文本序列化会自动走MarshalText得到 26 字符字符串。命令行工具生成与解析 ULID仓库同时提供命令行工具README.md可在终端生成与解析 ULIDgo install github.com/oklog/ulid/v2/cmd/ulidlatest用法Usage: ulid [-hlqz] [-f format] [parameters ...] -f, --formatformat when parsing, show times in this format: default, rfc3339, unix, ms -h, --help print this help text -l, --local when parsing, show local time instead of UTC -q, --quick when generating, use non-crypto-grade entropy -z, --zero when generating, fix entropy to all-zeroes示例$ ulid 01D78XYFJ1PRM1WPBCBT3VHMNV $ ulid -z 01D78XZ44G0000000000000000 $ ulid 01D78XZ44G0000000000000000 Sun Mar 31 03:51:23.536 UTC 2019 $ ulid --formatrfc3339 --local 01D78XZ44G0000000000000000 2019-03-31T05:51:23.53602:00注意-z将所有熵固定为 0仅用于演示时间戳解析-q使用非密码学熵换取生成速度。该工具位于仓库的 vendor/github.com/oklog/ulid/v2 目录对应的上游项目中本仓库仅内置库源码ulid.go与文档未包含 cmd 子目录。性能特性与测试方式README.md 给出了上游基准测试数据Intel Core i7 Ivy Bridge 2.7 GHz、Go 1.8.0beta1 环境下带加密熵的New约 771 ns/op普通熵New约 65.8 ns/op无熵仅时间戳约 30.0 ns/opParse约 30.0 ns/op0 分配String约 64.9 ns/op1 次分配二进制 Marshal/Unmarshal 为个位数 ns/op 级别。性能优势主要来自三点源码设计编码/解码的展开循环unrolled loop、256 字节查表dec、以及MarshalBinaryTo/MarshalTextTo这类零分配写入目标缓冲的接口。这些基准数据来自上游文档具体数据会随硬件与 Go 版本变化仅作量级参考。在本地测试可运行go test ./...在仓库根目录下执行会连同本 vendor 目录一并验证编译。小结从 CHANGELOG 读懂 ULID 的设计取舍回看 CHANGELOG.md 的七次发布可以清晰地看到 ULID 库的能力演进主线正确性优先v0.2.0修复 2262 年时间戳 bug解析非法编码不再 panic可排序性补齐v0.3.0Compare让 ULID 可直接参与排序解析安全加固v1.0.0ParseStrict严格校验字符集解析时强制 128 位溢出检查熵读取可靠性v1.1.0io.ReadFull保证 80 位熵完整读取时间往返能力v1.2.0毫秒时间与time.Time互转同毫秒单调性v1.3.0/v1.3.1单调熵机制引入并优化随机增量来源。这套设计让 ULID 成为兼具时间可排序、并发唯一与数据库友好的标识符方案。若你的系统需要基于 ID 的时间排序、分库分表时的字典序分片或希望 ID 本身携带时间信息以便排查问题可参考 README.md 的 API 说明与 ulid.go 的实现细节直接复用本仓库内置的 ulid 库但请记住 README 中的告诫——如果你根本不需要基于时间的排序ULID 可能并非最优选择普通 UUID 往往更简单直接。赞分享人工智能AI AgentAgent 沙箱云原生容器运行时零信任【免费下载链接】substrateAgent Substrate: the core system项目地址https://gitcode.com/GitHub_Trending/substrate7/substrate点击查看免费下载相关推荐Podman 仓库内嵌 oklog/ulid v2 演进史从 CHANGELOG 到源码级 ULID 实现解析Podman 仓库内嵌 oklog/ulid v2 演进史从 CHANGELOG 到源码级 ULID 实现解析 在 Podman 仓库的 test/tools容器运行时云原生CLICilium 仓库中的 oklog/ulid v2Go ULID 标识符库的版本演进与实现解析Cilium 仓库中的 oklog/ulid v2Go ULID 标识符库的版本演进与实现解析 CHANGELOG.md https://link.gitco云原生网络服务网格可观测性网络安全eBPFVictoriaMetrics 中的 ULID 标识符oklog/ulid 的二进制编码、单调熵与排序原理全解析VictoriaMetrics 中的 ULID 标识符oklog/ulid 的二进制编码、单调熵与排序原理全解析 ULIDUniversally Uniqu时序数据库数据库指标监控可观测性后端上一篇Telegraf 以 Windows 服务方式运行安装、配置、管理与故障排查实战指南下一篇amis 视频组件Video实战指南从普通播放到 flv/hls 直播与视频帧切换创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表