一、背景 常見的一種資料庫設計是使用連續的整數為做主鍵,當新的數據插入到資料庫時,由資料庫自動生成。但這種設計不一定適合所有場景。 隨著越來越多的使用Nhibernate、EntityFramework等ORM(對象關係映射)框架,應用程式被設計成為工作單元(Unit Of Work)模式,需要在數 ...
一、背景
常見的一種資料庫設計是使用連續的整數為做主鍵,當新的數據插入到資料庫時,由資料庫自動生成。但這種設計不一定適合所有場景。
隨著越來越多的使用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預設的排序過段規則是按左到右的,看下麵這個示例。
1 var list = new List<Guid> {
2 new Guid("00000000-0000-0000-0000-010000000000"),
3 new Guid("00000000-0000-0000-0000-000100000000"),
4 new Guid("00000000-0000-0000-0000-000001000000"),
5 new Guid("00000000-0000-0000-0000-000000010000"),
6 new Guid("00000000-0000-0000-0000-000000000100"),
7 new Guid("00000000-0000-0000-0000-000000000001"),
8 new Guid("00000000-0000-0000-0100-000000000000"),
9 new Guid("00000000-0000-0000-0010-000000000000"),
10 new Guid("00000000-0000-0001-0000-000000000000"),
11 new Guid("00000000-0000-0100-0000-000000000000"),
12 new Guid("00000000-0001-0000-0000-000000000000"),
13 new Guid("00000000-0100-0000-0000-000000000000"),
14 new Guid("00000001-0000-0000-0000-000000000000"),
15 new Guid("00000100-0000-0000-0000-000000000000"),
16 new Guid("00010000-0000-0000-0000-000000000000"),
17 new Guid("01000000-0000-0000-0000-000000000000")
18 };
19 list.Sort();
20
21 foreach (Guid guid in list) {
22 Console.WriteLine(guid.ToString());
23 }
輸出結果:
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
類型):
1 With UIDs As (-- 0 1 2 3 4 5 6 7 8 9 A B C D E F
2 Select ID = 1, UID = cast ('00000000-0000-0000-0000-010000000000' as uniqueidentifier)
3 Union Select ID = 2, UID = cast ('00000000-0000-0000-0000-000100000000' as uniqueidentifier)
4 Union Select ID = 3, UID = cast ('00000000-0000-0000-0000-000001000000' as uniqueidentifier)
5 Union Select ID = 4, UID = cast ('00000000-0000-0000-0000-000000010000' as uniqueidentifier)
6 Union Select ID = 5, UID = cast ('00000000-0000-0000-0000-000000000100' as uniqueidentifier)
7 Union Select ID = 6, UID = cast ('00000000-0000-0000-0000-000000000001' as uniqueidentifier)
8 Union Select ID = 7, UID = cast ('00000000-0000-0000-0100-000000000000' as uniqueidentifier)
9 Union Select ID = 8, UID = cast ('00000000-0000-0000-0010-000000000000' as uniqueidentifier)
10 Union Select ID = 9, UID = cast ('00000000-0000-0001-0000-000000000000' as uniqueidentifier)
11 Union Select ID = 10, UID = cast ('00000000-0000-0100-0000-000000000000' as uniqueidentifier)
12 Union Select ID = 11, UID = cast ('00000000-0001-0000-0000-000000000000' as uniqueidentifier)
13 Union Select ID = 12, UID = cast ('00000000-0100-0000-0000-000000000000' as uniqueidentifier)
14 Union Select ID = 13, UID = cast ('00000001-0000-0000-0000-000000000000' as uniqueidentifier)
15 Union Select ID = 14, UID = cast ('00000100-0000-0000-0000-000000000000' as uniqueidentifier)
16 Union Select ID = 15, UID = cast ('00010000-0000-0000-0000-000000000000' as uniqueidentifier)
17 Union Select ID = 16, UID = cast ('01000000-0000-0000-0000-000000000000' as uniqueidentifier)
18 )
19 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-8從左到右進行排序;
- 接著按第9-10位從右到左進行排序;
- 最後按後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。我們在這裡只給出最終的結論:
小結:
- .Net中GUID的排序規則是從左到右依次進行排序,與數字排序規則一致;
- Sql Server資料庫提供對GUID類型的支持,在資料庫中稱為
UniqueIdentifier
類型,但是排序規則比較複雜:Oracle資料庫未提供對GUID類型的支持,使用的是raw bytes類型保存數據raw(16),具體類型為,排序規則與GUID在.Net中規則一致;
- 先按每1-8從左到右進行排序;
- 接著按第9-10位從右到左進行排序;
- 最後按後11-16位從右到左進行排序;
- Oracle資料庫未提供對GUID類型的支持,使用的是raw bytes類型保存數據raw(16),具體類型為,排序規則與GUID在.Net中規則一致;
- MySql數據未提供對GUID類型的支持,使用的是字元串的類型保存數據,使用是的char(36)類型,由於使用的是字元串類型,排序規則與GUID在.Net中的規則一致。
三、有序GUID
有序GUID是有規則的生成GUID,保存在之後生成的GUID類型總是比之前的要大。不過在上一節中,已經提到過各個資料庫對GUID支持不一樣,而且排序的規則也不一樣,所以我們需要為每一個資料庫提供不一致的有序GUID生成規則。
UuidCreateSequential函數
我們都知道SQL Server資料庫有一個NewSequentialId()
函數,用於創建有序GUID。在創建表時,可以將它設置成為GUID類型欄位的預設值,在插入新增數據時自動創建主鍵的值(該函數只能做為欄位的預設值,不能直接在SQL中調用)。
示例:
1 Create Table TestTable
2 (
3 ID UniqueIdentifier Not Null Default ( NewSequentialId() ) ,
4 Number Int
5 );
NewSequentialId()
函數只能在資料庫使用,不過在 Microsoft 的 MSDN 文檔中有說明,NEWSEQUENTIALID 是對 Windows UuidCreateSequential 函數的包裝,https://msdn.microsoft.com/zh-cn/library/ms189786(v=sql.120).aspx。這樣我們可以在C#通過非托管方法調用:
1 [System.Runtime.InteropServices.DllImport("rpcrt4.dll", SetLastError = true)]
2 private static extern int UuidCreateSequential(out Guid guid);
3
4 public static Guid NewSequentialGuid()
5 {
6 const int RPC_S_OK = 0;
7
8 int result = UuidCreateSequential(out var guid);
9 if (result != RPC_S_OK) {
10 throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error());
11 }
12
13 return guid;
14 }
不這個方法也存在三個問題:
- 這個方法涉及到安全問題,
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地址:
-
由於
UuidCreateSequential
函數生成的有序GUID中包括MAC地址,所以如果在伺服器集群環境中,肯定存在一臺伺服器A上生成的有序GUID總比另一臺伺服器B生成要更小,伺服器A產生的數據插入到資料庫時,由於聚集索引的問題,總是會移動伺服器B已經持久化到資料庫中的數據。集群的伺服器越多,產生的IO問題更嚴重。在伺服器群集環境中,需要自行實現有序GUID。 -
UuidCreateSequential
函數生成的GUID規則與SQL Server中排序的規則存在不一致,這樣仍然會導致嚴重的IO問題,所以需要將GUID重新排序後再持久化到資料庫。例如上面列出生成的GUID列表,依次生成的數據可以看出,是第4位位元組在自增長,在這與任何一個資料庫的排序規則都不一致;關於該函數生成的規則,可以見此鏈接:https://stackoverflow.com/questions/5585307/sequential-guids。
下麵的方法是將生成的GUID調整成為適合Sql Server使用的有序GUID(針對其它資料庫支持,您可以按排序規則自行修改):
1 [System.Runtime.InteropServices.DllImport("rpcrt4.dll", SetLastError = true)]
2 static extern int UuidCreateSequential(byte[] buffer);
3
4 static Guid NewSequentialGuid() {
5
6 byte[] raw = new byte[16];
7 if (UuidCreateSequential(raw) != 0)
8 throw new System.ComponentModel.Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error());
9
10 byte[] fix = new byte[16];
11
12 // reverse 0..3
13 fix[0x0] = raw[0x3];
14 fix[0x1] = raw[0x2];
15 fix[0x2] = raw[0x1];
16 fix[0x3] = raw[0x0];
17
18 // reverse 4 & 5
19 fix[0x4] = raw[0x5];
20 fix[0x5] = raw[0x4];
21
22 // reverse 6 & 7