一、背景

常见的一种数据库设计是使用连续的整数为做主键,当新的数据插入到数据库时,由数据库自动生成。但这种设计不一定适合所有场景。

随着越来越多的使用Nhibernate、EntityFramework等ORM(对象关系映射)框架,应用程序被设计成为工作单元(Unit Of Work)模式,需要在数据持久化之前生成主键,为了保证在多线程并发以及站点集群环境中主键的唯一性,最简单最常见的方式是将主键设计成为GUID类型。

工作单元:是数据库应用程序经常使用的一种设计模式,简单一点来说,就是对多个数据库操作进行打包,记录对象上的所有变化,并在最后提交时一次性将所有变化通过系统事务写入数据库。目的是为了减少数据库调用次数以及避免数据库长事务。

GUID(全球唯一标识符)也称为UUID,是一种由算法生成的二进制长度为128位的数字标识符。在理想情况下,任何计算机和计算机集群都不会生成两个相同的GUID。GUID 的总数达到了2^128(3.4×10^38)个,所以随机生成两个相同GUID的可能性非常小,但并不为0。GUID一词有时也专指微软对UUID标准的实现。

RFC 41222描述了创建标准GUID,如今大多数GUID生成算法通常是一个很长的随机数,再结合一些像网络MAC地址这种随机的本地组件信息。

GUID的优点允许开发人员随时创建新值,而无需从数据库服务器检查值的唯一性,这似乎是一个完美的解决方案。

很多数据库在创建主键时,为了充分发挥数据库的性能,会自动在该列上创建聚集索引。

我们先来说一说什么是聚集索引。集索引确定表中数据的物理顺序。聚集索引类似于电话簿,按姓氏排列数据。由于聚集索引规定数据在表中的物理存储顺序,因此一个表也只能包含一个聚集索引。它能够快速查找到数据,但是如果插入数据库的主键不在列表的末尾,向表中添加新行时就非常缓慢。例如,看下面这个例子,在表中已经存在三行数据:

此时非常简单:数据行按对应ID列的顺序储存。如果我们新添加一行ID为8的数据,不会产生任何问题,新行会追加的末尾。

但如果我们想插入一行的ID为5的数据:

ID为7,8的数据行必须向下移动。虽然在这算什么事儿,但当您的数据量达到数百万行的级别之后,这就是个问题了。如果你还想要每秒处理上百次这种请求,那可真是难上加难了。

这就是GUID主键引发的问题:它是随机产生的,所以在数据插入时,随时都会涉及到数据的移动,导致插入会很缓慢,还会涉及大量不必要的磁盘活动。总结果有两点:

  • 空间的浪费以及由此带来的读写效率的下降;
  • 更主要的,存储的碎片化以及由此带来的读写效率严重下降。

GUID最关键的问题就是它是随机的。我们需要设计一种有规则的GUID生成方式,在之后生成的GUID类型总是比之前的要大,保证插入数据库的主键是在列表末尾追加的,这种我们称之为有序GUID

二、GUID排序规则

在讲解有序GUID之前,我们必须先了解一下GUID在.Net中以及各个数据库中的排序规则,排序规则不一样,生成有序GUID的规则也会随之变化。

128位的GUID主要有4部分组成:Data1, Data2, Data3, and Data4,你可以看成下面这样:

11111111-2222-3333-4444-444444444444

Data1占4个字节、Data2占2个字节、 Data3占2个字节、Data4占8个字节。我们分别的对个字节编上序号:

序号 1 2 3 4   5 6   7 8   9 10   11 12 13 14 15 16
Value 11 11 11 11 - 22 22 - 33 33 - 44 44 - 44 44 44 44 44 44

GUID在.Net中的排序规则

