• 聊聊.NET 9 中引入基于时间来生成 Guid的API
  • 发布于 2个月前
  • 178 热度
    0 评论
  • pckillers
  • 0 粉丝 46 篇博客
  •   
前言
.NET 9 中引入了基于时间来生成 Guid, 因为实现的 RFC 文档里的第七个版本, 所以 API 名称为 Guid.CreateVersion7()

新API
新增的 API 如下:
namespace System;
public partial struct Guid
{
    public static Guid AllBitsSet { get; }
    // 堆代码 duidaima.com
    public int Variant { get; }
    public int Version { get; }

    public static Guid CreateVersion7();
    public static Guid CreateVersion7(DateTimeOffset timestamp);
}
Guid.AllBitsSet 对应着 Guid.Empty, Empty 的所有比特位都是 0, AllBitsSet 则都是 1

Version 和 Variant 代表了当前 Guid 值的实现细节, 可以参考 RFC 文档说明 https://www.rfc-editor.org/rfc/rfc9562.html#name-variant-field
Guid.CreateVersion7()/Guid.CreateVersion7(DateTimeOffset timestamp) 用于创建基于时间的 Guid, 如果没有参数就会使用当前时间
public static Guid CreateVersion7() => CreateVersion7(DateTimeOffset.UtcNow);

例子
来看一个使用示例吧,
var guid = Guid.CreateVersion7();
Console.WriteLine(guid.ToString());
Console.WriteLine($"{nameof(guid.Version)}: {guid.Version}");
Console.WriteLine($"{nameof(guid.Variant)}: {guid.Variant}");

var timestamp = DateTimeOffset.UtcNow;
Console.WriteLine($"Timestamp: {timestamp} {timestamp.ToUnixTimeMilliseconds()}");
Console.WriteLine(Guid.CreateVersion7(timestamp));
用起来是不是还挺简单的, 有一个问题, 既然是基于时间的,同一个时间戳会不会生成的 Guid 是一样的呢?
我们来测试一下, 接着前面的示例,
guid = Guid.CreateVersion7(timestamp);
Console.WriteLine(guid);
输出结果如下:
0191fa19-7082-7541-ae8e-befcfffe79cb
Version: 7
Variant: 10
Timestamp: 9/16/2024 09:10:56 +00:00 1726477856901
0191fa19-7085-7e0b-ae72-aa63b4585467
0191fa19-7085-782f-a30b-3a0223ba3a31
可以看到两次生成的 guid 并不相同, 这从 rfc 文档或者实现细节中可以了解到, 这是因为除了时间参数之外还会有随机参数,导致即使时间一样生成的 guid 还是会不一样。那我们能否从 Guid 中获取到时间呢? 答案是肯定的, 不过获取到的时间不会完全准确有一定的误差, 因为可能会引入随机参数, 从上面的输出也可以看得出来, 两个 guid 的前面十二个字符是完全一样的, 前面 6 个 byte 会是一样的, 他们对应了时间信息, 我们也可以从源码里找到一些细节

这里的 _a, _b 对应的就是前面的两段, 也可以从源码的注释里获取更多说明

最后我们可以从 byte 里获取到时间的信息, 实现如下:
private static void PrintDateTime(Guid guid)
{
    if (guid.Version is not 7)
    {
        throw new InvalidOperationException("Guid.Version is not 7");
    }

    var bytes = guid.ToByteArray();
    var a = BitConverter.ToInt32(bytes.AsSpan(0, 4));
    var b = BitConverter.ToInt16(bytes.AsSpan(4, 2));
    var timestamp = (((long)a) << 16) + b; 
    var dateTime = DateTimeOffset.FromUnixTimeMilliseconds(timestamp);
    Console.WriteLine($"DateTime: {dateTime.UtcDateTime}   {timestamp}");
}
接着前面的示例试一下
Thread.Sleep(2000);
Console.WriteLine(Guid.CreateVersion7());
PrintDateTime(guid);
输出结果如下:

可以看到两个时间比较接近但还是会有一些误差,不过误差会比较小,可以看到只有一分钟多一点的误差。


更多
Github 上有一个根据 Guid 获取时间的 issue, 不过因为时间并不准确, 可能大概率不会支持, 感兴趣的朋友可以关注 https://github.com/dotnet/runtime/issues/107136

Github issue 上还有作者对于 Guid 实现的一些总结, 感觉可以了解一下, 也分享一下:
v1 被广泛认为已过时,应该尽可能用 v7 替代 v2 用于 DCE 安全目的,超出了正常规范 v3 被广泛认为已过时,应该尽可能用 v5 替代 v4 用于创建随机 UUID,目前已经通过 Guid.NewGuid 支持 v5 用于从字符串输入创建 UUID,但由于使用 SHA-1,因此也被广泛认为已过时,因为存在潜在的安全攻击风险 v6 是简单的 v1,并对位进行了替代排序,同样被广泛认为已过时,应该尽可能用 v7 替代 v7 是本提案通过新的 CreateVersion7 API 所支持的版本 有一些可选的扩展功能尚不支持,但我们可以在未来扩展以支持这些功能 v8 明确用于实验性和特定供应商使用,其包含的位没有定义,仅限于版本和变体字段 这间接地通过普通的新 Guid(...) API 得到支持,这些 API 允许您指定所有底层位的值

参考
https://datatracker.ietf.org/doc/rfc9562/
https://www.rfc-editor.org/rfc/rfc9562.html
https://github.com/dotnet/runtime/issues/103658
https://github.com/dotnet/runtime/pull/104124
https://github.com/dotnet/runtime/issues/103658#issuecomment-2226246739
https://github.com/dotnet/runtime/issues/107136
https://github.com/WeihanLi/SamplesInPractice/blob/main/net9sample/Net9Samples/GuidSample.cs

用户评论