背景
MongoDB,想必大家都使用過,在資料落盤後,查詢該條資料時,會發現其會自動生成一條"_id",如: db.test.insert({"name":"tom"}) 查詢結果: { "_id" : ObjectId("5fd049327fbb28868f4660a5"), "name" : "tom" } MongoID作為主鍵索引,即使是叢集情況下,其在整個資料庫中也是全域性唯一的。
那這種ID有什麼用呢?或者說在哪些場景下會被使用呢? 當然需要滿足下面兩個條件:
1.資料量現在或未來比較大,處於增長狀態。
2.需要用唯一ID來標識。
比如說訂單,優惠券,訊息,待檢索或處理資料(地圖poi資料,蜘蛛抓取的資料)等等。
那這些場景ID需要什麼特性呢:
1.全域性唯一。這是肯定的,兩個不同訂單生成了相同的ID,那怎麼區分呢? 2.趨勢遞增。生成的ID可能用作資料庫索引,而一些資料庫索引是B+樹索引,如果趨勢遞增,則更能使用磁碟的塊讀取儲存特性,提高寫入效能。 3.高效能,高可用。不能成為業務瓶頸。 4.非連續。如果該ID對外使用,則很容易得知一天的資料量。如果是詳情ID,也很容易被批次抓取。 5.位元組佔用少。如果全由數字構成,則儲存,排序更高效。如1111111比aaaaaa-111111在儲存及排序方面更有優勢
MongoID構成MongoID使用12個位元組來表示,每個位元組兩位十六進位制。如下圖:
而平時業務中需要先生成ID供業務方使用,那有哪些方案呢?
方案一:
使用UUID生成器(很多程式語言自帶UUID生成)。生成格式為:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 生成的格式是長字串,如:123e4567-e89b-12d3-a456-426655440000,如果作為主鍵,儲存及索引效能低。
方案二:
使用自增ID(用程式生成或藉助mysql,redis)。但是會存在下面問題:
1.不滿足特性4的非連續,不過如果僅內部使用,倒無所謂。
2.我們提到的是分散式,而這種方案如果要分散式提供服務,則運營成本很大。主要表現為新增機器及機器異常重啟時。 比如說我們現在有兩個機器A和B,則A機器可以用起始值1,步長2(其生成的ID為:1,3,5,7...); 機器B起始值為2,步長為2(其生成的ID為:2,4,6...),來滿足全域性唯一性。 如果隨著業務增長,需再增加一臺機器呢?那我們步長得改為
3.起始值得設為一個比現在生成的最大值更大的值,另外兩臺機器都得做調整。 另外需要記錄當前機器生成的最大值,在機器異常重啟後進行恢復。
方案三:
使用號段。
具體做法是在資料庫中儲存一個最大值欄位MaxValue,然後開始計數,僅當值到達MaxValue後再更新最大值(一種預先分配的思想,僅記錄最大值)。 如:資料庫最大值為10000,每次到達後步長+10000,發號機當前值為0,則各業務從發號機取號,每次發號+1,則到達10000後,更新資料庫,資料庫中最大值變更為20000,發號機器從10000逐漸增加到20000。 如果各業務想進行隔離,則資料庫中可以儲存該業務方ID。
方案四:
基於雪花演算法(Snowflake)
該演算法是twitter公司內部分散式專案採用的ID生成演算法。
使用了8位元組(64位),比MongoID位數少4位元組,具體如下:
其生成的結果為int64。其中第一位保留不用(正數),其餘位具體見上圖。
其中機器ID最大為:1024,
自增序列號最大為4096,即1毫秒內最多生成4096個數,如果需求超過,需要等待到下一毫秒(qps高達4096000,一般業務基本用不到),可支援使用 2^41/(1000360024*365)=69.7年,且無需藉助資料庫,可以說效能很高,而且因為其毫秒時間戳設計,如果不是1毫秒內連續生成,則就不會連續。
那雪花演算法有什麼缺點嗎?
那就是時間回撥。雪花演算法依賴系統時間,如果出現了時間回撥,則生成的ID就無法保證全域性唯一了。
那還有什麼更好的方案嗎?
我希望在雪花演算法基礎上做出這樣一款分散式ID生成:
1.生成ID時不依賴系統時間,這樣就不會有時間回撥.
2.效能更高:畢竟每次呼叫系統時間也是有不少時間消耗。
3.最小依賴,無需運營。這樣在新增機器,或者機器重啟時無需人工運營。
於是我設計了這樣一款分散式ID生成器-流星演算法。
先上下其和雪花演算法的benchmark對比。
執行機器:聯想小新pro13 Ryzen 5 3550H
go版本:1.15
goos: windowsgoarch: amd64BenchmarkSnowflake-84917643 244 ns/op 0 B/op 0 allocs/opBenchmarkMeteor-852173231 22.6 ns/op 0 B/op 0 allocs/op
可以看出流星演算法比雪花演算法快10倍,下面介紹下流星演算法。
方案五:
流星演算法
也是64位,
演算法組成:
設計初衷:
第一位同樣保留(正數)
Data位為當前NodeID被建立時的秒級差,這樣只在NodeID建立時需要依賴系統時間,後續生成ID時就無需系統時間,就可以防止時間回撥。
NodeID位為節點ID,為了確保生成ID唯一,如果發生了新增機器或服務重啟,則NodeID需要每次增加。這樣即使發生了時間回撥,由於NodeID唯一,則可以保證最終生成ID唯一性。
自增序列號,11位,最大2048。每次生成,自增序列號+1,當加滿後,Data位+1。
隨機數位,3位。為什麼不把自增序列號和隨機數位合成為自增序列號位呢?主要是為了特性4:非連續性。
雪花演算法在生成時依賴了毫秒,時間位很細,只有都在這一毫秒內連續生成的ID才會連續,這種條件非常苛刻(qps達到4096000生成才會連續)。
所以加了這個隨機數位來保證生成的ID非連續。那隨機數如何生成呢?大多數隨機數以系統時間作為種子,但是這樣就達不到去系統時間的高效能了,我希望一種不依賴系統時間的高效隨機數生成演算法。最終選用了Xorshift演算法。
生成10個ID如下:
5016762319896585501676231989659650167623198966005016762319896614501676231989661650167623198966265016762319896635501676231989664050167623198966515016762319896656
總結下來就是,
1.NodeID建立時依賴當前系統時間,但是生成時不需系統時間來達到去系統時間化,這樣就解決了時間回撥。 2.NodeID每次需要跟以前不重複。這樣就保證了全域性唯一。 3.隨機數位保證了生成ID非連續。
在使用時可以使用Mysql自增ID來註冊NodeID。
倉庫(https://github.com/TheFutureIsOurs/meteor)