在.Net中,GUID默认的排序过段规则是按左到右的,看下面这个示例。

 var list = new List<Guid> {
new Guid("00000000-0000-0000-0000-010000000000"),
new Guid("00000000-0000-0000-0000-000100000000"),
new Guid("00000000-0000-0000-0000-000001000000"),
new Guid("00000000-0000-0000-0000-000000010000"),
new Guid("00000000-0000-0000-0000-000000000100"),
new Guid("00000000-0000-0000-0000-000000000001"),
new Guid("00000000-0000-0000-0100-000000000000"),
new Guid("00000000-0000-0000-0010-000000000000"),
new Guid("00000000-0000-0001-0000-000000000000"),
new Guid("00000000-0000-0100-0000-000000000000"),
new Guid("00000000-0001-0000-0000-000000000000"),
new Guid("00000000-0100-0000-0000-000000000000"),
new Guid("00000001-0000-0000-0000-000000000000"),
new Guid("00000100-0000-0000-0000-000000000000"),
new Guid("00010000-0000-0000-0000-000000000000"),
new Guid("01000000-0000-0000-0000-000000000000")
};
list.Sort(); foreach (Guid guid in list) {
Console.WriteLine(guid.ToString());
}

输出结果:

00000000-0000-0000-0000-000000000001
00000000-0000-0000-0000-000000000100
00000000-0000-0000-0000-000000010000
00000000-0000-0000-0000-000001000000
00000000-0000-0000-0000-000100000000
00000000-0000-0000-0000-010000000000
00000000-0000-0000-0010-000000000000
00000000-0000-0000-0100-000000000000
00000000-0000-0001-0000-000000000000
00000000-0000-0100-0000-000000000000
00000000-0001-0000-0000-000000000000
00000000-0100-0000-0000-000000000000
00000001-0000-0000-0000-000000000000
00000100-0000-0000-0000-000000000000
00010000-0000-0000-0000-000000000000
01000000-0000-0000-0000-000000000000

通过上面的输出结果,我们可以得到排序的权重如下:

序号 1 2 3 4   5 6   7 8   9 10   11 12 13 14 15 16
权重 1 2 3 4   5 6   7 8   9 10   11 12 13 14 15 16
Value 11 11 11 11 - 22 22 - 33 33 - 44 44 - 44 44 44 44 44 44

这与数字排序规则一致,从右到左进行依次进行排序(数字越小,权重越高,排序的优先级越高)。

GUID在各个数据库中的排序规则

在SQL Server数据库中,我们有一种非常简单的方式来比较两个GUID类型的大小值(其实在SQL Server数据库中称为UniqueIdentifier类型):

 With UIDs As (--                         0 1 2 3  4 5  6 7  8 9  A B C D E F
Select ID = 1, UID = cast ('00000000-0000-0000-0000-010000000000' as uniqueidentifier)
Union Select ID = 2, UID = cast ('00000000-0000-0000-0000-000100000000' as uniqueidentifier)
Union Select ID = 3, UID = cast ('00000000-0000-0000-0000-000001000000' as uniqueidentifier)
Union Select ID = 4, UID = cast ('00000000-0000-0000-0000-000000010000' as uniqueidentifier)
Union Select ID = 5, UID = cast ('00000000-0000-0000-0000-000000000100' as uniqueidentifier)
Union Select ID = 6, UID = cast ('00000000-0000-0000-0000-000000000001' as uniqueidentifier)
Union Select ID = 7, UID = cast ('00000000-0000-0000-0100-000000000000' as uniqueidentifier)
Union Select ID = 8, UID = cast ('00000000-0000-0000-0010-000000000000' as uniqueidentifier)
Union Select ID = 9, UID = cast ('00000000-0000-0001-0000-000000000000' as uniqueidentifier)
Union Select ID = 10, UID = cast ('00000000-0000-0100-0000-000000000000' as uniqueidentifier)
Union Select ID = 11, UID = cast ('00000000-0001-0000-0000-000000000000' as uniqueidentifier)
Union Select ID = 12, UID = cast ('00000000-0100-0000-0000-000000000000' as uniqueidentifier)
Union Select ID = 13, UID = cast ('00000001-0000-0000-0000-000000000000' as uniqueidentifier)
Union Select ID = 14, UID = cast ('00000100-0000-0000-0000-000000000000' as uniqueidentifier)
Union Select ID = 15, UID = cast ('00010000-0000-0000-0000-000000000000' as uniqueidentifier)
Union Select ID = 16, UID = cast ('01000000-0000-0000-0000-000000000000' as uniqueidentifier)
)
Select * From UIDs Order By UID, ID

