如何在高並發分布式系統中生成全局唯一Id


 

  最近公司用到,並且在找最合適的方案,希望大家多參與討論和提出新方案。我和我的小伙伴們也討論了這個主題,我受益匪淺啊……

 

博文示例:

1.         GUID生成Int64值后是否還具有唯一性測試

2.         Random生成高唯一性隨機碼

 

今天分享的主題是:如何在高並發分布式系統中生成全局唯一Id

但這篇博文實際上是“半分享半討論”的博文:

1)         半分享是我將說下我所了解到的關於今天主題所涉及的幾種方案。

2)         半討論是我希望大家對各個方案都說說自己的見解,更加希望大家能提出更好的方案。(我還另外提問在此:http://q.cnblogs.com/q/53552/

 

我了解的方案如下……………………………………………………………………

1、  使用數據庫自增Id

優勢:編碼簡單,無需考慮記錄唯一標識的問題。

缺陷:

1)         在大表做水平分表時,就不能使用自增Id,因為Insert的記錄插入到哪個分表依分表規則判定決定,若是自增Id,各個分表中Id各自增長就會重復

2)         在業務上操作父、子表(即關聯表)插入時,需要在插入數據庫之前獲取max(id)用於標識父表和子表關系,若存在並發獲取max(id)的情況,max(id)會同時被別的線程獲取到。

3)         DB數據記錄都是可以根據ID號進行推測出來,對於一些數據敏感的場景,不建議采用

結論:適合小應用,無需分表,低並發。

2、  單獨開一個數據庫,獲取全局唯一的自增序列號或各表的MaxId

使用MaxId表存儲各表的MaxId

專門一個數據庫,記錄各個表的MaxId值,建一個存儲過程來取Id,邏輯大致為:開啟事物,對於在表中不存在記錄,直接返回一個默認值為1的鍵值,同時插入該條記錄到table_key表中。而對於已存在的記錄,key值直接在原來的key基礎上加1更新到MaxId表中並返回key。(給table_key中為每個表初始化一條key為1的記錄,這樣就不用每次if來判斷了---@輝_輝 提議

使用此方案的問題是:每次的查詢MaxId是一個性能損耗;

詳細可參考:《使用MaxId表存儲各表的MaxId值,以獲取全局唯一Id

                   我截取此文中的sql語法如下:

第一步:創建表
create table table_key
(
       table_name   varchar(50) not null primary key,
       key_value    int         not null
)


第二步:創建存儲過程來取自增ID
create procedure up_get_table_key
(
   @table_name     varchar(50),
   @key_value      int output
)
as
begin
     begin tran
         declare @key  int
         
         --initialize the key with 1
         set @key=1
         --whether the specified table is exist
         if not exists(select table_name from table_key where table_name=@table_name)
            begin
              insert into table_key values(@table_name,@key)        --default key vlaue:1
            end
         -- step increase
         else    
            begin
                select @key=key_value from table_key with (nolock) where table_name=@table_name
                set @key=@key+1
                --update the key value by table name
                update table_key set key_value=@key where table_name=@table_name
            end
        --set ouput value
    set @key_value=@key

    --commit tran
    commit tran
        if @@error>0
      rollback tran
end

2.         @樂活的CodeMonkey)提醒提高獲取ID時存儲過程的隔離級別,避免讀取到未提交事務導致並發ID重復的問題。(MSSQL事務隔離級別詳解

eg:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
BEGIN TRANSACTION;
……
GO
COMMIT TRANSACTION;

3.         @土豆烤肉)存儲過程中不使用事物,一旦使用到事物性能就急劇下滑。直接使用UPDATE獲取到的更新鎖,即SQL SERVER會保證UPDATE的順序執行。(已在用戶過千萬的並發系統中使用)

create procedure [dbo].[up_get_table_key]
(
   @table_name     varchar(50),
   @key_value      int output
)
as
begin

	SET NOCOUNT ON;
	DECLARE @maxId INT
	UPDATE table_key
	SET @maxId = key_value,key_value = key_value + 1 
	WHERE table_name=@table_name
	SELECT @maxId

