分佈式 Unique ID 的生成方法一覽

回覆 hadoop 獲取《9980元的Hadoop大數據視頻教程》


來源:江南白衣,html

calvin1978.blogcn.com/articles/uuid.htmljava


分佈式的Unique ID的用途如此普遍,從業務對象Id到日誌的TraceId,本文總結了林林總總的各類生成算法。mysql


1. 發號器web


我接觸的最先的Unique ID,就是Oracle的自增ID。redis


特色是準連續的自增數字,爲何說是準連續?由於性能考慮,每一個Client一次會領20個ID回去慢慢用,用完了再來拿。另外一個Client過來,拿的就是另外20個ID了。算法


新浪微博裏,Tim用Redis作相同的事情,Incr一下拿一批ID回去。若是有多個數據中心,那就拿高位的幾個bit來區分。sql


只要捨得在總架構裏增長額外Redis帶來的複雜度,一個64bit的long就夠表達了,並且不可能有重複ID。數組


批量是關鍵,不然每一個ID都遠程調用一次誰也吃不消。微信


2. UUID架構


2.1 概述


Universally Unique IDentifier(UUID),有着正兒八經的RFC規範,是一個128bit的數字,也能夠表現爲32個16進制的字符,中間用」-」分割。


- 時間戳+UUID版本號,分三段佔16個字符(60bit+4bit),

- Clock Sequence號與保留字段,佔4個字符(13bit+3bit),

- 節點標識佔12個字符(48bit),


好比:f81d4fae-7dec-11d0-a765-00a0c91e6bf6


實際上,UUID一共有多種算法,能用於TraceId的是:


- version1: 基於時間的算法

- version4: 基於隨機數的算法


version 4


先說Version4,這是最暴力的作法,也是JDK裏的算法,無論原來各個位的含義了,除了少數幾個位必須按規範填,其他所有用隨機數表達。


JDK裏的實現,用 SecureRandom生成了16個隨機的Byte,用2個long來存儲。記得加-Djava.security.egd=file:/dev/./urandom,不然會鎖住程序等噪音。

詳見 JVM上的隨機數與熵池策略


http://blog.csdn.net/xiaoxinyu316/article/details/39064003


version 1


而後是Version1,嚴格守着原來各個位的規矩:


由於時間戳有滿滿的60bit,因此能夠盡情花,以100納秒爲1,從1582年10月15日算起(能撐3655年,真是位數多給燒的,1582年有意思麼)


節點標識也有48bit,通常用MAC地址表達,若是有多塊網卡就隨便用一塊。若是沒網卡,就用隨機數湊數,或者拿一堆儘可能多的其餘的信息,好比主機名什麼的,拼在一塊兒再hash一把。


順序號這16bit則僅用於避免前面的節點標示改變(如網卡改了),時鐘系統出問題(如重啓後時鐘快了慢了),讓它隨機一下避免重複。


但好像Version 1就沒考慮過一臺機器上起了兩個進程這類的問題,也沒考慮相同時間戳的併發問題,因此嚴格的Version1沒人實現,接着往下看各個變種吧。


3. Version1變種 – Hibernate


Hibernate的CustomVersionOneStrategy.java,解決了以前version 1的兩個問題


- 時間戳(6bytes, 48bit):毫秒級別的,從1970年算起,能撐8925年….

- 順序號(2bytes, 16bit, 最大值65535): 沒有時間戳過了一秒要歸零的事,各搞各的,short溢出到了負數就歸0。

- 機器標識(4bytes 32bit): 拿localHost的IP地址,IPV4呢正好4個byte,但若是是IPV6要16個bytes,就只拿前4個byte。

- 進程標識(4bytes 32bit): 用當前時間戳右移8位再取整數應付,不信兩條線程會同時啓動。


值得留意就是,機器進程和進程標識組成的64bit Long幾乎不變,只變更另外一個Long就夠了。


4. Version1變種 – MongoDB


MongoDB的ObjectId.java


