You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现兼具极致性能与可测试性的跨平台GUID生成器?

高性能可测试的Sequential Guid生成方案问题

我需要通过单元测试验证一个静态类,确保它在物理机、虚拟机、容器等各类环境下正常工作,且同一环境并行多次运行时能生成唯一结果。当前的问题是这个类使用了DateTime.Now调用,若改为非静态类并使用TimeProvider结构体虽然能实现可测试性,但必须满足以下限制:

  • 该类每秒被调用数万次,生产环境记录显示2分钟内每秒调用151964235次,采用静态类就是为了避免GC压力
  • 解决方案性能不能低于当前实现,包括运行时、对象创建时间及GC压力
  • 具备可移植性,能在Windows、Linux和Mac系统运行
  • 欢迎任何进一步提升性能的思路
  • 当前核心关注点是DateTime.Now,UniqueId与Environment.CurrentManagedThreadId后续再考虑

实现代码

namespace My.Utils;

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Security.Cryptography;
using System.Threading;

/// <summary>
///     <see cref="SequentialGuid" /> generates instances of <see cref="Guid" /> that are in ascending order.
/// </summary>
/// <remarks>
///     It is both unique and ordered.
/// </remarks>
public static class SequentialGuid
{
    private const uint GuidVersion8 = 0x8000;
    private const uint GuidVersion7 = 0x7000;
    private const uint Variant = 0x80000000;
    private const uint VariantMask = 0xC0000000;
    private const uint SequenceRollover = 0x1000;
    private static readonly MapperAb EmptyMapperAb = new();
    private static readonly MapperCd BaseMapperCd;
    private static readonly uint C;
    private static long _lastMilliseconds;
    private static uint _sequence;
    private static SpinLock _spinLock;

    static SequentialGuid()
    {
        _spinLock = new SpinLock(false);
        int rnd = RandomNumberGenerator.GetInt32(int.MaxValue);
        rnd ^= Environment.MachineName.GetHashCode();
        BaseMapperCd.C = (uint)rnd;
        BaseMapperCd.C &= ~VariantMask;
        BaseMapperCd.C |= Variant;
        BaseMapperCd.D = (uint)(Environment.ProcessId ^ rnd);
        C = (uint)rnd;
        C &= ~VariantMask;
        C |= Variant;
    }

    public static Guid CreateVersion7()
    {
        return CreateVersion7(DateTimeOffset.UtcNow);
    }

    public static Guid CreateVersion7(in DateTimeOffset timestamp)
    {
        var mapperAb = EmptyMapperAb;
        GetTicksAndSequence(timestamp, ref mapperAb);
        mapperAb.B |= GuidVersion7;

        var d = (uint)RandomNumberGenerator.GetInt32(int.MaxValue);
        Vector128<byte> vec = Vector128.Create(mapperAb.A, mapperAb.B, C, d).AsByte();
        if (BitConverter.IsLittleEndian)
        {
            Vector128<byte> result = Vector128.Shuffle(vec, Vector128.Create((byte)0, 1, 2, 3, 6, 7, 4, 5, 11, 10, 9, 8, 15, 14, 13, 12));
            return Unsafe.As<Vector128<byte>, Guid>(ref result);
        }

        return Unsafe.As<Vector128<byte>, Guid>(ref vec);
    }

    public static Guid CreateVersion8()
    {
        return CreateVersion8(DateTimeOffset.UtcNow);
    }

    public static Guid CreateVersion8(in DateTimeOffset timestamp)
    {
        var mapperAb = EmptyMapperAb;
        GetTicksAndSequence(timestamp, ref mapperAb);
        mapperAb.B |= GuidVersion8;

        Vector128<byte> vec = Vector128.Create(mapperAb.A, mapperAb.B, BaseMapperCd.C, BaseMapperCd.D).AsByte();
        if (BitConverter.IsLittleEndian)
        {
            Vector128<byte> result = Vector128.Shuffle(vec, Vector128.Create((byte)0, 1, 2, 3, 6, 7, 4, 5, 11, 10, 9, 8, 15, 14, 13, 12));
            return Unsafe.As<Vector128<byte>, Guid>(ref result);
        }

        return Unsafe.As<Vector128<byte>, Guid>(ref vec);
    }