end

結論:適用中型應用,此方案解決了分表,關聯表插入記錄的問題。但是無法滿足高並發性能要求。存在單點問題

        改進方案:時間信息 + 緩存總的maxid  (@wee616 提議)

              從redis中用lpop指令取指定key值的數據。(lpop:移除並返回列表的頭元素)
              如果將指定key值的數據取完了,會觸發初始化。
              初次初始化:
                  1)用for update鎖表,存儲最小值1和最大值50到數據庫中。
                  2)將這50個數字放入redis中。
              下次初始化:
                  1)用for update鎖表,存儲最小值51和最大值100到數據庫中。
                  2)將這50個數字放入redis中。

               數據庫每天有腳本定時清理這個表,每天都將最小值歸0,避免最大值過大。

結論:適合大型應用,生成Id順序性,可讀性比較好。

3、  Sequence特性

這個特性在SQL Server 2012Oracle中可用。這個特性是數據庫級別的,允許在多個表之間共享序列號。它可以解決分表在同一個數據庫的情況,但倘若分表放在不同數據庫,那將共享不到此序列號。(egSequence使用場景:你需要在多個表之間公用一個流水號。以往的做法是額外建立一個表,然后存儲流水號)

相關Sequence特性資料:

SQL Server2012中的SequenceNumber嘗試

SQL Server 2012 開發新功能——序列對象(Sequence

identitysequence的區別

Difference between Identity and Sequence in SQL Server 2012

結論:適用中型應用,此方案不能完全解決分表問題。

4、  通過數據庫集群編號+集群內的自增類型兩個字段共同組成唯一主鍵

優點:實現簡單,維護也比較簡單。

缺點:關聯表操作相對比較復雜,需要兩個字段。並且業務邏輯必須是一開始就設計為處理復合主鍵的邏輯,倘若是到了后期,由單主鍵轉為復合主鍵那改動成本就太大了。

結論:適合大型應用,但需要業務邏輯配合處理復合主鍵。

5、  通過設置每個集群中自增 ID 起始點(auto_increment_offset),將各個集群的ID進行絕對的分段來實現全局唯一。當遇到某個集群數據增長過快后,通過命令調整下一個 ID 起始位置跳過可能存在的沖突。

優點:實現簡單,且比較容易根據 ID 大小直接判斷出數據處在哪個集群,對應用透明。缺點:維護相對較復雜,需要高度關注各個集群 ID 增長狀況。

結論:適合大型應用,但需要高度關注各個集群 ID 增長狀況。

6、  GUIDGlobally Unique Identifier全局唯一標識符

GUID通常表示成3216進制數字(09AF)組成的字符串,如:{21EC2020-3AEA-1069-A2DD-08002B30309D},它實質上是一個128位長的二進制整數。

GUID制定的算法中使用到用戶的網卡MAC地址,以保證在計算機集群中生成唯一GUID;在相同計算機上隨機生成兩個相同GUID的可能性是非常小的,但並不為0。所以,用於生成GUID的算法通常都加入了非隨機的參數(如時間),以保證這種重復的情況不會發生。

優點:GUID是最簡單的方案,跨平台,跨語言,跨業務邏輯,全局唯一的Id,數據間同步、遷移都能簡單實現。

缺點:

1)         存儲占了32位,且無可讀性

2)         插入時因為GUID是無需的,在聚集索引的排序規則下可能移動大量的記錄。

有兩位園友主推GUID,無須順序GUID方案原因如下:

@徐少俠           GUID無序在並發下效率高,並且一個數據頁內添加新行,是在B樹內增加,本質沒有什么數據被移動,唯一可能的,是頁填充因子滿了,需要拆頁。而GUID方案導致的拆頁比順序ID要低太多了

@無色                我們要明白id是什么,是身份標識,標識身份是id最大的業務邏輯,不要引入什么時間,什么用戶業務邏輯,那是另外一個字段干的事,使用base64(guid,uuid),是通盤考慮,完全可以更好的兼容nosql,key-value存儲。