- 時間戳(4 bytes 32bit): 是秒級別的,從1970年算起,能撐136年。


- 自增序列(3bytes 24bit, 最大值一千六百萬): 是一個從隨機數開始(機智)的Int不斷加一,也沒有時間戳過了一秒要歸零的事,各搞各的。由於只有3bytes,因此一個4bytes的Int還要截一下後3bytes。


- 機器標識(3bytes 24bit): 將全部網卡的Mac地址拼在一塊兒作個HashCode,一樣一個int還要截一下後3bytes。搞不到網卡就用隨機數混過去。


- 進程標識(2bytes 16bits):從JMX裏搞回來到進程號,搞不到就用進程名的hash或者隨機數混過去。


可見,MongoDB的每個字段設計都比Hibernate的更合理一點,好比時間戳是秒級別的。總長度也降到了12 bytes 96bit,但若是果用64bit長的Long來保存有點不上不下的,只能表達成byte數組或16進制字符串。


另外對Java版的driver在自增序列那裏好像有bug。


5. Twitter的snowflake派號器


snowflake也是一個派號器,基於Thrift的服務,不過不是用redis簡單自增,而是相似UUID version1,


只有一個Long 64bit的長度,因此IdWorker緊巴巴的分配成:


- 時間戳(42bit) 自從2012年以來(比那些從1970年算起的會過日子)的毫秒數,能撐139年。

- 自增序列(12bit,最大值4096), 毫秒以內的自增,過了一毫秒會從新置0。

- DataCenter ID (5 bit, 最大值32),配置值。

- Worker ID ( 5 bit, 最大值32),配置值,由於是派號器的id,因此一個數據中內心最多32個派號器就夠了,還會在ZK裏作下注冊。


可見,由於是派號器,把機器標識和進程標識都省出來了,因此可以只用一個Long表達。


另外,這種派號器,client每次只能一個ID,不能批量取,因此額外增長的延時是問題。


6. 最後問題,能不能不用派號器,又一個Long搞定UUID??


前面說這麼多都是鋪墊,若是當初你的ID一開始類型設爲了Long,又不用派號器的話,怎麼辦?

從UUID的128位壓縮到Long的64位,又不用中央派號器而是本地生成,最難仍是怎麼來區分本地的機器+進程號。


思路一,壓縮其餘字段,留足夠多的長度來作機器+進程號標識

時間戳是秒級別,1年要24位,兩年要25位…..

自增序列,6萬QPS要16位,10萬要17位…

剩下20-24位,百萬分之一到一千六百萬分之一的重複率,而後把網卡Mac+進程號拼在一塊兒再hash,取結果32個bit的後面20或24個bit。但假如這個標識字段重複了,後面時間戳和自增序列也很容易重複,不停的重複。


思路二,使用ZK 或 mysql 或 redis來自增管理標識號

若是workder字段只留了12位(4096),就要用ZK或etcd,當進程關閉了要回收這個號。

若是workder字段的位數留得夠多,好比有20位(一百萬),那用redis或mysql來自增最簡單,每一個進程啓動時拿一個worker id。


思路三,繼續Random

繼續拼了,直接拿JDK UUID.randomUUID()的低位long(按UUID規範,高位的long被置了4個默認值的bit,低位只被設置3個bit),或者直接SecureRandom.nextLong(),不浪費了那3個bit。


擴展閱讀


一樂那篇《業務系統須要什麼樣的ID生成器》,其中 惟一性,時間相關,粗略有序,可反解,可製造 這個提法很好,說白了就是讓你們儘可能用UUID version1風格。


業務系統須要什麼樣的ID生成器

http://ericliang.info/what-kind-of-id-generator-we-need-in-business-systems/


細聊分佈式ID生成方法


http://calvin1978.blogcn.com/articles/%E2%80%9Chttp://chuansong.me/n/2459549%E2%80%9D


看完本文有收穫?請轉發分享給更多人

關注「ImportNew」,看技術乾貨



本文分享自微信公衆號 - 架構師智庫(beijing-tmt)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索