查询结果:

ID UID
16 01000000-0000-0000-0000-000000000000
15 00010000-0000-0000-0000-000000000000
14 00000100-0000-0000-0000-000000000000
13 00000001-0000-0000-0000-000000000000
12 00000000-0100-0000-0000-000000000000
11 00000000-0001-0000-0000-000000000000
10 00000000-0000-0100-0000-000000000000
9 00000000-0000-0001-0000-000000000000
8 00000000-0000-0000-0010-000000000000
7 00000000-0000-0000-0100-000000000000
6 00000000-0000-0000-0000-000000000001
5 00000000-0000-0000-0000-000000000100
4 00000000-0000-0000-0000-000000010000
3 00000000-0000-0000-0000-000001000000
2 00000000-0000-0000-0000-000100000000
1 00000000-0000-0000-0000-010000000000

通过上面可以得于是如下结果:

  1. 先按每1-8从左到右进行排序;
  2. 接着按第9-10位从右到左进行排序;
  3. 最后按后11-16位从右到左进行排序;

通过分析,我们可得到如下权重列表:

序号 1 2 3 4   5 6   7 8   9 10   11 12 13 14 15 16
权重 16 15 14 13   12 11   10 9   7 8   1 2 3 4 5 6
Value 11 11 11 11 - 22 22 - 33 33 - 44 44 - 44 44 44 44 44 44

在Microsoft官方文档中,有一篇文档关于GUID与uniqueidentifier的值比较:Comparing GUID and uniqueidentifier Values

不同的数据库处理GUID的方式也是不同的:

1)在SQL Server存在内置GUID类型,没有原生GUID支持的数据库通过模拟方式来实现的;

2)在Oracle保存为raw bytes类型,具体类型为raw(16)

3)在MySql中通常将GUID储存为char(36)的字符串形式;

关于Oracle、MySql数据库的排序规则与.Net中排序规则,不过篇章的限制,这里不再做具体的演示。在github上提供了示例SQL语句:https://gist.github.com/tangdf/f0aed064ba10bfa0050e4344b9236889。我们在这里只给出最终的结论:

小结:

  1. .Net中GUID的排序规则是从左到右依次进行排序,与数字排序规则一致;
  2. Sql Server数据库提供对GUID类型的支持,在数据库中称为UniqueIdentifier类型,但是排序规则比较复杂:Oracle数据库未提供对GUID类型的支持,使用的是raw bytes类型保存数据raw(16),具体类型为,排序规则与GUID在.Net中规则一致;
    • 先按每1-8从左到右进行排序;
    • 接着按第9-10位从右到左进行排序;
    • 最后按后11-16位从右到左进行排序;
  3. Oracle数据库未提供对GUID类型的支持,使用的是raw bytes类型保存数据raw(16),具体类型为,排序规则与GUID在.Net中规则一致;
  4. MySql数据未提供对GUID类型的支持,使用的是字符串的类型保存数据,使用是的char(36)类型,由于使用的是字符串类型,排序规则与GUID在.Net中的规则一致。

三、有序GUID

有序GUID是有规则的生成GUID,保存在之后生成的GUID类型总是比之前的要大。不过在上一节中,已经提到过各个数据库对GUID支持不一样,而且排序的规则也不一样,所以我们需要为每一个数据库提供不一致的有序GUID生成规则。

UuidCreateSequential函数

我们都知道SQL Server数据库有一个NewSequentialId()函数,用于创建有序GUID。在创建表时,可以将它设置成为GUID类型字段的默认值,在插入新增数据时自动创建主键的值(该函数只能做为字段的默认值,不能直接在SQL中调用)。

示例:

 Create Table TestTable