結論:適合大型應用;生成的Id不夠友好;占據了32位;

改進:

1)         @dudu告知)在SQL Server 2005中新增了NEWSEQUENTIALID函數。

詳細請看:《理解newid()newsequentialid()

在指定計算機上創建大於先前通過該函數生成的任何 GUID GUID newsequentialid 產生的新的值是有規律的,則索引B+樹的變化是有規律的,就不會導致索引列插入時移動大量記錄的問題。

但一旦服務器重新啟動,其再次生成的GUID可能反而變小(但仍然保持唯一)。這在很大程度上提高了索引的性能,但並不能保證所生成的GUID一直增大。SQL的這個函數產生的GUID很簡單就可以預測,因此不適合用於安全目的。

a)         只能做為數據庫列的DEFAULT VALUE,不能執行類似SELECT NEWSEQUENTIALID()的語句.

b)         如何獲得生成的GUID.

如果生成的GUID所在字段做為外鍵要被其他表使用,我們就需要得到這個生成的值。通常,PK是一個IDENTITY字段,我們可以在INSERT之后執行 SELECT SCOPE_IDENTITY()來獲得新生成的ID,但是由於NEWSEQUENTIALID()不是一個INDETITY類型,這個辦法是做不到了,而他本身又只能在默認值中使用,不可以事先SELECT好再插入,那么我們如何得到呢?有以下兩種方法:

--1. 定義臨時表變量 
DECLARE @outputTable TABLE(ID uniqueidentifier)
INSERT INTO TABLE1(col1, col2)
OUTPUT INSERTED.ID INTO @outputTable
VALUES('value1', 'value2')
SELECT ID FROM @outputTable
 
--2. 標記ID字段為ROWGUID(一個表只能有一個ROWGUID)
INSERT INTO TABLE1(col1, col2)
VALUES('value1', 'value2')
--在這里,ROWGUIDCOL其實相當於一個別名
SELECT ROWGUIDCOL FROM TABLE1

結論:適合大型應用,解決了GUID無序特性導致索引列插入移動大量記錄的問題。但是在關聯表插入時需要返回數據庫中生成的GUID;生成的Id不夠友好;占據了32位。

2)         COMB”(combined guid/timestamp,意思是:組合GUID/時間截)

(感謝:@ ethan-luo @lcs-帥

COMB數據類型的基本設計思路是這樣的:既然GUID數據因毫無規律可言造成索引效率低下,影響了系統的性能,那么能不能通過組合的方式,保留GUID10個字節,用另6個字節表示GUID生成的時間(DateTime),這樣我們將時間信息與GUID組合起來,在保留GUID的唯一性的同時增加了有序性,以此來提高索引效率。

NHibernate中,COMB型主鍵的生成代碼如下所示:

        /// <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);
        }

結論:適合大型應用。即保留GUID的唯一性的同時增加了GUID有序性,提高了索引效率;解決了關聯表業務問題;生成的Id不夠友好;占據了32位。

3)         長度問題,使用Base64Ascii85編碼解決。(要注意的是上述有序性方案在進行編碼后也會變得無序)

如:

GUID{3F2504E0-4F89-11D3-9A0C-0305E82C3301}

當需要使用更少的字符表示GUID時,可能會使用Base64Ascii85編碼。Base64編碼的GUID2224個字符,如:

7QDBkvCA1+B9K/U0vrQx1A

7QDBkvCA1+B9K/U0vrQx1A==

Ascii85編碼后是20個字符,如:

5:$Hj:Pf\4RLB9%kU\Lj

                   代碼如:

         Guid guid = Guid.NewGuid();

         byte[] buffer = guid.ToByteArray();

         var shortGuid = Convert.ToBase64String(buffer);

                   結論:適合大型應用,縮短GUID的長度。生成的Id不夠友好;

7、  GUID TO Int64