    private static void GetTicksAndSequence(in DateTimeOffset timestamp, ref MapperAb mapperAb)
    {
        mapperAb.Ticks = timestamp.ToUnixTimeMilliseconds();
        ArgumentOutOfRangeException.ThrowIfNegative(mapperAb.Ticks, nameof(timestamp));

        var lockTaken = false;
        _spinLock.Enter(ref lockTaken);
        if (mapperAb.Ticks > _lastMilliseconds)
        {
            _sequence = 0;
            _lastMilliseconds = mapperAb.Ticks;
        }
        else
        {
            if (_sequence == SequenceRollover) // rollover will happen, so we increase ticks
            {
                _sequence = 0;
                ++_lastMilliseconds;
            }

            mapperAb.Ticks = _lastMilliseconds;
        }

        uint b = _sequence++;
        if (lockTaken) { _spinLock.Exit(); }

        mapperAb.Ticks <<= 16;
        mapperAb.B |= b;
    }

    [StructLayout(LayoutKind.Explicit)]
    private struct MapperAb
    {
        [FieldOffset(0)] public long Ticks;
        [FieldOffset(0)] public uint B;
        [FieldOffset(sizeof(uint))] public uint A;
    }

    [StructLayout(LayoutKind.Explicit)]
    private struct MapperCd
    {
        [FieldOffset(0)] public uint D;
        [FieldOffset(0)] public ushort ProcessId;
        [FieldOffset(sizeof(ushort))] public byte ThreadId;
        [FieldOffset(sizeof(ushort) + sizeof(byte))] public byte TaskId;
        [FieldOffset(sizeof(uint))] public uint C;
    }
}

基准测试结果

基准测试显示:自定义UUIDv7实现性能优于.NET9内置的Guid.CreateVersion7,自定义UUIDv8的性能比内置UUIDv7提升约一倍,且所有自定义实现的内存分配均为0。

基准测试代码

namespace Guid.Benchmarks;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNetVisualizer;
using JetBrains.Annotations;
using Matching.Utils;

// ReSharper disable once ClassCanBeSealed.Global
[MaxColumn]
[MemoryDiagnoser(false)]
[MinColumn]
[RichHtmlExporter("Benchmark of UUID Creation",
                  ["Job"],
                  ["Mean", "Allocated"],
                  ["Mean", "Min", "Max", "Allocated"],
                  dividerMode: RenderTableDividerMode.SeparateTables,
                  htmlWrapMode: HtmlDocumentWrapMode.RichDataTables)]
[ShortRunJob(RuntimeMoniker.Net90)]
[UsedImplicitly]
public class BenchmarkSequentialGuidCreation
{
    [Benchmark]
    public Guid BenchmarUuidV4() => Guid.NewGuid(); // UUIDv4

    [Benchmark(Baseline = true)]
    public Guid BenchmarkUuidV7() => Guid.CreateVersion7(); // UUIDv7

    [Benchmark]
    public Guid BenchmarkUuidV7Custom() => SequentialGuid.CreateVersion7(); // UUIDv7

    [Benchmark]
    public Guid BenchmarkUuidV8Custom() => SequentialGuid.CreateVersion8(); // UUIDv7
}

更新内容

  • 研究了.NET9中的UUID v7实现,逻辑与自定义实现基本一致,但性能明显更低
  • 研究了EF Core 8的实现,该实现未使用真实时间组件,而是以对象初始化时间作为所有ID生成的基础,在进程启停场景下可能存在重复风险,且无其他唯一性保障机制
  • 已将代码更新至最终版本,支持单元测试(通过接收DateTimeOffset参数的重载方法),遵循Guid.CreateVersion7的实现逻辑,同时新增CreateVersion8方法,虽存在可预测性,但性能提升一倍

内容的提问来源于stack exchange,提问作者Iaman Swtrse

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 00:07:03