(
ID UniqueIdentifier Not Null Default ( NewSequentialId() ) ,
Number Int
);

     NewSequentialId()函数只能在数据库使用,不过在 Microsoft 的 MSDN 文档中有说明,NEWSEQUENTIALID 是对 Windows UuidCreateSequential 函数的包装https://msdn.microsoft.com/zh-cn/library/ms189786(v=sql.120).aspx。这样我们可以在C#通过非托管方法调用:

 [System.Runtime.InteropServices.DllImport("rpcrt4.dll", SetLastError = true)]
private static extern int UuidCreateSequential(out Guid guid); public static Guid NewSequentialGuid()
{
const int RPC_S_OK = ; int result = UuidCreateSequential(out var guid);
if (result != RPC_S_OK) {
throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error());
} return guid;
}

不这个方法也存在三个问题:

  1. 这个方法涉及到安全问题,UuidCreateSequential函数依赖的计算硬件,该方法的后12位其实是网卡的MAC地址。这是我电脑生成的一组有序GUID。

{A2A93393-C8DC-11E7-B133-2C56DC497A97}
{A2A93394-C8DC-11E7-B133-2C56DC497A97}
{A2A93395-C8DC-11E7-B133-2C56DC497A97}
{A2A93396-C8DC-11E7-B133-2C56DC497A97}
{A2A93397-C8DC-11E7-B133-2C56DC497A97}
{A2A93398-C8DC-11E7-B133-2C56DC497A97}
{A2A93399-C8DC-11E7-B133-2C56DC497A97}
{A2A9339A-C8DC-11E7-B133-2C56DC497A97}
{A2A9339B-C8DC-11E7-B133-2C56DC497A97}
{A2A9339C-C8DC-11E7-B133-2C56DC497A97}

这是我电脑的网卡的MAC地址:

  1. 由于UuidCreateSequential函数生成的有序GUID中包括MAC地址,所以如果在服务器集群环境中,肯定存在一台服务器A上生成的有序GUID总比另一台服务器B生成要更小,服务器A产生的数据插入到数据库时,由于聚集索引的问题,总是会移动服务器B已经持久化到数据库中的数据。集群的服务器越多,产生的IO问题更严重。在服务器群集环境中,需要自行实现有序GUID。

  2. UuidCreateSequential函数生成的GUID规则与SQL Server中排序的规则存在不一致,这样仍然会导致严重的IO问题,所以需要将GUID重新排序后再持久化到数据库。例如上面列出生成的GUID列表,依次生成的数据可以看出,是第4位字节在自增长,在这与任何一个数据库的排序规则都不一致;关于该函数生成的规则,可以见此链接:https://stackoverflow.com/questions/5585307/sequential-guids

下面的方法是将生成的GUID调整成为适合Sql Server使用的有序GUID(针对其它数据库支持,您可以按排序规则自行修改):

 [System.Runtime.InteropServices.DllImport("rpcrt4.dll", SetLastError = true)]
static extern int UuidCreateSequential(byte[] buffer); static Guid NewSequentialGuid() { byte[] raw = new byte[];
if (UuidCreateSequential(raw) != )
throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error()); byte[] fix = new byte[]; // reverse 0..3
fix[0x0] = raw[0x3];
fix[0x1] = raw[0x2];
fix[0x2] = raw[0x1];
fix[0x3] = raw[0x0]; // reverse 4 & 5
fix[0x4] = raw[0x5];
fix[0x5] = raw[0x4]; // reverse 6 & 7
fix[0x6] = raw[0x7];
fix[0x7] = raw[0x6]; // all other are unchanged
fix[0x8] = raw[0x8];
fix[0x9] = raw[0x9];
fix[0xA] = raw[0xA];
fix[0xB] = raw[0xB];
fix[0xC] = raw[0xC];
fix[0xD] = raw[0xD];
fix[0xE] = raw[0xE];
fix[0xF] = raw[0xF]; return new Guid(fix);
}

小结:
     UuidCreateSequential函数存在隐私的问题,不适合集群环境,并且需要重新排序后再提交到数据库;