對於GUID的可讀性,有園友給出如下方案:(感謝:@黑色的羽翼

即將GUID轉為了19位數字,數字反饋給客戶可以一定程度上緩解友好性問題。EG:

GUID: cfdab168-211d-41e6-8634-ef5ba6502a22    (不友好)

Int64: 5717212979449746068                                      (友好性還行)

不過我的小伙伴說ToInt64后就不唯一了。因此我專門寫了個並發測試程序,后文將給出測試結果截圖及代碼簡單說明。

(唯一性、業務適合性是可以權衡的,這個唯一性肯定比不過GUID的,一般程序上都會安排錯誤處理機制,比如異常后執行一次重插的方案……)

結論:適合大型應用,生成相對友好的Id(純數字)

8、  自己寫編碼規則

優點:全局唯一Id,符合業務后續長遠的發展(可能具體業務需要自己的編碼規則等等)。

缺陷:根據具體編碼規則實現而不同;還要考慮倘若主鍵在業務上允許改變的,會帶來外鍵同步的麻煩。

我這邊寫兩個編碼規則方案:(可能不唯一,只是個人方案,也請大家提出自己的編碼規則

1)         12位年月日時分秒+5位隨機碼+3位服務器編碼  (這樣就完全單機完成生成全局唯一編碼)---20

缺陷:因為附帶隨機碼,所以編碼缺少一定的順序感。(生成高唯一性隨機碼的方案稍后給給出程序)

2)         12位年月日時分秒+5位流水碼+3位服務器編碼 (這樣流水碼就需要結合數據庫和緩存)---20位   (將影響順序權重大的“流水碼”放前面,影響順序權重小的服務器編碼放后)

缺陷:因為使用到流水碼,流水碼的生成必然會遇到和MaxId、序列表、Sequence方案中類似的問題

(為什么沒有毫秒?毫秒也不具備業務可讀性,我改用5位隨機碼、流水碼代替,推測1秒內應該不會下99999[五位]條語法)

 

結論:適合大型應用,從業務上來說,有一個規則的編碼能體現產品的專業成度。

 

 

GUID生成Int64值后是否還具有唯一性測試

測試環境

clip_image002

主要測試思路:

1.         根據內核數使用多線程並發生成Guid后再轉為Int64位值,放入集合AB、…N,多少個線程就有多少個集合。

2.         再使用Dictionary字典高效查key的特性,將步驟1中生成的多個集合全部加到Dictionary中,看是否有重復值。

示例注解:測了 Dictionary<long,bool> 最大容量就在5999470左右,所以每次並發生成的唯一值總數控制在此范圍內,讓測試達到最有效話。

主要代碼:

            for (int i = 0; i <= Environment.ProcessorCount - 1; i++)
            {
                ThreadPool.QueueUserWorkItem(
                    (list) =>
                    {
                        List<long> tempList = list as List<long>;
                        for (int j = 1; j < listLength; j++)
                        {
                            byte[] buffer = Guid.NewGuid().ToByteArray();
                            tempList.Add(BitConverter.ToInt64(buffer, 0));
                        }
                        barrier.SignalAndWait();
                    }, totalList[i]);
            }

測試數據截圖:                                                                           

clip_image004

 

數據一(循環1000次,測試數:1000*5999470)

image

數據二(循環5000次,測試數:5000*5999470)--跑了一個晚上……

image

 

感謝@Justany_WhiteSnow的專業回答:(大家分析下,我數學比較差,稍后再說自己的理解)

GUID桶數量:(2 ^ 4) ^ 32 = 2 ^ 128

Int64桶數量: 2 ^ 64

倘若每個桶的機會是均等的,則每個桶的GUID數量為:

(2 ^ 128) / (2 ^ 64) = 2 ^ 64 = 18446744073709551616

也就是說,其實重復的機會是有的,只是概率問題。

樓主測試數是29997350000發生重復的概率是:

1 - ((1 - (1 / (2 ^ 64))) ^ 29997350000) 1 - ((1 - 1 / 2 ^ 64)) ^ (2 ^ 32)) < 1 - 1 + 1 / (2 ^ 32) = 1 / (2 ^ 32) 2.3283064e-10

(唯一性、業務適合性是可以權衡的,這個唯一性肯定比不過GUID的,一般程序上都會安排錯誤處理機制,比如異常后執行一次重插的方案……)

