.NET 9 中引入了基于时间来生成 Guid, 因为实现的 RFC 文档里的第七个版本, 所以 API 名称为 Guid.CreateVersion7()
新增的 API 如下:
namespace System;
public partial struct Guid
{
public static Guid AllBitsSet { get; }
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
AllBitsSet
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 允许您指定所有底层位的值
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://github.com/dotnet/runtime/issues/103658#issuecomment-2226246739