一.前言
1.1预备
本文不会过多解释 Guid 是什么,以及顺序 Guid 的作用,需要读者自行具备:
1.知道 Guid,并且清楚其作用与优势08da7241-170b-c8ef-a094-06224a651c6a该 Guid 有16字节(byte)共128位(bit),可以包含时间戳,而顺序 Guid 主要是根据时间戳顺序来实现的,所以时间戳的部分,作为排序的决定性因素。
637947921111435500这是一个以时钟周期数(Tick)为单位的时间戳,为从公元1年1月1日0点至今的时钟周期数,1个 Tick 为 100ns(参考微软官方关于 Ticks 的介绍)。
public class SequentialGuidGenerator : IGuidGenerator, ITransientDependency { public Guid Create(SequentialGuidType guidType) { // 获取 10 字节随机序列数组 var randomBytes = new byte[10]; RandomNumberGenerator.GetBytes(randomBytes); // 获取 Ticks,并处理为毫秒级(1个Tick为100ns,1ms=1000us=1000000ns) long timestamp = DateTime.UtcNow.Ticks / 10000L; // 时间戳转为 byte 数组 byte[] timestampBytes = BitConverter.GetBytes(timestamp); // 因为数组是从 int64 转化过来的,如果是在小端系统中(little-endian),需要翻转 if (BitConverter.IsLittleEndian) { Array.Reverse(timestampBytes); } byte[] guidBytes = new byte[16]; switch (guidType) { case SequentialGuidType.SequentialAsString: case SequentialGuidType.SequentialAsBinary: // 16位数组:前6位为时间戳,后10位为随机数 Buffer.BlockCopy(timestampBytes, 2, guidBytes, 0, 6); Buffer.BlockCopy(randomBytes, 0, guidBytes, 6, 10); // .NET中,Data1 和 Data2块 分别视为 Int32 和 Int16 // 跟时间戳从 Int64 转 byte 数组后需要翻转一个理,在小端系统,需要翻转这两个块。 if (guidType == SequentialGuidType.SequentialAsString && BitConverter.IsLittleEndian) { Array.Reverse(guidBytes, 0, 4); Array.Reverse(guidBytes, 4, 2); } break; case SequentialGuidType.SequentialAtEnd: // 16位数组:前10位为随机数,后6位为时间戳 Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 10); Buffer.BlockCopy(timestampBytes, 2, guidBytes, 10, 6); break; } return new Guid(guidBytes); } }RandomNumberGenerator 用于生成随机序列数组。
public enum SequentialGuidType { /// <summary> /// 用于 MySql 和 PostgreSql.当使用 Guid.ToString() 方法进行格式化时连续. /// </summary> SequentialAsString, /// <summary> /// 用于 Oracle.当使用 Guid.ToByteArray() 方法进行格式化时连续. /// </summary> SequentialAsBinary, /// <summary> /// 用以 SqlServer.连续性体现于 GUID 的第4块(Data4). /// </summary> SequentialAtEnd }如各个枚举属性的 summary 描述,主要是因为数据库关于 Guid 排序方式的不同。
// 获取 Ticks,并处理为毫秒级(1个Tick为100ns,1ms=1000us=1000000ns) long timestamp = DateTime.UtcNow.Ticks / 10000L;非标准 Guid
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx其中 M 为 RFC 版本(version),版本4的话,值为4。
long timestamp = DateTime.UtcNow.Ticks;直接将毫秒的处理去掉,让时间戳为纳秒级(ns)。另外,还需要将时间戳原本只取6个字节,改成8个字节,让尾部的时间戳作用于 Guid 上。
public static Guid Next(SequentialGuidType guidType) { // 原先 10 字节的随机序列数组,减少为 8 字节 var randomBytes = new byte[8]; _randomNumberGenerator.GetBytes(randomBytes); // 时间戳保持纳秒级,不额外处理 long timestamp = DateTime.UtcNow.Ticks; byte[] timestampBytes = BitConverter.GetBytes(timestamp); if (BitConverter.IsLittleEndian) { Array.Reverse(timestampBytes); } byte[] guidBytes = new byte[16]; switch (guidType) { case SequentialGuidType.SequentialAsString: case SequentialGuidType.SequentialAsBinary: // 16位数组:前8位为时间戳,后8位为随机数 Buffer.BlockCopy(timestampBytes, 0, guidBytes, 0, 8); Buffer.BlockCopy(randomBytes, 0, guidBytes, 8, 8); // .NET中,Data1、Data2、Data3 块 分别视为 Int32、Int16、Int16 // 跟时间戳从 Int64 转 byte 数组后需要翻转一个理,在小端系统,需要翻转这3个块。 if (guidType == SequentialGuidType.AsString && BitConverter.IsLittleEndian) { Array.Reverse(guidBytes, 0, 4); Array.Reverse(guidBytes, 4, 2); Array.Reverse(guidBytes, 6, 2); // 翻转 } break; case SequentialGuidType.SequentialAtEnd: // 16位数组:前8位为随机数,后8位为时间戳 Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 8); // 方案1:正常拼接。这种方式只能连续1年+ Buffer.BlockCopy(timestampBytes, 0, guidBytes, 8, 8); // 方案2:将时间戳末尾的2个字节,放到 Data4 的前2个字节 Buffer.BlockCopy(timestampBytes, 6, guidBytes, 8, 2); Buffer.BlockCopy(timestampBytes, 0, guidBytes, 10, 6); break; } return new Guid(guidBytes); }测试
# 主要影响排序的,体现在 Guid 第8个字节。 08da7241-170b-c8ef-a094-06224a651c6a 0 08da7241-170b-d141-6ffc-5cdcecec5db9 1 08da7241-170b-d14e-d49e-81ce5efa6143 2 08da7241-170b-d150-8f59-836eab8d1939 3 08da7241-170b-d152-ac41-0c357a8aa4a1 4 08da7241-170b-d163-90a4-6083d462eeaf 5 08da7241-170b-d175-25b2-1d47ddd25939 6 08da7241-170b-d178-aa93-dc86e6391438 7 08da7241-170b-d185-619f-c24faf992806 8 08da7241-170b-d188-bd51-e36029ad9816 9AtEnd 方式:
// 顺序体现在最后一个字节 983C1A57-8C2B-DE7D-08DA-724214AED77D 0 4F1389B8-59F6-7C78-08DA-724214AEDAB6 1 CF6D52B1-3BFA-272F-08DA-724214AEDABC 2 017C4F99-4499-67DB-08DA-724214AEDABE 3 4B0A0685-4355-2060-08DA-724214AEDAC0 4 D690E344-DDB4-16CB-08DA-724214AEDAC6 5 6E22CDBE-65FE-64DC-08DA-724214AEDAC8 6 72E67EB4-CA92-DF3A-08DA-724214AEDACA 7 AA93D914-5415-21C9-08DA-724214AEDACB 8 9D93FA3F-84B6-519D-08DA-724214AEDACD 93.2、产生符合 RFC 4122 标准的 Guid
public static Guid Next(SequentialGuidType guidType) { // see: What is a GUID? http://guid.one/guid // see: https://github.com/richardtallent/RT.Comb#gory-details-about-uuids-and-guids // According to RFC 4122: // dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr // - M = RFC 版本(version), 版本4的话,值为4 // - N = RFC 变体(variant),值为 8, 9, A, B 其中一个,这里固定为8 // - d = 从公元1年1月1日0时至今的时钟周期数(DateTime.UtcNow.Ticks) // - r = 随机数(random bytes) var randomBytes = new byte[8]; _randomNumberGenerator.GetBytes(randomBytes); byte version = (byte)4; byte variant = (byte)8; byte filterHighBit = 0b00001111; byte filterLowBit = 0b11110000; long timestamp = DateTime.UtcNow.Ticks; byte[] timestampBytes = BitConverter.GetBytes(timestamp); if (BitConverter.IsLittleEndian) { Array.Reverse(timestampBytes); } byte[] guidBytes = new byte[16]; switch (guidType) { case SequentialGuidType.SequentialAsString: case SequentialGuidType.SequentialAsBinary: // AsString: dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr Buffer.BlockCopy(timestampBytes, 0, guidBytes, 0, 6); // 时间戳前6个字节,共48位 // guidBytes[6]:高4位为版本 | 低4位取时间戳序号[6]的元素的高4位 guidBytes[6] = (byte)((version << 4) | ((timestampBytes[6] & filterLowBit) >> 4)); // guidBytes[7]:高4位取:[6]低4位 | 低4位取:[7]高4位 guidBytes[7] = (byte)(((timestampBytes[6] & filterHighBit) << 4) | ((timestampBytes[7] & filterLowBit) >> 4)); // guidBytes[8]:高4位为:变体 | 低4位取:[7]低4位 guidBytes[8] = (byte)((variant << 4) | (timestampBytes[7] & filterHighBit)); Buffer.BlockCopy(randomBytes, 0, guidBytes, 9, 7); // 余下7个字节由随机数组填充 // .NET中,Data1、Data2、Data3 块 分别视为 Int32、Int16、Int16,在小端系统,需要翻转这3个块。 if (guidType == SequentialGuidType.AsString && BitConverter.IsLittleEndian) { Array.Reverse(guidBytes, 0, 4); Array.Reverse(guidBytes, 4, 2); Array.Reverse(guidBytes, 6, 2); } break; case SequentialGuidType.SequentialAtEnd: // AtEnd: rrrrrrrr-rrrr-Mxdr-Nddd-dddddddddddd // Block: 1 2 3 4 5 // Data4 = Block4 + Block5 // 排序顺序:Block5 > Block4 > Block3 > Block2 > Block1 // Data3 = Block3 被认为是 uint16,排序并不是从左到右,为消除影响,x 位取固定值 Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 6); // Mx 高4位为版本 | 低4位取:全0 guidBytes[6] = (byte)(version << 4); // dr 高4位为:时间戳[7]低4位 | 低4位取:随机数 guidBytes[7] = (byte)(((timestampBytes[7] & filterHighBit) << 4) | (randomBytes[7] & filterHighBit)); // Nd 高4位为:变体 | 低4位取:时间戳[6]高4位 guidBytes[8] = (byte)((variant << 4) | ((timestampBytes[6] & filterLowBit) >> 4)); // dd 高4位为:时间戳[6]低4位 | 低4位取:时间戳[7]高4位 guidBytes[9] = (byte)(((timestampBytes[6] & filterHighBit) << 4) | ((timestampBytes[7] & filterLowBit) >> 4)); Buffer.BlockCopy(timestampBytes, 0, guidBytes, 10, 6); // 时间戳前6个字节 if (BitConverter.IsLittleEndian) { //Array.Reverse(guidBytes, 0, 4); // 随机数就不翻转了 //Array.Reverse(guidBytes, 4, 2); Array.Reverse(guidBytes, 6, 2); // 包含版本号的 Data3 块需要翻转 } break; } return new Guid(guidBytes); }四、Sql Server 关于 Guid 的处理方案
// 获取所有注册的实体,遍历 foreach (var entityType in builder.Model.GetEntityTypes()) { // 获取实体的所有属性,遍历 PropertyInfo[] propertyInfos = entityType.ClrType.GetProperties(); foreach (PropertyInfo propertyInfo in propertyInfos) { string propertyName = propertyInfo.Name; // 将 Guid 类型设置为 char(36) if (propertyInfo.PropertyType == typeof(Guid)) { builder.Entity(entityType.ClrType).Property(propertyName).HasColumnType("char(36)"); } if(propertyInfo.PropertyType == typeof(Nullable<Guid>)) { builder.Entity(entityType.ClrType).Property(propertyName).HasColumnType("char(36)").IsRequired(false); } } }五、完整的代码
using System.Security.Cryptography; public enum SequentialGuidType { /// <summary> /// 用于 MySql 和 PostgreSql. /// 当使用 <see cref="Guid.ToString()" /> 方法进行格式化时连续. /// </summary> AsString, /// <summary> /// 用于 Oracle. /// 当使用 <see cref="Guid.ToByteArray()" /> 方法进行格式化时连续. /// </summary> AsBinary, /// <summary> /// 用以 SqlServer. /// 连续性体现于 GUID 的第4块(Data4). /// </summary> AtEnd } public static class GuidHelper { private const byte version = (byte)4; private const byte variant = (byte)8; private const byte filterHighBit = 0b00001111; private const byte filterLowBit = 0b11110000; private static readonly RandomNumberGenerator _randomNumberGenerator = RandomNumberGenerator.Create(); /// <summary> /// 连续 Guid 类型,默认:AsString. /// </summary> public static SequentialGuidType SequentialGuidType { get; set; } = SequentialGuidType.AsString; /// <summary> /// 生成连续 Guid. /// </summary> /// <returns></returns> public static Guid Next() { return Next(SequentialGuidType); } /// <summary> /// 生成连续 Guid(生成的 Guid 并不符合 RFC 4122 标准). /// 来源:Abp. from jhtodd/SequentialGuid https://github.com/jhtodd/SequentialGuid/blob/master/SequentialGuid/Classes/SequentialGuid.cs . /// </summary> /// <param name="guidType"></param> /// <returns></returns> public static Guid NextOld(SequentialGuidType guidType) { var randomBytes = new byte[8]; _randomNumberGenerator.GetBytes(randomBytes); long timestamp = DateTime.UtcNow.Ticks; byte[] timestampBytes = BitConverter.GetBytes(timestamp); if (BitConverter.IsLittleEndian) { Array.Reverse(timestampBytes); } byte[] guidBytes = new byte[16]; switch (guidType) { case SequentialGuidType.AsString: case SequentialGuidType.AsBinary: // 16位数组:前8位为时间戳,后8位为随机数 Buffer.BlockCopy(timestampBytes, 0, guidBytes, 0, 8); Buffer.BlockCopy(randomBytes, 0, guidBytes, 8, 8); // .NET中,Data1、Data2、Data3 块 分别视为 Int32、Int16、Int16,在小端系统,需要翻转这3个块。 if (guidType == SequentialGuidType.AsString && BitConverter.IsLittleEndian) { Array.Reverse(guidBytes, 0, 4); Array.Reverse(guidBytes, 4, 2); Array.Reverse(guidBytes, 6, 2); } break; case SequentialGuidType.AtEnd: // 16位数组:前8位为随机数,后8位为时间戳 Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 8); Buffer.BlockCopy(timestampBytes, 6, guidBytes, 8, 2); Buffer.BlockCopy(timestampBytes, 0, guidBytes, 10, 6); break; } return new Guid(guidBytes); } /// <summary> /// 生成连续 Guid. /// </summary> /// <param name="guidType"></param> /// <returns></returns> public static Guid Next(SequentialGuidType guidType) { // see: What is a GUID? http://guid.one/guid // see: https://github.com/richardtallent/RT.Comb#gory-details-about-uuids-and-guids // According to RFC 4122: // dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr // - M = RFC 版本(version), 版本4的话,值为4 // - N = RFC 变体(variant),值为 8, 9, A, B 其中一个,这里固定为8 // - d = 从公元1年1月1日0时至今的时钟周期数(DateTime.UtcNow.Ticks) // - r = 随机数(random bytes) var randomBytes = new byte[8]; _randomNumberGenerator.GetBytes(randomBytes); long timestamp = DateTime.UtcNow.Ticks; byte[] timestampBytes = BitConverter.GetBytes(timestamp); if (BitConverter.IsLittleEndian) { Array.Reverse(timestampBytes); } byte[] guidBytes = new byte[16]; switch (guidType) { case SequentialGuidType.AsString: case SequentialGuidType.AsBinary: // AsString: dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr Buffer.BlockCopy(timestampBytes, 0, guidBytes, 0, 6); // 时间戳前6个字节,共48位 guidBytes[6] = (byte)((version << 4) | ((timestampBytes[6] & filterLowBit) >> 4)); // 高4位为版本 | 低4位取时间戳序号[6]的元素的高4位 guidBytes[7] = (byte)(((timestampBytes[6] & filterHighBit) << 4) | ((timestampBytes[7] & filterLowBit) >> 4)); // 高4位取:[6]低4位 | 低4位取:[7]高4位 guidBytes[8] = (byte)((variant << 4) | (timestampBytes[7] & filterHighBit)); // 高4位为:变体 | 低4位取:[7]低4位 Buffer.BlockCopy(randomBytes, 0, guidBytes, 9, 7); // 余下7个字节由随机数组填充 // .NET中,Data1、Data2、Data3 块 分别视为 Int32、Int16、Int16,在小端系统,需要翻转这3个块。 if (guidType == SequentialGuidType.AsString && BitConverter.IsLittleEndian) { Array.Reverse(guidBytes, 0, 4); Array.Reverse(guidBytes, 4, 2); Array.Reverse(guidBytes, 6, 2); } break; case SequentialGuidType.AtEnd: // AtEnd: rrrrrrrr-rrrr-Mxdr-Nddd-dddddddddddd // Block: 1 2 3 4 5 // Data4 = Block4 + Block5 // 排序顺序:Block5 > Block4 > Block3 > Block2 > Block1 // Data3 = Block3 被认为是 uint16,排序并不是从左到右,为消除影响,x 位取固定值 Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 6); guidBytes[6] = (byte)(version << 4); // Mx 高4位为版本 | 低4位取:全0 guidBytes[7] = (byte)(((timestampBytes[7] & filterHighBit) << 4) | (randomBytes[7] & filterHighBit)); ; // dr 高4位为:时间戳[7]低4位 | 低4位取:随机数 guidBytes[8] = (byte)((variant << 4) | ((timestampBytes[6] & filterLowBit) >> 4)); // Nd 高4位为:变体 | 低4位取:时间戳[6]高4位 guidBytes[9] = (byte)(((timestampBytes[6] & filterHighBit) << 4) | ((timestampBytes[7] & filterLowBit) >> 4)); // dd 高4位为:时间戳[6]低4位 | 低4位取:时间戳[7]高4位 Buffer.BlockCopy(timestampBytes, 0, guidBytes, 10, 6); // 时间戳前6个字节 if (BitConverter.IsLittleEndian) { Array.Reverse(guidBytes, 6, 2); // 包含版本号的 Data3 块需要翻转 } break; } return new Guid(guidBytes); } }