(唯一性、業務適合性是可以權衡的,這個唯一性肯定比不過GUID的,一般程序上都會安排錯誤處理機制,比如異常后執行一次重插的方案……)

結論:GUID轉為Int64值后,也具有高唯一性,可以使用與項目中。

 

Random生成高唯一性隨機碼

我使用了五種Random生成方案,要Random生成唯一主要因素就是種子參數要唯一。

不過該測試是在單線程下的,多線程應使用不同的Random實例,所以對結果影響不會太大。

1.         使用Environment.TickCount做為Random參數(即Random的默認參數),重復性最大。

2.         使用DateTime.Now.Ticks做為Random參數,存在重復。

3.         使用unchecked((int)DateTime.Now.Ticks)做為Random參數,存在重復。

4.         使用Guid.NewGuid().GetHashCode()做為random參數,測試不存在重復(或存在性極小)。

5.         使用RNGCryptoServiceProvider做為random參數,測試不存在重復(或存在性極小)。

即:

        static int GetRandomSeed()

        {

            byte[] bytes = new byte[4];

            System.Security.Cryptography.RNGCryptoServiceProvider rng

= new System.Security.Cryptography.RNGCryptoServiceProvider();

            rng.GetBytes(bytes);

            return BitConverter.ToInt32(bytes, 0);

        }

測試結果:

clip_image007

結論:隨機碼使用RNGCryptoServiceProviderGuid.NewGuid().GetHashCode()生成的唯一性較高。

 

 

一些精彩評論(部分更新到原博文對應的地方)

一、

數據庫文件體積只是一個參考值,可水平擴展系統性能(如nosql,緩存系統)並不和文件體積有高指數的線性相關。

taobao/qq的系統比拼byte系統慢,關鍵在於索引的命中率,緩存,系統的水平擴展。

如果數據庫很少,你搞這么多byte能提高性能?

如果數據庫很大,你搞這么多byte不兼容索引不兼容緩存,不是害自已嗎?

如果數據庫要求伸縮性,你搞這么多byte,需要不斷改程序,不是自找苦嗎?

如果數據庫要求移植性,你搞這么多byte,移植起來不如重新設計,這是不是很多公司不斷加班的原因?

 

不依賴於數據存儲系統是分層設計思想的精華,實現戰略性能最大化,而不是追求戰術單機性能最大化。

 

不要迷信數據庫性能,不要迷信三范式,不要使用外鍵,不要使用byte,不要使用自增id,不要使用存儲過程,不要使用內部函數,不要使用非標准sql,存儲系統只做存儲系統的事。當出現系統性能時,如此設計的數據庫可以更好的實現遷移數據庫(如mysql->oracle),實現nosql改造((mongodb/hadoop),實現key-value緩存(redis,memcache)

 

二、

很多程序員有對性能認識有誤區,如使用存儲過程代替正常程序,其實使用存儲過程只是追求單服務器的高性能,當需要服務器水平擴展時,存儲過程中的業務邏輯就是你的噩運。(web服務器可以簡單伸縮,但是數據庫伸縮比較復雜)

 

三、

除數字日期,能用字符串存儲的字段盡量使用字符串存儲,不要為節省那不值錢的1g的硬盤而使用類似字節之類的字段,進而大幅犧牲系統可伸縮性和可擴展性。

不要為了追求所謂的性能,引入byte,使用byte注定是短命和難於移植,想想為什么html,email一直流行,因為它們使用的是字符串表示法,只要有人類永遠都能解析,如email把二進制轉成base64存儲。除了實時系統,視頻外,建議使用字符串來存儲數據,系統性能的關鍵在於分布式,在於水平擴展。

 

 

本次博文到此結束,希望大家對本次主題“如何在高並發分布式系統中生成全局唯一Id”多提出自己寶貴的意見。另外看着感覺舒服,還請多幫推薦…推薦……

 

 

推薦閱讀

          Twitter分布式id生成算法SnowFlake(雪花)

          無限容量數據庫架構設計

          9種分布式ID生成方式

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM