如何使用有序GUID提升数据库读写性能
源寶導讀:數據庫設計時,經常會使用GUID作為表的主鍵,但由于GUID的隨機性會導致數據庫在讀寫數據時效率嚴重下降,影響應用程序整體性能。本文將深入探討如何通過使用有序GUID提升數據讀寫的性能。
一、背景
? ??常見的數據庫設計是使用連續的整數為做主鍵,當新的數據插入到數據庫時,由數據庫自動生成,但這種設計不一定適合所有場景。
? 隨著越來越多的應用程序使用Nhibernate、Entity Framework Core等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的優點允許開發人員隨時創建新值,而無需從數據庫服務器檢查值的唯一性,這似乎是一個完美的解決方案。
? ? 很多數據庫在創建主鍵時,為了充分發揮數據庫的性能,會自動在該列上創建聚集索引。我們先來說一說什么是聚集索引。集索引確定表中數據的物理順序,類似于電話簿,按姓氏排列數據。由于聚集索引規定數據在表中的物理存儲順序,因此一個表也只能包含一個聚集索引。它能夠快速查找到數據,但是如果插入數據庫的主鍵不在列表的末尾,向表中添加新行時就非常緩慢。例如,看下面這個例子,在表中已經存在三行數據(例子來自Jeremy Todd的博客《GUIDs as fast primary keys under multiple databases》):
? ? 此時非常簡單:數據行按對應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個字節。我們分別的對各字節編上序號:
GUID在.Net中的排序規則
? ? 在.Net中,GUID默認的排序規則是按左到右的,看下面這個示例。
? ? 輸出結果:
? ? 通過上面的輸出結果,我們可以得到排序的權重如下
? ? 這與數字排序規則一致,從右到左進行依次進行排序(數字越小,權重越高,排序的優先級越高)。
GUID在各個數據庫中的排序規則
? ? 在SQL Server數據庫中,我們有一種非常簡單的方式來比較兩個GUID類型的大小值(其實在SQL Server數據庫中稱為UniqueIdentifier類型):
? ? 上面的例子來自Ferrari的博客《How are GUIDs sorted by SQL Server?》。
? ? 查詢結果:
通過上面可以得到如下結果:
先按每1-8從左到右進行排序;
接著按第9-10位從右到左進行排序;
最后按后11-16位從右到左進行排序;
通過分析,我們可得到如下權重列表:
? ? 在Microsoft官方文檔中,有一篇文檔關于GUID與uniqueidentifier的值比較:《Comparing GUID and uniqueidentifier Values》。
? ? 不同的數據庫處理GUID的方式也是不同的。在SQL Server存在內置GUID類型,沒有原生GUID支持的數據庫通過模擬來方式來實現的。在Oracle保存為raw bytes類型,具體類型為raw(16);在MySql中通常將GUID儲存為char(36)的字符串形式。
? ? 關于Oracle、MySql數據庫的排序規則與.Net中排序規則,不過篇章的限制,這里不再做具體的演示,您可以自己進行測試。我們在這里只給出最終的結論:
.Net中GUID的排序規則是從左到右依次進行排序,與數字排序規則一致;
Sql Server數據庫提供對GUID類型的支持,在數據庫中稱為UniqueIdentifier類型,但是排序規則比較復雜:
先按每1-8從左到右進行排序;
接著按第9-10位從右到左進行排序;
最后按后11-16位從右到左進行排序;
Oracle數據庫未提供對GUID類型的支持,使用的是raw bytes類型保存數據,真實類型為raw(16),排序規則是按Oracle二進制進行排序的;
MySql數據庫未提供對GUID類型的支持,使用的是字符串的類型保存數據,使用是的char(36)類型,由于使用的是字符串類型,排序規則與GUID在.Net中的規則一致。
三、有序GUID
? ? 有序GUID是有規則的生成GUID,保證在之后生成的GUID的值總是比之前的要大。不過在上一節中,已經提到過各個數據庫對GUID支持不一樣,而且排序的規則也不一樣,所以我們需要為每一個數據庫提供不一致的有序GUID生成規則。
UuidCreateSequential函數
? ? 我們都知道SQL Server數據庫有一個NewSequentialId()函數,用于創建有序GUID。在創建表時,可以將它設置成為GUID類型字段的默認值,在插入新增數據時自動創建主鍵的值(該函數只能做為字段的默認值,不能直接在SQL中調用)。示例如下:
? ? NewSequentialId()函數只能在數據庫使用,不過在 Microsoft 的 MSDN 文檔中有說明,NEWSEQUENTIALID 是對 Windows UuidCreateSequential 函數的包裝,https://msdn.microsoft.com/zh-cn/library/ms189786(v=sql.120).aspx。這樣我們可以在C#通過非托管方法調用:
? ? 但是上面的方法也存在三個問題:
1、這個方法涉及到安全問題,UuidCreateSequential函數依賴的計算硬件,該方法的后12位其實是網卡的MAC地址。這是我電腦生成的一組有序GUID。
? ? 這是我本地電腦的網卡的MAC地址:
2、由于UuidCreateSequential函數生成的有序GUID中包括MAC地址,所以如果在服務器集群環境中,肯定存在一臺服務器A上生成的有序GUID總比另一臺服務器B生成要更小,服務器A產生的數據插入到數據庫時,由于聚集索引的問題,總是會移動服務器B已經持久化到數據庫中的數據。集群的服務器越多,產生的IO問題更嚴重。在服務器群集環境中,需要自行實現有序GUID。
3、UuidCreateSequential函數生成的GUID規則與SQL Server中排序的規則存在不一致,這樣仍然會導致嚴重的IO問題,所以需要將GUID重新排序后再持久化到數據庫。例如上面列出生成的GUID列表,依次生成的數據可以看出,是第4位字節在自增長,在這與任何一個數據庫的排序規則都不一致;關于該函數生成的規則,可以見此文章:https://stackoverflow.com/questions/5585307/sequential-guids。
? ? 下面的方法是將生成的GUID調整成為適合Sql Server使用的有序GUID(針對其它數據庫支持,您可以按排序規則自行修改):
小結:
? ? 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#L45-L69。
? ? 在EF以及EF Core也同樣實現了類似的解決方案,EF Core的實現方式:https://github.com/aspnet/EntityFrameworkCore/blob/f7f6d6e23c8e47e44a61983827d9e41f2afe5cc7/src/EFCore/ValueGeneration/SequentialGuidValueGenerator.cs#L25-L44。
? ? 在這里介紹一下使用的方式,由EF Core框架自動生成有序GUID的方式:
? ? 但是請注意,這兩個ORM的解決方案只針對Sql Server數據庫,因為只保證了最后幾位字節是按順序來生成的。
SequentialGuid框架
? ? SequentialGuid框架也是我要推薦給您,因為它提供了常見數據庫生成有序Guid的解決方案。
? ? 基本原理與COMB方案一樣,使用時間來保證有序GUID的順序,使用System.Security.Cryptography. RNGCryptoServiceProvider保證生成的數據的唯一性;關于該框架的設計思路以及針對各個數據庫的性能測試,見鏈接:https://www.codeproject.com/Articles/388157/GUIDs-as-fast-primary-keys-undermultiple-database。
? ? 使用方式,建議您參考ABP框架,在ABP中使用SequentialGuid框架來生成有序GUID,關鍵代碼鏈接:https://github.com/aspnetboilerplate/aspnetboilerplate/ blob/b36855f0c238c3592203f058c641862844a0614e/src/Abp/SequentialGuidGenerator.cs#L36-L51。
四、總結
? ? 我們來總結一下:
在數據庫中最好不要使用隨機的GUID,它會影響性能;
在SQL Server中提供了NewSequentialId函數來生成有序GUID;
各個數據庫對GUID支持的不一樣,而且排序的規則也不一樣;
UuidCreateSequential函數存在隱私的問題,不適合集群環境,并且需要重新排序后再提交到數據庫;
各ORM框架提供了有序GUID的支持,但是其實只是針對Sql Server數據庫設計的;
推薦您使用SequentialGuid框架,它解決了多數據庫以及集群環境的問題。
------ END ------
作者簡介
唐同學:?架構師,目前負責ERP運行平臺整體架構設計和開發。
也許您還想看
ERP緩存實踐經驗分享
大數據列表頁面前端性能優化方案與實踐
.Net最小工作線程對應用程序性能的影響
成本計算引擎動態規則解析技術詳解
總結
以上是生活随笔為你收集整理的如何使用有序GUID提升数据库读写性能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core开发实战(第16课:选
- 下一篇: 《商业洞察力30讲》学习笔记(上)