如何使用有序GUID提升数据库读写性能
源寶導(dǎo)讀:數(shù)據(jù)庫設(shè)計(jì)時(shí),經(jīng)常會(huì)使用GUID作為表的主鍵,但由于GUID的隨機(jī)性會(huì)導(dǎo)致數(shù)據(jù)庫在讀寫數(shù)據(jù)時(shí)效率嚴(yán)重下降,影響應(yīng)用程序整體性能。本文將深入探討如何通過使用有序GUID提升數(shù)據(jù)讀寫的性能。
一、背景
? ??常見的數(shù)據(jù)庫設(shè)計(jì)是使用連續(xù)的整數(shù)為做主鍵,當(dāng)新的數(shù)據(jù)插入到數(shù)據(jù)庫時(shí),由數(shù)據(jù)庫自動(dòng)生成,但這種設(shè)計(jì)不一定適合所有場(chǎng)景。
? 隨著越來越多的應(yīng)用程序使用Nhibernate、Entity Framework Core等ORM(對(duì)象關(guān)系映射)框架,應(yīng)用被設(shè)計(jì)成為工作單元(Unit Of Work)模式,需要在數(shù)據(jù)持久化之前生成主鍵,解決主實(shí)體與子系統(tǒng)的依賴關(guān)系;為了保證在多線程并發(fā)以及站點(diǎn)集群環(huán)境中主鍵的唯一性,最簡(jiǎn)單最常見的方式是將主鍵設(shè)計(jì)成為GUID類型。
? ? 工作單元是數(shù)據(jù)庫應(yīng)用程序經(jīng)常使用的一種設(shè)計(jì)模式,簡(jiǎn)單一點(diǎn)來說,就是對(duì)多個(gè)數(shù)據(jù)庫操作進(jìn)行打包,記錄對(duì)象上的所有變化,并在最后提交時(shí)一次性將所有變化通過系統(tǒng)事務(wù)寫入數(shù)據(jù)庫。目的是為了減少數(shù)據(jù)庫調(diào)用次數(shù)以及避免數(shù)據(jù)庫長(zhǎng)事務(wù)。關(guān)于工作單元的知識(shí)可以在各類博客網(wǎng)站中都有說明,在這里就不做詳細(xì)的介紹了。
? ? GUID(全球唯一標(biāo)識(shí)符)也稱為UUID,是一種由算法生成的二進(jìn)制長(zhǎng)度為128位的數(shù)字標(biāo)識(shí)符。在理想情況下,任何計(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)允許開發(fā)人員隨時(shí)創(chuàng)建新值,而無需從數(shù)據(jù)庫服務(wù)器檢查值的唯一性,這似乎是一個(gè)完美的解決方案。
? ? 很多數(shù)據(jù)庫在創(chuàng)建主鍵時(shí),為了充分發(fā)揮數(shù)據(jù)庫的性能,會(huì)自動(dòng)在該列上創(chuàng)建聚集索引。我們先來說一說什么是聚集索引。集索引確定表中數(shù)據(jù)的物理順序,類似于電話簿,按姓氏排列數(shù)據(jù)。由于聚集索引規(guī)定數(shù)據(jù)在表中的物理存儲(chǔ)順序,因此一個(gè)表也只能包含一個(gè)聚集索引。它能夠快速查找到數(shù)據(jù),但是如果插入數(shù)據(jù)庫的主鍵不在列表的末尾,向表中添加新行時(shí)就非常緩慢。例如,看下面這個(gè)例子,在表中已經(jīng)存在三行數(shù)據(jù)(例子來自Jeremy Todd的博客《GUIDs as fast primary keys under multiple databases》):
? ? 此時(shí)非常簡(jiǎn)單:數(shù)據(jù)行按對(duì)應(yīng)ID列的順序儲(chǔ)存。如果我們新添加一行ID為8的數(shù)據(jù),不會(huì)產(chǎn)生任何問題,新行會(huì)追加的末尾。
? ? 但如果我們想插入一行的ID為5的數(shù)據(jù)。
? ? ID為7,8的數(shù)據(jù)行必須向下移動(dòng)。雖然在這算什么事兒,但當(dāng)您的數(shù)據(jù)量達(dá)到數(shù)百萬行的級(jí)別之后,這就是個(gè)問題了。如果您還想要每秒處理上百次這種請(qǐng)求,那可真是難上加難了。
? ? 這就是GUID主鍵引發(fā)的問題:它是隨機(jī)產(chǎn)生的,所以在數(shù)據(jù)插入時(shí),隨時(shí)都會(huì)涉及到數(shù)據(jù)的移動(dòng),導(dǎo)致插入會(huì)很緩慢,還會(huì)涉及大量不必要的磁盤活動(dòng)。根據(jù)數(shù)據(jù)庫的存儲(chǔ)的相關(guān)知識(shí),會(huì)帶如下兩點(diǎn)問題:
空間的浪費(fèi)以及由此帶來的讀寫效率的下降;
更主要的,存儲(chǔ)的碎片化以及由此帶來的讀寫效率嚴(yán)重下降。
? ? GUID最關(guān)鍵的問題就是它是隨機(jī)的。我們需要設(shè)計(jì)一種有規(guī)則的GUID生成方式,在之后生成的GUID類型總是比之前的要大,保證插入數(shù)據(jù)庫的主鍵是在表數(shù)據(jù)的末尾追加的,這種我們稱之為有序GUID。
二、GUID排序規(guī)則
? ? 在講解有序GUID之前,我們必須先了解一下GUID在.Net中以及各個(gè)數(shù)據(jù)庫中的排序規(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ì)各字節(jié)編上序號(hào):
GUID在.Net中的排序規(guī)則
? ? 在.Net中,GUID默認(rèn)的排序規(guī)則是按左到右的,看下面這個(gè)示例。
? ? 輸出結(jié)果:
? ? 通過上面的輸出結(jié)果,我們可以得到排序的權(quán)重如下
? ? 這與數(shù)字排序規(guī)則一致,從右到左進(jìn)行依次進(jìn)行排序(數(shù)字越小,權(quán)重越高,排序的優(yōu)先級(jí)越高)。
GUID在各個(gè)數(shù)據(jù)庫中的排序規(guī)則
? ? 在SQL Server數(shù)據(jù)庫中,我們有一種非常簡(jiǎn)單的方式來比較兩個(gè)GUID類型的大小值(其實(shí)在SQL Server數(shù)據(jù)庫中稱為UniqueIdentifier類型):
? ? 上面的例子來自Ferrari的博客《How are GUIDs sorted by SQL Server?》。
? ? 查詢結(jié)果:
通過上面可以得到如下結(jié)果:
先按每1-8從左到右進(jìn)行排序;
接著按第9-10位從右到左進(jìn)行排序;
最后按后11-16位從右到左進(jìn)行排序;
通過分析,我們可得到如下權(quán)重列表:
? ? 在Microsoft官方文檔中,有一篇文檔關(guān)于GUID與uniqueidentifier的值比較:《Comparing GUID and uniqueidentifier Values》。
? ? 不同的數(shù)據(jù)庫處理GUID的方式也是不同的。在SQL Server存在內(nèi)置GUID類型,沒有原生GUID支持的數(shù)據(jù)庫通過模擬來方式來實(shí)現(xiàn)的。在Oracle保存為raw bytes類型,具體類型為raw(16);在MySql中通常將GUID儲(chǔ)存為char(36)的字符串形式。
? ? 關(guān)于Oracle、MySql數(shù)據(jù)庫的排序規(guī)則與.Net中排序規(guī)則,不過篇章的限制,這里不再做具體的演示,您可以自己進(jìn)行測(cè)試。我們?cè)谶@里只給出最終的結(jié)論:
.Net中GUID的排序規(guī)則是從左到右依次進(jìn)行排序,與數(shù)字排序規(guī)則一致;
Sql Server數(shù)據(jù)庫提供對(duì)GUID類型的支持,在數(shù)據(jù)庫中稱為UniqueIdentifier類型,但是排序規(guī)則比較復(fù)雜:
先按每1-8從左到右進(jìn)行排序;
接著按第9-10位從右到左進(jìn)行排序;
最后按后11-16位從右到左進(jìn)行排序;
Oracle數(shù)據(jù)庫未提供對(duì)GUID類型的支持,使用的是raw bytes類型保存數(shù)據(jù),真實(shí)類型為raw(16),排序規(guī)則是按Oracle二進(jìn)制進(jìn)行排序的;
MySql數(shù)據(jù)庫未提供對(duì)GUID類型的支持,使用的是字符串的類型保存數(shù)據(jù),使用是的char(36)類型,由于使用的是字符串類型,排序規(guī)則與GUID在.Net中的規(guī)則一致。
三、有序GUID
? ? 有序GUID是有規(guī)則的生成GUID,保證在之后生成的GUID的值總是比之前的要大。不過在上一節(jié)中,已經(jīng)提到過各個(gè)數(shù)據(jù)庫對(duì)GUID支持不一樣,而且排序的規(guī)則也不一樣,所以我們需要為每一個(gè)數(shù)據(jù)庫提供不一致的有序GUID生成規(guī)則。
UuidCreateSequential函數(shù)
? ? 我們都知道SQL Server數(shù)據(jù)庫有一個(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)用)。示例如下:
? ? NewSequentialId()函數(shù)只能在數(shù)據(jù)庫使用,不過在 Microsoft 的 MSDN 文檔中有說明,NEWSEQUENTIALID 是對(duì) Windows UuidCreateSequential 函數(shù)的包裝,https://msdn.microsoft.com/zh-cn/library/ms189786(v=sql.120).aspx。這樣我們可以在C#通過非托管方法調(diào)用:
? ? 但是上面的方法也存在三個(gè)問題:
1、這個(gè)方法涉及到安全問題,UuidCreateSequential函數(shù)依賴的計(jì)算硬件,該方法的后12位其實(shí)是網(wǎng)卡的MAC地址。這是我電腦生成的一組有序GUID。
? ? 這是我本地電腦的網(wǎng)卡的MAC地址:
2、由于UuidCreateSequential函數(shù)生成的有序GUID中包括MAC地址,所以如果在服務(wù)器集群環(huán)境中,肯定存在一臺(tái)服務(wù)器A上生成的有序GUID總比另一臺(tái)服務(wù)器B生成要更小,服務(wù)器A產(chǎn)生的數(shù)據(jù)插入到數(shù)據(jù)庫時(shí),由于聚集索引的問題,總是會(huì)移動(dòng)服務(wù)器B已經(jīng)持久化到數(shù)據(jù)庫中的數(shù)據(jù)。集群的服務(wù)器越多,產(chǎn)生的IO問題更嚴(yán)重。在服務(wù)器群集環(huán)境中,需要自行實(shí)現(xiàn)有序GUID。
3、UuidCreateSequential函數(shù)生成的GUID規(guī)則與SQL Server中排序的規(guī)則存在不一致,這樣仍然會(huì)導(dǎo)致嚴(yán)重的IO問題,所以需要將GUID重新排序后再持久化到數(shù)據(jù)庫。例如上面列出生成的GUID列表,依次生成的數(shù)據(jù)可以看出,是第4位字節(jié)在自增長(zhǎng),在這與任何一個(gè)數(shù)據(jù)庫的排序規(guī)則都不一致;關(guān)于該函數(shù)生成的規(guī)則,可以見此文章:https://stackoverflow.com/questions/5585307/sequential-guids。
? ? 下面的方法是將生成的GUID調(diào)整成為適合Sql Server使用的有序GUID(針對(duì)其它數(shù)據(jù)庫支持,您可以按排序規(guī)則自行修改):
小結(jié):
? ? UuidCreateSequential函數(shù)存在隱私的問題,不適合集群環(huán)境,并且需要重新排序后再提交到數(shù)據(jù)庫;
COMB解決方案
? ? COMB 類型的GUID 是由Jimmy Nilsson在他的“The Cost of GUIDs as Primary Keys”一文中設(shè)計(jì)出來的。
? ? ?基本設(shè)計(jì)思路是這樣的:既然GUID數(shù)據(jù)生成是隨機(jī)的,會(huì)造成索引效率低下,影響了系統(tǒng)的性能,那么能不能通過組合的方式,保留GUID的前10個(gè)字節(jié),用后6個(gè)字節(jié)表示GUID生成的時(shí)間(DateTime),這樣我們將時(shí)間信息與GUID組合起來,在保留GUID的唯一性的同時(shí)增加了有序性,以此來提高索引效率(這是針對(duì)Sql Server數(shù)據(jù)庫來設(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#L45-L69。
? ? 在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的方式:
? ? 但是請(qǐng)注意,這兩個(gè)ORM的解決方案只針對(duì)Sql Server數(shù)據(jù)庫,因?yàn)橹槐WC了最后幾位字節(jié)是按順序來生成的。
SequentialGuid框架
? ? SequentialGuid框架也是我要推薦給您,因?yàn)樗峁┝顺R姅?shù)據(jù)庫生成有序Guid的解決方案。
? ? 基本原理與COMB方案一樣,使用時(shí)間來保證有序GUID的順序,使用System.Security.Cryptography. RNGCryptoServiceProvider保證生成的數(shù)據(jù)的唯一性;關(guān)于該框架的設(shè)計(jì)思路以及針對(duì)各個(gè)數(shù)據(jù)庫的性能測(cè)試,見鏈接:https://www.codeproject.com/Articles/388157/GUIDs-as-fast-primary-keys-undermultiple-database。
? ? 使用方式,建議您參考ABP框架,在ABP中使用SequentialGuid框架來生成有序GUID,關(guān)鍵代碼鏈接:https://github.com/aspnetboilerplate/aspnetboilerplate/ blob/b36855f0c238c3592203f058c641862844a0614e/src/Abp/SequentialGuidGenerator.cs#L36-L51。
四、總結(jié)
? ? 我們來總結(jié)一下:
在數(shù)據(jù)庫中最好不要使用隨機(jī)的GUID,它會(huì)影響性能;
在SQL Server中提供了NewSequentialId函數(shù)來生成有序GUID;
各個(gè)數(shù)據(jù)庫對(duì)GUID支持的不一樣,而且排序的規(guī)則也不一樣;
UuidCreateSequential函數(shù)存在隱私的問題,不適合集群環(huán)境,并且需要重新排序后再提交到數(shù)據(jù)庫;
各ORM框架提供了有序GUID的支持,但是其實(shí)只是針對(duì)Sql Server數(shù)據(jù)庫設(shè)計(jì)的;
推薦您使用SequentialGuid框架,它解決了多數(shù)據(jù)庫以及集群環(huán)境的問題。
------ END ------
作者簡(jiǎn)介
唐同學(xué):?架構(gòu)師,目前負(fù)責(zé)ERP運(yùn)行平臺(tái)整體架構(gòu)設(shè)計(jì)和開發(fā)。
也許您還想看
ERP緩存實(shí)踐經(jīng)驗(yàn)分享
大數(shù)據(jù)列表頁面前端性能優(yōu)化方案與實(shí)踐
.Net最小工作線程對(duì)應(yīng)用程序性能的影響
成本計(jì)算引擎動(dòng)態(tài)規(guī)則解析技術(shù)詳解
總結(jié)
以上是生活随笔為你收集整理的如何使用有序GUID提升数据库读写性能的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core开发实战(第16课:选
- 下一篇: 3月数据库排行:前10整体下行,出新技术