再见 GUID 索引碎片:在 C# 中使用 Nulid 生成有序 ID
受够了Guid的无序,又不满足int类型id的限制,那么是时候了解一下Ulid了。
链接
Ulid 的特点
为什么在分布式系统或高并发 Web 应用中,Ulid 正在逐渐取代传统的 Guid?它的核心优势可以概括为以下几点:
- 天生有序(Lexicographically Sortable): 这是解决 Guid 痛点的杀手锏。Ulid 的前 48 位是毫秒级时间戳,这意味着新生成的 ID 在字典序上永远大于旧的 ID。对于使用 B-Tree 索引的数据库(如 SQL Server, MySQL, PostgreSQL)来说,这极大减少了索引碎片,提升了插入性能。
- 128 位兼容性:
Ulid 的二进制长度与 Guid 完全一致(均为 16 字节/128 位)。这意味着你可以在数据库中直接使用
uniqueidentifier或uuid类型来存储它,而不需要修改现有的数据库表结构。 - 更友好的编码格式 (Base32): 不同于 Guid 冗长且带有连字符的十六进制格式,Ulid 使用 Crockford’s Base32 编码(26 个字符)。它不包含 I、L、O、U,有效避免了视觉混淆,并且是 URL 安全的。
- 高精度与抗碰撞:
相比于
int或long的自增 ID,Ulid 提供了 80 位的熵(随机数),足以保证在分布式环境下极低的碰撞概率。
Nulid 的限制
虽然 Nulid 是 .NET 平台上性能极佳的 Ulid 实现方案,但在实际工程中使用它时,你需要注意以下几个限制和“坑”:
- 单机单毫秒内的单调性: Ulid 标准规定,如果在同一毫秒内生成多个 ID,为了保持严格递增,最后的随机位应该自增。但并非所有的 .NET 实现库(包括 Nulid 的默认配置)都能在高并发下完美保证这种严格单调性,在极端的并发下可能会出现时间戳相同但随机位乱序的情况。
- 时间溢出问题: Ulid 的时间戳部分设计上限是到公元 10889 年。虽然这对我们来说足够长了,但如果你在处理某些远古数据或极端未来的逻辑模拟,需要意识到它的时间边界。
- 生态链支持: 虽然 C# 端处理很方便,但前端 JS 库或一些老旧的报表系统可能无法原生识别 Ulid 字符串。你可能需要在 Web API 的序列化层(JsonSerializerOptions)做一些自定义转换工作,以确保前端能正确处理这 26 位字符串。
解决单机单毫秒内的单调性
在Nulid中,通过使用MonotonicUlidRng来解决这一问题。
// Create monotonic rng
var rng = new MonotonicRng();
// Create ULIDs, assume that these calls occur within the same millisecond:
Console.WriteLine(Ulid.NewUlid(rng)); // 01DBN5W2SG000DCBVYHX4T6MCX
Console.WriteLine(Ulid.NewUlid(rng)); // 01DBN5W2SG000DCBVYHX4T6MCY
Console.WriteLine(Ulid.NewUlid(rng)); // 01DBN5W2SG000DCBVYHX4T6MCZ
Console.WriteLine(Ulid.NewUlid(rng)); // 01DBN5W2SG000DCBVYHX4T6MD0
Console.WriteLine(Ulid.NewUlid(rng)); // 01DBN5W2SG000DCBVYHX4T6MD1
Console.WriteLine(Ulid.NewUlid(rng)); // 01DBN5W2SG000DCBVYHX4T6MD2
其余的rng类型可以查看源代码https://github.com/RobThree/NUlid/tree/master/NUlid/Rng