.NET中對數(shù)據(jù)庫友好的GUID的變種使用方法
概述
.NET生成的GUID唯一性很好,用之方便,但是,缺少像雪花算法那樣的有序性。雖然分布式系統(tǒng)中做不到絕對的有序,但是,相對的有序?qū)τ谀壳皵?shù)據(jù)庫而言,索引效率等方面的提升還是有明顯效果的(當然,我認為,這是數(shù)據(jù)庫的問題,而非編程的問題,數(shù)據(jù)庫應該處理好任何類型數(shù)據(jù)作為主鍵索引時的性能,除非在SQL標準中明確寫不支持哪些數(shù)據(jù)類型)。
在當前數(shù)據(jù)庫無法解決這些問題的時候,除了使用雪花算法,是否能夠改造GUID,利用微軟已經(jīng)相當成熟的GUID的性能與效率的同時,加上序列的特性呢。本文就是做此嘗試。
GUID與雪花算法
個人主觀的感受,雪花算法生成簡單,但是,在生產(chǎn)環(huán)境配置需要注意,同時,有范圍限制(時間、數(shù)據(jù)中心、工作機器、單位時間生成個數(shù)都有限制),正常情況下,這些范圍足夠使用,但是,畢竟GUID是沒有限制的,唯一的問題就是有極低的概念會重復,這種重復在插入的時候,通過數(shù)據(jù)庫的Pk是可以及時發(fā)現(xiàn)并處理的,不會產(chǎn)生生產(chǎn)故障。
claus是這樣評價這兩個算法的:
GUID 和 Snowflake 都是常見的唯一ID生成方案,主要有以下區(qū)別:
優(yōu)點:
1、GUID是完全隨機的,碰撞概率極低,唯一性非常好。
2、GUID不依賴中心節(jié)點,分布式環(huán)境下也能生成,更適合于分布式系統(tǒng)。
3、GUID包含版本信息,可以對不同算法生成的GUID進行區(qū)分,向下兼容。
缺點:
1、GUID是無序的,不能進行排序,對數(shù)據(jù)庫索引不友好。
2、GUID較長,作為主鍵會占用更多存儲空間。
3、GUID包含 BPSK 碼,大小寫字母和短劃線,可讀性較差。
Snowflake算法:
優(yōu)點:
1、可以按時間有序生成ID,對數(shù)據(jù)庫索引更友好。
2、整體ID更短小,節(jié)省存儲空間。
3、不包含特殊字符,可讀性更好。
缺點:
1、依賴中心節(jié)點時間戳,必須保證節(jié)點時間同步。
2、不適合大規(guī)模分布式環(huán)境,擴展性較弱。
3、序列號周期較短,需要自定義優(yōu)化。
4、不向下兼容,算法變更會導致ID不連續(xù)。
總之,應根據(jù)分布式需求、時間序需求、存儲空間和可讀性等進行權衡選擇。
我們需要用到時間值
(戳?還是不蹭UNIX的概念吧)
1 毫秒=1000000 納秒
var dt = DateTime.Now;// 當前時間
Console.WriteLine(dt.Ticks);// 638322150575422659,這是.net自帶的運算,其它語言可以使用下面的方式生成。
Console.WriteLine((dt - new DateTime(1, 1, 1)).TotalMilliseconds*10000); // 638322150575422700
// 通過Ticks,可以取得100ns,即萬分之一毫秒的精度。
到3023年(1千年以后),Ticks的值也不會進位,其值為953650368000000000。
了解一下GUID
GUID 的格式為“xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”。
其含義為:【時間值低位 32bit】-【時間值中位 16bit】-【版本號 4bit】【時間值高位 4bit】【時間值高位 8bit】-【變體值 2bit】【時間序列高位 6bit】-【節(jié)點值 48bit】
位數(shù)為:8hex-4hex-4hex-4hex-12hex。
var uuidN = Guid.NewGuid().ToString("N"); // e0a953c3ee6040eaa9fae2b667060e09
時間值+GUID
時間值本身是一個long類型的數(shù)字,其大小為Int64,即8byte。
guid本身就是一個byte[],其長度為16位。
所以,我們生成一個byte[],前8位放時間值,后面放GUID,在比較大小的時候,前端的位置優(yōu)先級更高,所以,后面的GUID的無序特性會被覆蓋。
| 8 位 Byte | 8 位 Byte | 8 位 Byte |
|---|---|---|
| 時間值 | GUID值 |
加在一起,一共24個字節(jié)。
Base64字符串化
因為數(shù)據(jù)庫、前端、CSV等環(huán)境下,無法描述所有的Byte(原因是部分AscII是非可見字符),故而,需要將其進行類似Base64的轉(zhuǎn)換。
轉(zhuǎn)換后,我們會得到一個長度大概24/3*4=32位長度的字符串。這個字符串的字節(jié)數(shù)至少是32,但是,其具體更好的可讀性。
但是,Base64有一個缺陷,我們來看一下它的碼表:
可以看到,【大寫字母】<【小寫字母】<【數(shù)字】<【+】<【/】。
但是,我們的程序進行比較時,并非如此,而是遵循了AscII碼表的次序。我們來看一下AscII碼表的次序:
在AscII碼表中,【/】<【數(shù)字】<【=】<【大寫字母】<【小寫字母】。
與上面的base64對比可以發(fā)現(xiàn),如果我們將兩串二進制用base64表示,則他們將無法使用base64字面的字符進行大小比較。所以,我們需要對base64進行一次轉(zhuǎn)換,轉(zhuǎn)換的結果要與ascii對應,起到和ascii大小次序一次的效果。
BaseSortValue-BSV
為了解決這個問題,我們需要將base64的二進制拿出來,然后, 給予他們有次序的新的碼表即可。
但是,我們要做更長遠的考慮,我們的BSV大概率會被用作主鍵,會用來查詢,用出現(xiàn)在URL中,所以,我們應該避開URL的字義字符。URL的轉(zhuǎn)義表如下:
除了大小寫字母、數(shù)字以外,我們還需要3個字符。除去URL轉(zhuǎn)義字符,ASCII中可用的可視字符只剩下 !"$'()*,-.;<>[\]^_{|}~。其中"'出現(xiàn)在代碼中容易影響代碼本身的轉(zhuǎn)義,故而不可。
_符號在查詢時,經(jīng)常因為疏忽看不見。
所以,最好的應該是!$-。因為這三者的中英文區(qū)別較大,具有較高的可識別度。同時,!小于數(shù)字及字母,作為補位可以不影響大小。最終形成的碼表如下:
生成的結果如下:
0Bj4hRXIFkDoc$DXPivPF7nPBmO-smcF
0Bj4hU4f674ny-f0keZnG6VpDZm1b75r
0Bj4hU4h3WXnCsIKnnrrG7LHBzpH4yMF
0Bj4hU4h4FhniG-wH57BF6pSCVD$5sGp
0Bj4hU4h4Za$B5tkH2sIEtjA39M-nGuQ
0Bj4hU4h4qL0M-4nKRLZFcrU8qF$yezv
0Bj4hU4h548bDgf6ypAiFvI-HQSzZFeH
0Bj4hU4h5I47IsKrnkfdF7bfvOjMXWXm
0Bj4hU4h5V3P7fTSP0lBEcbZbF5h2CXV
0Bj4hU4h5iCiT-m$R7PfEeko7oaFcIPO
0Bj4hU4h5vDF6VNYTDSSFsHi1FUQt93p
實現(xiàn) - .NET
public static class SeqGuid
{
/// <summary>
/// 生成BSV的GUID。
/// </summary>
/// <returns></returns>
public static string NewGuid()
{
var gid = Guid.NewGuid().ToByteArray();// 獲取唯一的guid,對應uuid的版本應該是v4。此處直接獲取其byte數(shù)組。
var dtvalue = DateTime.Now.Ticks;//獲取當前時間到1年1月1日的總ticks數(shù),ticks單位是100ns,即萬分之一毫秒。
var dtbytes = BitConverter.GetBytes(dtvalue);// 將ticks時間戳轉(zhuǎn)換為字節(jié)數(shù)組,默認是小端。
var bytes = new Byte[gid.Length + dtbytes.Length];// 實例化新的數(shù)字,用以存放時間值和GUID值。
// 因為BitConverter.GetBytes獲得的Byte[]是小端,不符合排序要求,所以,要逆序?qū)懭隻ytes數(shù)組中,形成大端的方式。
// 將時間值放入bytes數(shù)組中。
for (long i = 0; i < dtbytes.Length; i++)
{
var cvalue = dtbytes[dtbytes.Length - i - 1];
bytes[i] = cvalue;
}
// 將guid的值,放入bytes數(shù)組中。
gid.CopyTo(bytes, dtbytes.Length);
// 將值轉(zhuǎn)換為base64,主要原因是,前端、數(shù)據(jù)庫比較容易處理字符串類型的數(shù)據(jù)。
var b64 = Convert.ToBase64String(bytes);
// 將無序的base64轉(zhuǎn)換為有序的偽base64格式。
var ss = b64.ToArray();
for (var i = 0; i < ss.Length; i++)
{
ss[i] = dic[ss[i]];
}
return new string(ss);
}
/// <summary>
/// 仿base64的有序字典,其與base64相似,使用有限的字符,表示6bit的二進制,不足的地方補=。但是,與base64的區(qū)別是,字符串是按從小到大的次序表示000000到111111的數(shù)值的。
/// </summary>
public static readonly Dictionary<char, char> dic = new Dictionary<char, char>()
{
{'A','$'},{'B','-'},{'C','0'},{'D','1'},{'E','2'},{'F','3'},{'G','4'},{'H','5'},{'I','6'},{'J','7'},{'K','8'},
{'L','9'},{'M','A'},{'N','B'},{'O','C'},{'P','D'},{'Q','E'},{'R','F'},{'S','G'},{'T','H'},{'U','I'},{'V','J'},
{'W','K'},{'X','L'},{'Y','M'},{'Z','N'},{'a','O'},{'b','P'},{'c','Q'},{'d','R'},{'e','S'},{'f','T'},{'g','U'},
{'h','V'},{'i','W'},{'j','X'},{'k','Y'},{'l','Z'},{'m','a'},{'n','b'},{'o','c'},{'p','d'},{'q','e'},{'r','f'},
{'s','g'},{'t','h'},{'u','i'},{'v','j'},{'w','k'},{'x','l'},{'y','m'},{'z','n'},{'0','o'},{'1','p'},{'2','q'},
{'3','r'},{'4','s'},{'5','t'},{'6','u'},{'7','v'},{'8','w'},{'9','x'},{'+','y'},{'/','z'},{'=','!'}
};
}
internal class Program
{
static void Main(string[] args)
{
var preone = SeqGuid.NewGuid();
for(int i = 0; i < 9999999; i++)
{
var newone = SeqGuid.NewGuid();
if (String.CompareOrdinal(newone, preone)<0)//必須使用CompareOrdinal,因為Compare和CompareTo等都受本地的CultureInfo影響,可能會忽略大小寫。
{
Console.WriteLine($"error ,{newone} < {preone}");
}
preone = newone;
}
Console.WriteLine("done...");
Console.ReadLine();
}
}
運行結果:
執(zhí)行1000萬次,沒有大小次序錯誤。單線程 的情況下,每秒生成143萬個。
多線程并發(fā)測試
因為我的電腦只有20核,所以,使用15個線程來處理,為了避免受Task的優(yōu)化影響,我們直接使用Thread來模擬。
static ConcurrentBag<string> exists = new ConcurrentBag<string>();
static void 測試并發(fā)唯一性()
{
Thread[] tks = new Thread[15];
for(var t = 0; t < 15; t++)
{
tks[t] = new Thread(workThread);
tks[t].Start();
}
Console.WriteLine("所有任務啟動完成,等待執(zhí)行完成中……");
for (var t = 0; t < 15; t++)
{
tks[t].Join();
}
Console.WriteLine($"全部執(zhí)行完成,總生成guid個數(shù):"+exists.Count);
}
static void workThread()
{
var count = 1000;
Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":啟動");
for (int i = 0; i < count; i++)
{
var nid = SeqGuid.NewGuid();
if (exists.Contains(nid))
{
Console.WriteLine("出現(xiàn)重復guid:" + nid);
}
else
{
exists.Add(nid);
}
}
Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":完成");
}
測試結果如下:
說明,借著.NET原本的GUID,完美的避開了并發(fā)情況下唯一的問題。至于說,高并發(fā)情況下次序問題,如果兩個動作在100ns以內(nèi),是否區(qū)分次序或者不是那么重要了。
并且此GUID也并非是提供強一致次序(這種需求還是需要用Redis之類的來實現(xiàn)),而是提供有限的次序,以便解決數(shù)據(jù)庫優(yōu)化的問題。
轉(zhuǎn)自:ensleep
鏈接:cnblogs.com/ensleep/p/17745166.html