COMB解决方案

COMB 类型的GUID 是由Jimmy Nilsson在他的“The Cost of GUIDs as Primary Keys”一文中设计出来的。
     基本设计思路是这样的:既然GUID数据生成是随机的造成索引效率低下,影响了系统的性能,那么能不能通过组合的方式,保留GUID的前10个字节,用后6个字节表示GUID生成的时间(DateTime),这样我们将时间信息与GUID组合起来,在保留GUID的唯一性的同时增加了有序性,以此来提高索引效率(这是针对Sql
Server数据库来设计的)。

在NHibernate框架中已经实现该功能,可以在github上看到实现方式:https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs#L25-L72

在EF以及EF Core也同样实现了类似的解决方案,EF Core的实现方式:https://github.com/aspnet/EntityFrameworkCore/blob/f7f6d6e23c8e47e44a61983827d9e41f2afe5cc7/src/EFCore/ValueGeneration/SequentialGuidValueGenerator.cs#L25-L44

在这里介绍一下使用的方式,由EF Core框架自动生成有序GUID的方式:

 public class SampleDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<GuidEntity>(b =>
{
b.Property(e => e.Id).HasValueGenerator<SequentialGuidValueGenerator>();
});
}
}

但是请注意,这两个ORM的解决方案只针对Sql Server数据库,因为只保证了最后几位字节是按顺序来生成的。

SequentialGuid框架

SequentialGuid框架也是我要推荐给您,因为它提供了常见数据库生成有序Guid的解决方案。

关于该框架的设计思路以及针对各个数据库的性能测试,见链接:https://www.codeproject.com/Articles/388157/GUIDs-as-fast-primary-keys-under-multiple-database

使用方式,建议您参考ABP框架,在ABP中使用SequentialGuid框架来生成有序GUID,关键代码链接:https://github.com/aspnetboilerplate/aspnetboilerplate/blob/b36855f0c238c3592203f058c641862844a0614e/src/Abp/SequentialGuidGenerator.cs#L36-L51

总结

我们来总结一下:

  1. 在数据库中最好不要使用随机的GUID,它会影响性能;
  2. 在SQL Server中提供了NewSequentialId函数来生成有序GUID;
  3. 各个数据库对GUID支持的不一样,而且排序的规则也不一样;
  4. UuidCreateSequential函数存在隐私的问题,不适合集群环境,并且需要重新排序后再提交到数据库;
  5. 各ORM框架提供了有序GUID的支持,但是其实只是针对Sql Server数据库设计的;
  6. 推荐您使用SequentialGuid框架,它解决了多数据库以及集群环境的问题。

转载链接:http://www.cnblogs.com/tdfblog/p/SequentialGuid.html

