分布式系統唯一ID生成方案彙總
資料庫自增主鍵
最常見的方式。利用資料庫,全資料庫唯一。
優點:
1)簡單,代碼友善,性能可以接受。
2)數字ID天然排序,對分頁或者需要排序的結果很有幫助。
缺點:
1)不同資料庫文法和實作不同,資料庫遷移的時候或多資料庫版本支援的時候需要處理。
2)在單個資料庫或讀寫分離或一主多從的情況下,隻有一個主庫可以生成。有單點故障的風險。
3)在性能達不到要求的情況下,比較難于擴充。
4)如果遇見多個系統需要合并或者涉及到資料遷移會相當痛苦。
5)分表分庫的時候會有麻煩。
優化方案:
1)針對主庫單點,如果有多個Master庫,則每個Master庫設定的起始數字不一樣,步長一樣,可以是Master的個數。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。這樣就可以有效生成叢集中的唯一ID,也可以大大降低ID生成資料庫操作的負載。
這種方式可保證id不重複。但導緻id不是絕對遞增,而是整體趨勢上遞增;其次是寫入的壓力仍然很大,mysql容易成為性能瓶頸。
資料庫批量生成id(TDDL采用)
優點:效率高;降低資料庫壓力
缺點:需考慮安全性問題,防止取到重複id;如果業務需求是每次隻生成一個id,性能有問題
備注:利用資料庫,初始化一行資料,初始值為1,取10個id,就給該值加10,調用端取傳回id值的前10個數值。以上即為批量生成id思路。
UUID
常見的方式。可以利用資料庫也可以利用程式生成,一般來說全球唯一。
1)簡單,代碼友善。
2)生成ID性能非常好,基本不會有性能問題。
3)全球唯一,在遇見資料遷移,系統資料合并,或者資料庫變更等情況下,可以從容應對。
1)沒有排序,無法保證趨勢遞增。
2)UUID往往是使用字元串存儲,查詢的效率比較低。
3)存儲空間比較大,如果是海量資料庫,就需要考慮存儲量的問題。
4)傳輸資料量大
5)不可讀。
UUID變種
1)為了解決UUID不可讀,可以使用UUID to Int64的方法。即
/// <summary>
/// 根據GUID擷取唯一數字序列
/// </summary>
public static long GuidToInt64()
{
byte[] bytes = Guid.NewGuid().ToByteArray();
return BitConverter.ToInt64(bytes, 0);
}
2)為了解決UUID無序的問題,NHibernate在其主鍵生成方式中提供了Comb算法(combined guid/timestamp)。保留GUID的10個位元組,用另6個位元組表示GUID生成的時間(DateTime)。
/// <summary>
/// Generate a new <see cref="Guid"/> using the comb algorithm.
/// </summary>
private Guid GenerateComb()
{
byte[] guidArray = Guid.NewGuid().ToByteArray();
DateTime baseDate = new DateTime(1900, 1, 1);
DateTime now = DateTime.Now;
// Get the days and milliseconds which will be used to build
//the byte string
TimeSpan days = new TimeSpan(now.Ticks - baseDate.Ticks);
TimeSpan msecs = now.TimeOfDay;
// Convert to a byte array
// Note that SQL Server is accurate to 1/300th of a
// millisecond so we divide by 3.333333
byte[] daysArray = BitConverter.GetBytes(days.Days);
byte[] msecsArray = BitConverter.GetBytes((long)
(msecs.TotalMilliseconds / 3.333333));
// Reverse the bytes to match SQL Servers ordering
Array.Reverse(daysArray);
Array.Reverse(msecsArray);
// Copy the bytes into the guid
Array.Copy(daysArray, daysArray.Length - 2, guidArray,
guidArray.Length - 6, 2);
Array.Copy(msecsArray, msecsArray.Length - 4, guidArray,
guidArray.Length - 4, 4);
return new Guid(guidArray);
}
用上面的算法測試一下,得到如下的結果:作為比較,前面3個是使用COMB算法得出的結果,最後12個字元串是時間序(統一毫秒生成的3個UUID),過段時間如果再次生成,則12個字元串會比圖示的要大。後面3個是直接生成的GUID。

如果想把時間序放在前面,可以生成後改變12個字元串的位置,也可以修改算法類的最後兩個Array.Copy。
Redis生成ID
原子操作 INCR和INCRBY
當使用資料庫來生成ID性能不夠要求的時候,我們可以嘗試使用Redis來生成ID。這主要依賴于Redis是單線程的,是以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY來實作。
可以使用Redis叢集來擷取更高的吞吐量。假如一個叢集中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然後步長都是5。各個Redis生成的ID為:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25
這個,随便負載到哪個機确定好,未來很難做修改。但是3-5台伺服器基本能夠滿足器上,都可以獲得不同的ID。但是步長和初始值一定需要事先需要了。使用Redis叢集也可以方式單點故障的問題。
另外,比較适合使用Redis來生成每天從0開始的流水号。比如訂單号=日期+當日自增長号。可以每天在Redis中生成一個Key,使用INCR進行累加。
1)不依賴于資料庫,靈活友善,且性能優于資料庫。
1)如果系統中沒有Redis,還需要引入新的元件,增加系統複雜度。
2)需要編碼和配置的工作量比較大。
EVAL,EVALSHA兩個指令(LUA)
GitHub 位址:https://github.com/hengyunabc/redis-id-generator
依賴redis的EVAL,EVALSHA兩個指令,利用redis的lua腳本執行功能,在每個節點上通過lua腳本生成唯一ID。 生成的ID是64位的:
使用41 bit來存放時間,精确到毫秒,可以使用41年。
使用12 bit來存放邏輯分片ID,最大分片ID是4095
使用10 bit來存放自增長ID,意味着每個節點,每毫秒最多可以生成1024個ID
Redis提供了TIME指令,可以取得redis伺服器上的秒數和微秒數。因些lua腳本傳回的是一個四元組。
second, microSecond, partition, seq
用戶端要自己處理,生成最終ID。
((second * 1000 + microSecond / 1000) << (12 + 10)) + (shardId << 10) + seq;
在redis-id-generator-java目錄下,有example和benchmark代碼,提供了 Java用戶端生成模式,其它語言隻要支援redis evalsha指令就可以了。
ZK生成唯一ID
zookeeper主要通過其znode資料版本來生成序列号,可以生成32位和64位的資料版本号,用戶端可以使用這個版本号來作為唯一的序列号。
很少會使用zookeeper來生成唯一ID。主要是由于需要依賴zookeeper,并且是多步調用API,如果在競争較大的情況下,需要考慮使用分布式鎖。是以,性能在高并發的分布式環境下,也不甚理想。
Stat stat = zkClient.writeData("/seq", new byte[0], -1);
nt versionAsSeq = stat.getVersion();
System.out.println(taskName + " obtain seq=" +versionAsSeq );
往指定節點寫資料,每次寫成功,拿到的版本号用來做唯一ID,性能不大好。
snowflake
分布式ID生成器--SnowFlake
公司用
時間戳+機器ID+自增ID