數據庫自增主鍵
最常見的方式。利用數據庫,全數據庫唯一。
優點:
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)不依賴於數據庫,靈活方便,且性能優於數據庫。
2)數字ID天然排序,對分頁或者需要排序的結果很有幫助。
缺點:
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+自增ID
自增ID用Mysql,每個應用每次取100個ID緩存在內存。Mysql對應的行數值加100。類似TDDL 方案。
總體上保持遞增。
嚴格遞增,可以用redis做。