针对多类型数据库,集群数据库的有序GUID的更多相关文章

  1. SequoiaDB数据库集群部署

    一般在多机环境下部署数据库的集群模式是比较繁琐的,下面我来分享一个如何通过shell脚本的方式简单.方便地部署我们的集群. 首先,我们要给机器配置信任关系,这样我们就无需手动的输入密码来执行ssh和s ...

  2. H2数据库集群

    H2数据库集群 1. H2数据库简单介绍 1.1 H2数据库优势 经常使用的开源数据库:H2,Derby,HSQLDB.MySQL,PostgreSQL. 当中H2,HSQLDB相似,十分适合作为嵌入 ...

  3. [原创]Greenplum数据库集群实践

    GreenPlum实践 ============================================== 目录: 一.安装环境准备 二.GP数据库安装 三.集群添加standby节点 四. ...

  4. mycat数据库集群系列之mysql主从同步设置

    最近在梳理数据库集群的相关操作,现在花点时间整理一下关于mysql数据库集群的操作总结,恰好你又在看这一块,供一份参考.本次系列终结大概包括以下内容:多数据库安装.mycat部署安装.数据库之读写分离 ...

  5. mycat数据库集群系列之mycat读写分离安装配置

    最近在梳理数据库集群的相关操作,现在花点时间整理一下关于mysql数据库集群的操作总结,恰好你又在看这一块,供一份参考.本次系列终结大概包括以下内容:多数据库安装.mycat部署安装.数据库之读写分离 ...

  6. 聊一聊mycat数据库集群系列之双主双重实现

    最近在梳理数据库集群的相关操作,现在花点时间整理一下关于mysql数据库集群的操作总结,恰好你又在看这一块,供一份参考.本次系列终结大概包括以下内容:多数据库安装.mycat部署安装.数据库之读写分离 ...

  7. 【Data Cluster】真机环境下MySQL数据库集群搭建

    真机环境下MySQL-Cluster搭建文档  摘要:本年伊始阶段,由于实验室对不同数据库性能测试需求,才出现MySQL集群搭建.购置主机,交换机,双绞线等一系列准备工作就绪,也就开始集群搭建.起初笔 ...

  8. MySQL数据库集群进行正确配置步骤

    MySQL数据库集群进行正确配置步骤 2010-06-09 10:47 arrowcat 博客园 字号:T | T 我们今天是要和大家一起分享的是对MySQL数据库集群进行正确配置,我前两天在相关网站 ...

  9. Facebook揭密:如何让MySQL数据库集群自主运行

    Facebook运行着全球最大的MySQL数据库集群,该集群分布在两个大洲上的多个数据中心中数以千计的服务器上.让人不解的是,Facebook只动用了一个很小的团队来管理这个庞大的MySQL数据库集群 ...

  10. 手把手教你用Mysql-Cluster-7.5搭建数据库集群

    前言 当你的业务到达一定的当量,肯定需要一定数量的数据库来负载均衡你的数据库请求,我在之前的博客中已经说明了,如何实现负载均衡,但是还有一个问题就是数据同步,因为负载均衡的前提就是,各个服务器的数据库 ...

随机推荐

  1. 二,PHP会话机制---session的基本使用

    1,思考:登录网站后,在每个网页都能拿到用户信息 (1) 使用超链接传递用户名,这样太繁琐了,不建议使用 . (2) 使用数据库,每打开一个页面都查询一次用户信息表,这样网页加载速度变慢,用户体验变差 ...

  2. 恢复xfs文件系统superblock实验

    1. 创建一个XFS文件系统[root@localhost ~]# mkfs.xfs -f /dev/vdb1meta-data=/dev/vdb1              isize=256    ...

  3. Linux必备命令

      目录                                                              概述 常用系统工作命令 系统状态检测命令 工作目录切换命令 文本文件 ...

  4. POJ 1111

    #include<iostream> #define MAXN 30 using namespace std; char _m[MAXN][MAXN]; bool mark[MAXN][M ...

  5. Linux Cluster环境下批量分发执行补丁

    转自:http://blog.csdn.net/napolunyishi/article/details/18219867 这两天做了一个需求,因为上一个版本的/tmp空间默认只分配了5G,而升级程序 ...

  6. asp.net MVC 多系统目录结构

    学习了几天的mvc5,发现vs把所有的控制器都放在同一个目录Controllers目录下,细想一下,假如一个项目包含几个系统: 行政办公系统.培训管理系统.督办管理系统.会议管理系统…… 如果还把控制 ...

  7. Windows下PATH等环境变量详解

    在学习JAVA的过程中,涉及到多个环境变量(environment variable)的概念,如PATH.正确地配置这些环境变量,是能够顺利学习.开发的前提.而经常出现的问题是:有的学习者能够按照提示 ...

  8. sql 保留两位小数

    select convert(decimal(18,2),1800.2669)

  9. safari input默认样式

    在i标签下嵌套一个input标签  设置了 -webkit-apprarence:none: 设置了宽高,和padding:3px 结果placeholder显示不全 经排查 这时候的input默认有 ...

  10. 一条命令在Centos7中换163 yum源、安装python3并与python2共存、使用豆瓣pip源加速

    打开一个Terminal: 换yum源: mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup & ...