针对多类型数据库,集群数据库的有序GUID
一、背景
???? 常見(jiàn)的一種數(shù)據(jù)庫(kù)設(shè)計(jì)是使用連續(xù)的整數(shù)為做主鍵,當(dāng)新的數(shù)據(jù)插入到數(shù)據(jù)庫(kù)時(shí),由數(shù)據(jù)庫(kù)自動(dòng)生成。但這種設(shè)計(jì)不一定適合所有場(chǎng)景。
???? 隨著越來(lái)越多的使用Nhibernate、EntityFramework等ORM(對(duì)象關(guān)系映射)框架,應(yīng)用程序被設(shè)計(jì)成為工作單元(Unit Of Work)模式,需要在數(shù)據(jù)持久化之前生成主鍵,為了保證在多線程并發(fā)以及站點(diǎn)集群環(huán)境中主鍵的唯一性,最簡(jiǎn)單最常見(jiàn)的方式是將主鍵設(shè)計(jì)成為GUID類型。
???? (工作單元:是數(shù)據(jù)庫(kù)應(yīng)用程序經(jīng)常使用的一種設(shè)計(jì)模式,簡(jiǎn)單一點(diǎn)來(lái)說(shuō),就是對(duì)多個(gè)數(shù)據(jù)庫(kù)操作進(jìn)行打包,記錄對(duì)象上的所有變化,并在最后提交時(shí)一次性將所有變化通過(guò)系統(tǒng)事務(wù)寫入數(shù)據(jù)庫(kù)。目的是為了減少數(shù)據(jù)庫(kù)調(diào)用次數(shù)以及避免數(shù)據(jù)庫(kù)長(zhǎng)事務(wù)。)
???? GUID(全球唯一標(biāo)識(shí)符)也稱為UUID,是一種由算法生成的二進(jìn)制長(zhǎng)度為128位的數(shù)字標(biāo)識(shí)符。在理想情況下,任何計(jì)算機(jī)和計(jì)算機(jī)集群都不會(huì)生成兩個(gè)相同的GUID。GUID 的總數(shù)達(dá)到了2^128(3.4×10^38)個(gè),所以隨機(jī)生成兩個(gè)相同GUID的可能性非常小,但并不為0。GUID一詞有時(shí)也專指微軟對(duì)UUID標(biāo)準(zhǔn)的實(shí)現(xiàn)。
???? (RFC 41222描述了創(chuàng)建標(biāo)準(zhǔn)GUID,如今大多數(shù)GUID生成算法通常是一個(gè)很長(zhǎng)的隨機(jī)數(shù),再結(jié)合一些像網(wǎng)絡(luò)MAC地址這種隨機(jī)的本地組件信息。)
???? GUID的優(yōu)點(diǎn)允許開(kāi)發(fā)人員隨時(shí)創(chuàng)建新值,而無(wú)需從數(shù)據(jù)庫(kù)服務(wù)器檢查值的唯一性,這似乎是一個(gè)完美的解決方案。
???? 很多數(shù)據(jù)庫(kù)在創(chuàng)建主鍵時(shí),為了充分發(fā)揮數(shù)據(jù)庫(kù)的性能,會(huì)自動(dòng)在該列上創(chuàng)建聚集索引。
???? 我們先來(lái)說(shuō)一說(shuō)什么是聚集索引。集索引確定表中數(shù)據(jù)的物理順序。聚集索引類似于電話簿,按姓氏排列數(shù)據(jù)。由于聚集索引規(guī)定數(shù)據(jù)在表中的物理存儲(chǔ)順序,因此一個(gè)表也只能包含一個(gè)聚集索引。它能夠快速查找到數(shù)據(jù),但是如果插入數(shù)據(jù)庫(kù)的主鍵不在列表的末尾,向表中添加新行時(shí)就非常緩慢。例如,看下面這個(gè)例子,在表中已經(jīng)存在三行數(shù)據(jù):
????
???? 此時(shí)非常簡(jiǎn)單:數(shù)據(jù)行按對(duì)應(yīng)ID列的順序儲(chǔ)存。如果我們新添加一行ID為8的數(shù)據(jù),不會(huì)產(chǎn)生任何問(wèn)題,新行會(huì)追加的末尾。
?????
???? 但如果我們想插入一行的ID為5的數(shù)據(jù):
???? ?
???? ID為7,8的數(shù)據(jù)行必須向下移動(dòng)。雖然在這算什么事兒,但當(dāng)您的數(shù)據(jù)量達(dá)到數(shù)百萬(wàn)行的級(jí)別之后,這就是個(gè)問(wèn)題了。如果你還想要每秒處理上百次這種請(qǐng)求,那可真是難上加難了。
???? 這就是GUID主鍵引發(fā)的問(wèn)題:它是隨機(jī)產(chǎn)生的,所以在數(shù)據(jù)插入時(shí),隨時(shí)都會(huì)涉及到數(shù)據(jù)的移動(dòng),導(dǎo)致插入會(huì)很緩慢,還會(huì)涉及大量不必要的磁盤活動(dòng)。總結(jié)果有兩點(diǎn):
- 空間的浪費(fèi)以及由此帶來(lái)的讀寫效率的下降;
- 更主要的,存儲(chǔ)的碎片化以及由此帶來(lái)的讀寫效率嚴(yán)重下降。
???? ?GUID最關(guān)鍵的問(wèn)題就是它是隨機(jī)的。我們需要設(shè)計(jì)一種有規(guī)則的GUID生成方式,在之后生成的GUID類型總是比之前的要大,保證插入數(shù)據(jù)庫(kù)的主鍵是在列表末尾追加的,這種我們稱之為有序GUID。
?
二、GUID排序規(guī)則
???? 在講解有序GUID之前,我們必須先了解一下GUID在.Net中以及各個(gè)數(shù)據(jù)庫(kù)中的排序規(guī)則,排序規(guī)則不一樣,生成有序GUID的規(guī)則也會(huì)隨之變化。
???? 128位的GUID主要有4部分組成:Data1, Data2, Data3, and Data4,你可以看成下面這樣:
11111111-2222-3333-4444-444444444444
???? Data1占4個(gè)字節(jié)、Data2占2個(gè)字節(jié)、 Data3占2個(gè)字節(jié)、Data4占8個(gè)字節(jié)。我們分別的對(duì)個(gè)字節(jié)編上序號(hào):
| Value | 11 | 11 | 11 | 11 | - | 22 | 22 | - | 33 | 33 | - | 44 | 44 | - | 44 | 44 | 44 | 44 | 44 | 44 |
?
?
?
???? GUID在.Net中的排序規(guī)則
???? 在.Net中,GUID默認(rèn)的排序過(guò)段規(guī)則是按左到右的,看下面這個(gè)示例。
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 }???? 輸出結(jié)果:
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
???? 通過(guò)上面的輸出結(jié)果,我們可以得到排序的權(quán)重如下:
| 權(quán)重 | 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 |
?
?
?
????? 這與數(shù)字排序規(guī)則一致,從右到左進(jìn)行依次進(jìn)行排序(數(shù)字越小,權(quán)重越高,排序的優(yōu)先級(jí)越高)。
???? GUID在各個(gè)數(shù)據(jù)庫(kù)中的排序規(guī)則
???? 在SQL Server數(shù)據(jù)庫(kù)中,我們有一種非常簡(jiǎn)單的方式來(lái)比較兩個(gè)GUID類型的大小值(其實(shí)在SQL Server數(shù)據(jù)庫(kù)中稱為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???? 查詢結(jié)果:
| 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 |
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
???? 通過(guò)上面可以得于是如下結(jié)果:
???? 通過(guò)分析,我們可得到如下權(quán)重列表:
| 權(quán)重 | 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官方文檔中,有一篇文檔關(guān)于GUID與uniqueidentifier的值比較:Comparing GUID and uniqueidentifier Values。
???? 不同的數(shù)據(jù)庫(kù)處理GUID的方式也是不同的:
???? ???? 1)在SQL Server存在內(nèi)置GUID類型,沒(méi)有原生GUID支持的數(shù)據(jù)庫(kù)通過(guò)模擬方式來(lái)實(shí)現(xiàn)的;
???? ???? 2)在Oracle保存為raw bytes類型,具體類型為raw(16);
???? ???? 3)在MySql中通常將GUID儲(chǔ)存為char(36)的字符串形式;
???? 關(guān)于Oracle、MySql數(shù)據(jù)庫(kù)的排序規(guī)則與.Net中排序規(guī)則,不過(guò)篇章的限制,這里不再做具體的演示。在github上提供了示例SQL語(yǔ)句:https://gist.github.com/tangdf/f0aed064ba10bfa0050e4344b9236889。我們?cè)谶@里只給出最終的結(jié)論:
小結(jié):
- 先按每1-8從左到右進(jìn)行排序;
- 接著按第9-10位從右到左進(jìn)行排序;
- 最后按后11-16位從右到左進(jìn)行排序;
?
三、有序GUID
???? 有序GUID是有規(guī)則的生成GUID,保存在之后生成的GUID類型總是比之前的要大。不過(guò)在上一節(jié)中,已經(jīng)提到過(guò)各個(gè)數(shù)據(jù)庫(kù)對(duì)GUID支持不一樣,而且排序的規(guī)則也不一樣,所以我們需要為每一個(gè)數(shù)據(jù)庫(kù)提供不一致的有序GUID生成規(guī)則。
???? UuidCreateSequential函數(shù)
???? 我們都知道SQL Server數(shù)據(jù)庫(kù)有一個(gè)NewSequentialId()函數(shù),用于創(chuàng)建有序GUID。在創(chuàng)建表時(shí),可以將它設(shè)置成為GUID類型字段的默認(rèn)值,在插入新增數(shù)據(jù)時(shí)自動(dòng)創(chuàng)建主鍵的值(該函數(shù)只能做為字段的默認(rèn)值,不能直接在SQL中調(diào)用)。
???? 示例:
1 Create Table TestTable 2 ( 3 ID UniqueIdentifier Not Null Default ( NewSequentialId() ) , 4 Number Int 5 );???? NewSequentialId()函數(shù)只能在數(shù)據(jù)庫(kù)使用,不過(guò)在 Microsoft 的 MSDN 文檔中有說(shuō)明,NEWSEQUENTIALID 是對(duì) Windows UuidCreateSequential 函數(shù)的包裝,https://msdn.microsoft.com/zh-cn/library/ms189786(v=sql.120).aspx。這樣我們可以在C#通過(guò)非托管方法調(diào)用:
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 }???? 不這個(gè)方法也存在三個(gè)問(wèn)題:
{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}
???? 這是我電腦的網(wǎng)卡的MAC地址:
????
由于UuidCreateSequential函數(shù)生成的有序GUID中包括MAC地址,所以如果在服務(wù)器集群環(huán)境中,肯定存在一臺(tái)服務(wù)器A上生成的有序GUID總比另一臺(tái)服務(wù)器B生成要更小,服務(wù)器A產(chǎn)生的數(shù)據(jù)插入到數(shù)據(jù)庫(kù)時(shí),由于聚集索引的問(wèn)題,總是會(huì)移動(dòng)服務(wù)器B已經(jīng)持久化到數(shù)據(jù)庫(kù)中的數(shù)據(jù)。集群的服務(wù)器越多,產(chǎn)生的IO問(wèn)題更嚴(yán)重。在服務(wù)器群集環(huán)境中,需要自行實(shí)現(xiàn)有序GUID。
UuidCreateSequential函數(shù)生成的GUID規(guī)則與SQL Server中排序的規(guī)則存在不一致,這樣仍然會(huì)導(dǎo)致嚴(yán)重的IO問(wèn)題,所以需要將GUID重新排序后再持久化到數(shù)據(jù)庫(kù)。例如上面列出生成的GUID列表,依次生成的數(shù)據(jù)可以看出,是第4位字節(jié)在自增長(zhǎng),在這與任何一個(gè)數(shù)據(jù)庫(kù)的排序規(guī)則都不一致;關(guān)于該函數(shù)生成的規(guī)則,可以見(jiàn)此鏈接:https://stackoverflow.com/questions/5585307/sequential-guids。
???? 下面的方法是將生成的GUID調(diào)整成為適合Sql Server使用的有序GUID(針對(duì)其它數(shù)據(jù)庫(kù)支持,您可以按排序規(guī)則自行修改):
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 23 fix[0x6] = raw[0x7]; 24 fix[0x7] = raw[0x6]; 25 26 // all other are unchanged 27 fix[0x8] = raw[0x8]; 28 fix[0x9] = raw[0x9]; 29 fix[0xA] = raw[0xA]; 30 fix[0xB] = raw[0xB]; 31 fix[0xC] = raw[0xC]; 32 fix[0xD] = raw[0xD]; 33 fix[0xE] = raw[0xE]; 34 fix[0xF] = raw[0xF]; 35 36 return new Guid(fix); 37 }?小結(jié):
???? UuidCreateSequential函數(shù)存在隱私的問(wèn)題,不適合集群環(huán)境,并且需要重新排序后再提交到數(shù)據(jù)庫(kù);
???? COMB解決方案
???? COMB 類型的GUID 是由Jimmy Nilsson在他的“The Cost of GUIDs as Primary Keys”一文中設(shè)計(jì)出來(lái)的。
???? 基本設(shè)計(jì)思路是這樣的:既然GUID數(shù)據(jù)生成是隨機(jī)的造成索引效率低下,影響了系統(tǒng)的性能,那么能不能通過(guò)組合的方式,保留GUID的前10個(gè)字節(jié),用后6個(gè)字節(jié)表示GUID生成的時(shí)間(DateTime),這樣我們將時(shí)間信息與GUID組合起來(lái),在保留GUID的唯一性的同時(shí)增加了有序性,以此來(lái)提高索引效率(這是針對(duì)Sql Server數(shù)據(jù)庫(kù)來(lái)設(shè)計(jì)的)。
???? 在NHibernate框架中已經(jīng)實(shí)現(xiàn)該功能,可以在github上看到實(shí)現(xiàn)方式:https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs#L25-L72。
???? 在EF以及EF Core也同樣實(shí)現(xiàn)了類似的解決方案,EF Core的實(shí)現(xiàn)方式:https://github.com/aspnet/EntityFrameworkCore/blob/f7f6d6e23c8e47e44a61983827d9e41f2afe5cc7/src/EFCore/ValueGeneration/SequentialGuidValueGenerator.cs#L25-L44
???? 在這里介紹一下使用的方式,由EF Core框架自動(dòng)生成有序GUID的方式:
1 public class SampleDbContext : DbContext 2 { 3 protected override void OnModelCreating(ModelBuilder modelBuilder) 4 { 5 modelBuilder.Entity<GuidEntity>(b => 6 { 7 b.Property(e => e.Id).HasValueGenerator<SequentialGuidValueGenerator>(); 8 }); 9 } 10 }?但是請(qǐng)注意,這兩個(gè)ORM的解決方案只針對(duì)Sql Server數(shù)據(jù)庫(kù),因?yàn)橹槐WC了最后幾位字節(jié)是按順序來(lái)生成的。
???? SequentialGuid框架
???? SequentialGuid框架也是我要推薦給您,因?yàn)樗峁┝顺R?jiàn)數(shù)據(jù)庫(kù)生成有序Guid的解決方案。
???? 關(guān)于該框架的設(shè)計(jì)思路以及針對(duì)各個(gè)數(shù)據(jù)庫(kù)的性能測(cè)試,見(jiàn)鏈接:https://www.codeproject.com/Articles/388157/GUIDs-as-fast-primary-keys-under-multiple-database。
???? 使用方式,建議您參考ABP框架,在ABP中使用SequentialGuid框架來(lái)生成有序GUID,關(guān)鍵代碼鏈接:https://github.com/aspnetboilerplate/aspnetboilerplate/blob/b36855f0c238c3592203f058c641862844a0614e/src/Abp/SequentialGuidGenerator.cs#L36-L51。
總結(jié)
我們來(lái)總結(jié)一下:
?
?轉(zhuǎn)載鏈接:http://www.cnblogs.com/tdfblog/p/SequentialGuid.html
轉(zhuǎn)載于:https://www.cnblogs.com/supersnowyao/p/8335397.html
總結(jié)
以上是生活随笔為你收集整理的针对多类型数据库,集群数据库的有序GUID的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 金额 输入实现
- 下一篇: 从本地上传项目到 github 以及从g