在.Net 9中生成有序的Guid

前言

在C#中叫Guid,其他语言中叫UUID,新的有序Guid就是使用UUID V7(第七版).

在项目中,我们是使用Guid作为主键,但有时候还需要有序的,在ABP中是提供生成有序的Guid,下面我们看看这几种生成Guid的方式.

示例

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using BenchmarkDotNet.Attributes;

namespace CSharpBenchmarks.SystemTest
{
	[MemoryDiagnoser]
	[DisassemblyDiagnoser(printSource: true)]
	public class GuidTest
	{
		public SequentialGuidGenerator sequentialGuidGenerator;

		[GlobalSetup]
		public void Init()
		{
			sequentialGuidGenerator = new();
		}

		[Params(1024, 2048, 4096)]
		public int Count { get; set; }

		[Benchmark(Baseline = true)]
		public void Test1()
		{
			List<Guid> list = new List<Guid>(Count);
			for (int i = 0; i < Count; i++)
			{
				list.Add(Guid.NewGuid()); //第一种:DotNet自带,无序的Guid
			}
		}

		[Benchmark]
		public void Test2()
		{
			List<Guid> list = new List<Guid>(Count);
			for (int i = 0; i < Count; i++)
			{
				list.Add(Guid.CreateVersion7()); //第二种: .Net 9增加的,有序的
			}
		}

		[Benchmark]
		public void Test3()
		{
			List<Guid> list = new List<Guid>(Count);
			for (int i = 0; i < Count; i++)
			{
				list.Add(sequentialGuidGenerator.Create()); //第三种: ABP生成有序的Guid
			}
		}
	}
}

/// <summary>
/// Describes the type of a sequential GUID value.
/// </summary>
public enum SequentialGuidType
{
	/// <summary>
	/// The GUID should be sequential when formatted using the <see cref="Guid.ToString()" /> method.
	/// Used by MySql and PostgreSql.
	/// </summary>
	SequentialAsString,

	/// <summary>
	/// The GUID should be sequential when formatted using the <see cref="Guid.ToByteArray" /> method.
	/// Used by Oracle.
	/// </summary>
	SequentialAsBinary,

	/// <summary>
	/// The sequential portion of the GUID should be located at the end of the Data4 block.
	/// Used by SqlServer.
	/// </summary>
	SequentialAtEnd
}

/// <summary>
/// ABP 具体生成有序Guid的代码
/// </summary>
public class SequentialGuidGenerator
{
	private static readonly RandomNumberGenerator RandomNumberGenerator = RandomNumberGenerator.Create();

	public SequentialGuidGenerator()
	{
	}

	public Guid Create()
	{
		return Create(SequentialGuidType.SequentialAsString);
	}

	public Guid Create(SequentialGuidType guidType)
	{
		// We start with 16 bytes of cryptographically strong random data.
		var randomBytes = new byte[10];
		RandomNumberGenerator.GetBytes(randomBytes);

		// An alternate method: use a normally-created GUID to get our initial
		// random data:
		// byte[] randomBytes = Guid.NewGuid().ToByteArray();
		// This is faster than using RNGCryptoServiceProvider, but I don't
		// recommend it because the .NET Framework makes no guarantee of the
		// randomness of GUID data, and future versions (or different
		// implementations like Mono) might use a different method.

		// Now we have the random basis for our GUID.  Next, we need to
		// create the six-byte block which will be our timestamp.

		// We start with the number of milliseconds that have elapsed since
		// DateTime.MinValue.  This will form the timestamp.  There's no use
		// being more specific than milliseconds, since DateTime.Now has
		// limited resolution.

		// Using millisecond resolution for our 48-bit timestamp gives us
		// about 5900 years before the timestamp overflows and cycles.
		// Hopefully this should be sufficient for most purposes. :)
		long timestamp = DateTime.UtcNow.Ticks / 10000L;

		// Then get the bytes
		byte[] timestampBytes = BitConverter.GetBytes(timestamp);

		// Since we're converting from an Int64, we have to reverse on
		// little-endian systems.
		if (BitConverter.IsLittleEndian)
		{
			Array.Reverse(timestampBytes);
		}

		byte[] guidBytes = new byte[16];

		switch (guidType)
		{
			case SequentialGuidType.SequentialAsString:
			case SequentialGuidType.SequentialAsBinary:

				// For string and byte-array version, we copy the timestamp first, followed
				// by the random data.
				Buffer.BlockCopy(timestampBytes, 2, guidBytes, 0, 6);
				Buffer.BlockCopy(randomBytes, 0, guidBytes, 6, 10);

				// If formatting as a string, we have to compensate for the fact
				// that .NET regards the Data1 and Data2 block as an Int32 and an Int16,
				// respectively.  That means that it switches the order on little-endian
				// systems.  So again, we have to reverse.
				if (guidType == SequentialGuidType.SequentialAsString && BitConverter.IsLittleEndian)
				{
					Array.Reverse(guidBytes, 0, 4);
					Array.Reverse(guidBytes, 4, 2);
				}

				break;

			case SequentialGuidType.SequentialAtEnd:

				// For sequential-at-the-end versions, we copy the random data first,
				// followed by the timestamp.
				Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 10);
				Buffer.BlockCopy(timestampBytes, 2, guidBytes, 10, 6);
				break;
		}

		return new Guid(guidBytes);
	}
}

看看几种生成Guid的性能测试:

csharp几种生成Guid的方式,有无序和有序两种

可以看到ABP中生成有序的Guid是这几种中耗时最多的,相当于.Net 中的生成无序Guid的两倍,如果不需要顺序的话,Guid.NewGuid()这种方式最好(这里不包含.Net framework,在.Net Core中对Guid有优化),如果需要顺序的话,且有需要性能的话,可以使用Guid.CreateVersion7()这种方式(不过这个需要.Net 9),短时间使用顺序的Guid还可以使用ABP的这种方式

秋风 2024-07-14