前言
通常單機或者單數據庫的項目可能規模比較小,適應的場景也比較有限,平臺的訪問量和業務量都較小,業務ID的生成方式比較原始可是夠用,它並無給這樣的系統帶來問題和瓶頸,因此這種狀況下咱們並無對此給予太多的關注。可是對於大廠的那種大規模複雜業務、分佈式高併發的應用場景,顯然這種ID的生成方式不會像小項目同樣僅僅依靠簡單的數據自增序列來完成,並且在分佈式環境下這種方式已經沒法知足業務的需求,不只沒法完成業務能力,業務ID生成的速度或者重複問題可能給系統帶來嚴重的故障。因此這一次,咱們看看大廠都是怎麼分析和解決這種ID生成問題的,同時,我也將我以前使用過的方式拿出來對比,看看有什麼問題,從中可以獲得什麼啓發。算法
分佈式ID的生成特性
在分析以前,咱們先明確一下業務ID的生成特性,在此特性的基礎上,咱們可以對下面的這幾種生成方式有更加深入的認識和感悟。數據庫
- 全局惟一,這是基本要求,不能出現重複。
- 數字類型,趨勢遞增,後面的ID必須比前面的大,這是從MySQL存儲引擎來考慮的,須要保證寫入數據的性能。
- 長度短,可以提升查詢效率,這也是從MySQL數據庫規範出發的,尤爲是ID做爲主鍵時。
- 信息安全,若是ID連續生成,勢必會泄露業務信息,甚至可能被猜出,因此須要無規則不規則。
- 高可用低延時,ID生成快,可以扛住高併發,延時足夠低不至於成爲業務瓶頸。
分佈式ID的幾種生成辦法
下面介紹幾種我積累的分佈式ID生成辦法,網絡上都可以找獲得,我經過學習積累並後期整理加上本身的感悟分享於此。雖然平時可能由於項目規模小而用不着,可是這種提出方案的思想仍是很值得學習的,尤爲是像美團的Leaf方案,我感受特別的酷。緩存
目錄:安全
- 基於UUID
- 基於數據庫主鍵自增
- 基於數據庫多實例主鍵自增
- 基於類Snowflake算法
- 基於Redis生成辦法
- 基於美團的Leaf方案(ID段、雙Buffer、動態調整Step)
基於UUID
這是很容易想到的方案,畢竟UUID全球惟一的特性深刻人心,可是,但凡熟悉MySQL數據庫特性的人,應該不會用此來做爲業務ID,它不可讀並且過於長,在此不是好主意,除非你的系統足夠小並且不講究這些,那就另說了。下面咱們簡要總結下使用UUID做爲業務ID的優缺點,以及這種方式適用的業務場景。服務器
優勢網絡
- 代碼實現足夠簡單易用。
- 本地生成沒有性能問題。
- 由於具有全球惟一的特性,因此對於數據庫遷移這種狀況不存在問題。
缺點併發
- 每次生成的ID都是無序的,並且不是全數字,且沒法保證趨勢遞增。
- UUID生成的是字符串,字符串存儲性能差,查詢效率慢。
- UUID長度過長,不適用於存儲,耗費數據庫性能。
- ID無必定業務含義,可讀性差。
適用場景jvm
- 能夠用來生成如token令牌一類的場景,足夠沒辨識度,並且無序可讀,長度足夠。
- 能夠用於無純數字要求、無序自增、無可讀性要求的場景。
基於數據庫主鍵自增
使用數據庫主鍵自增的方式算是比較經常使用的了,以MySQL爲例,在新建表時指定主鍵以auto_increment的方式自動增加生成,或者再指定個增加步長,這在小規模單機部署的業務系統裏面足夠使用了,使用簡單並且具有必定業務性,可是在分佈式高併發的系統裏面,倒是不適用的,分佈式系統涉及到分庫分表,跨機器甚至跨機房部署的環境下,數據庫自增的方式知足不了業務需求,同時在高併發大量訪問的狀況之下,數據庫的承受能力是有限的,咱們簡單的陳列一下這種方式的優缺點。分佈式
優勢高併發
- 實現簡單,依靠數據庫便可,成本小。
- ID數字化,單調自增,知足數據庫存儲和查詢性能。
- 具備必定的業務可讀性。
缺點
- 強依賴DB,存在單點問題,若是數據庫宕機,則業務不可用。
- DB生成ID性能有限,單點數據庫壓力大,沒法扛高併發場景。
適用場景
- 小規模的,數據訪問量小的業務場景。
- 無高併發場景,插入記錄可控的場景。
基於數據庫多實例主鍵自增
上面咱們大體講解了數據庫主鍵自增的方式,討論的時單機部署的狀況,若是要以此提升ID生成的效率,能夠橫向擴展機器,平衡單點數據庫的壓力,這種方案如何實現呢?那就是在auto_increment的基礎之上,設置step增加步長,讓DB以前生成的ID趨勢遞增且不重複。
從上圖能夠看出,水平擴展的數據庫集羣,有利於解決數據庫單點壓力的問題,同時爲了ID生成特性,將自增步長按照機器數量來設置,可是,這裏有個缺點就是不能再擴容了,若是再擴容,ID就無法兒生成了,步長都用光了,那若是你要解決新增機器帶來的問題,你或許能夠將第三臺機器的ID起始生成位置設定離如今的ID比較遠的位置,同時把新的步長設置進去,同時修改舊機器上ID生成的步長,但必須在ID尚未增加到新增機器設置的開始自增ID值,不然就要出現重複了。
優勢
- 解決了ID生成的單點問題,同時平衡了負載。
缺點
- 必定肯定好步長,將對後續的擴容帶來困難,並且單個數據庫自己的壓力仍是大,沒法知足高併發。
適用場景
- 數據量不大,數據庫不須要擴容的場景。
這種方案,除了難以適應大規模分佈式和高併發的場景,普通的業務規模仍是可以勝任的,因此這種方案仍是值得積累。
基於類Snowflake算法
咱們如今的項目都不大,使用的是IdWorker——國內開源的基於snowflake算法思想實現的一款分佈式ID生成器,snowflake雪花算法是twitter公司內部分佈式項目採用的ID生成算法,如今開源並流行了起來,下面是Snowflake算法的ID構成圖。
這種方案巧妙地把64位分別劃分紅多段,分開表示時間戳差值、機器標識和隨機序列,先以今生成一個64位地二進制正整數,而後再轉換成十進制進行存儲。
其中,1位標識符,不使用且標記爲0;41位時間戳,用來存儲時間戳的差值;10位機器碼,能夠標識1024個機器節點,若是機器分機房部署(IDC),這10位還能夠拆分,好比5位表示機房ID,5位表示機器ID,這樣就有32*32種組合,通常來講是足夠了;最後的12位隨即序列,用來記錄毫秒內的計數,一個節點就可以生成4096個ID序號。因此綜上所述,綜合計算下來,理論上Snowflake算法方案的QPS大約爲409.6w/s,性能足夠強悍了,並且這種方式,可以確保集羣中每一個節點生成的ID都是不一樣的,且區間內遞增。
優勢
- 每秒可以生成百萬個不一樣的ID,性能佳。
- 時間戳值在高位,中間是固定的機器碼,自增的序列在地位,整個ID是趨勢遞增的。
- 可以根據業務場景數據庫節點佈置靈活挑戰bit位劃分,靈活度高。
缺點
- 強依賴於機器時鐘,若是時鐘回撥,會致使重複的ID生成,因此通常基於此的算法發現時鐘回撥,都會拋異常處理,阻止ID生成,這可能致使服務不可用。
適用場景
- 雪花算法有很明顯的缺點就是時鐘依賴,若是確保機器不存在時鐘回撥狀況的話,那使用這種方式生成分佈式ID是可行的,固然小規模系統徹底是可以使用的。
基於Redis生成辦法
Redis的INCR命令可以將key中存儲的數字值增一,得益於此操做的原子特性,咱們可以巧妙地使用此來作分佈式ID地生成方案,還能夠配合其餘如時間戳值、機器標識等聯合使用。
優勢
- 有序遞增,可讀性強。
- 可以知足必定性能。
缺點
- 強依賴於Redis,可能存在單點問題。
- 佔用寬帶,並且須要考慮網絡延時等問題帶來地性能衝擊。
適用場景
- 對性能要求不是過高,並且規模較小業務較輕的場景,並且Redis的運行狀況有必定要求,注意網絡問題和單點壓力問題,若是是分佈式狀況,那考慮的問題就更多了,因此一幫狀況下這種方式用的比較少。
Redis的方案其實可靠性有待考究,畢竟依賴於網絡,延時故障或者宕機均可能致使服務不可用,這種風險是不得不考慮在系統設計內的。
基於美團的Leaf方案
從上面的幾種分佈式ID方案能夠看出,可以解決必定問題,可是都有明顯缺陷,爲此,美團在數據庫的方案基礎上作了一個優化,提出了一個叫作Leaf-segment的數據庫方案。
原方案咱們每次獲取ID都須要去讀取一次數據庫,這在高併發和大數據量的狀況下很容易形成數據庫的壓力,那能不能一次性獲取一批ID呢,這樣就無需頻繁的造訪數據庫了。
Leaf-segment的方案就是採用每次獲取一個ID區間段的方式來解決,區間段用完以後再去數據庫獲取新的號段,這樣一來能夠大大減輕數據庫的壓力,那怎麼作呢?
很簡單,咱們設計一張表以下:
+-------------+--------------+------+-----+-------------------+-----------------------------+ | Field | Type | Null | Key | Default | Extra | +-------------+--------------+------+-----+-------------------+-----------------------------+ | biz_tag | varchar(128) | NO | PRI | | | | max_id | bigint(20) | NO | | 1 | | | step | int(11) | NO | | NULL | | | desc | varchar(256) | YES | | NULL | | | update_time | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | +-------------+--------------+------+-----+-------------------+-----------------------------+
其中biz_tag用來區分業務,max_id表示該biz_tag目前所被分配的ID號段的最大值,step表示每次分配的號段長度,後面的desc和update_time分別表示業務描述和上一次更新號段的時間。原來每次獲取ID都要訪問數據庫,如今只須要把Step設置的足夠合理如1000,那麼如今能夠在1000個ID用完以後再去訪問數據庫了,看起來真的很酷。
咱們如今能夠這樣設計整個獲取分佈式ID的流程了:
- 用戶服務在註冊一個用戶時,須要一個用戶ID;會請求生成ID服務(是獨立的應用)的接口
- 生成ID的服務會去查詢數據庫,找到user_tag的id,如今的max_id爲0,step=1000
- 生成ID的服務把max_id和step返回給用戶服務,而且把max_id更新爲max_id = max_id + step,即更新爲1000
- 用戶服務得到max_id=0,step=1000;
- 這個用戶服務能夠用[max_id + 1,max_id+step]區間的ID,即爲[1,1000]
- 用戶服務把這個區間保存到jvm中
- 用戶服務須要用到ID的時候,在區間[1,1000]中依次獲取id,可採用AtomicLong中的getAndIncrement方法。
- 若是把區間的值用完了,再去請求生產ID的服務的接口,獲取到max_id爲1000,便可以用[max_id + 1,max_id+step]區間的ID,即爲[1001,2000]
顯而易見,這種方式很好的解決了數據庫自增的問題,並且能夠自定義max_id的起點,能夠自定義步長,很是靈活易於擴容,於此同時,這種方式也很好的解決了數據庫壓力問題,並且ID號段是存儲在JVM中的,性能得到極大的保障,可用性也過得去,即時數據庫宕機了,由於JVM緩存的號段,系統也可以所以撐住一段時間。
優勢
- 擴張靈活,性能強可以撐起大部分業務場景。
- ID號碼是趨勢遞增的,知足數據庫存儲和查詢性能要求。
- 可用性高,即便ID生成服務器不可用,也可以使得業務在短期內可用,爲排查問題爭取時間。
- 能夠自定義max_id的大小,方便業務遷移,方便機器橫向擴張。
缺點
- ID號碼不夠隨機,完整的順序遞增可能帶來安全問題。
- DB宕機可能致使整個系統不可用,仍然存在這種風險,由於號段只能撐一段時間。
- 可能存在分佈式環境各節點同一時間爭搶分配ID號段的狀況,這可能致使併發問題而出現ID重複生成。
上面的缺點一樣須要引發足夠的重視,美團技術團隊一樣想出了一個妙招——雙Buffer。
正如上所述,既然可能存在多個節點同時請求ID區間的狀況,那麼避免這種狀況就行了,Leaf-segment對此作了優化,將獲取一個號段的方式優化成獲取兩個號段,在一個號段用完以後不用立馬去更新號段,還有一個緩存號段備用,這樣可以有效解決這種衝突問題,並且採用雙buffer的方式,在當前號段消耗了10%的時候就去檢查下一個號段有沒有準備好,若是沒有準備好就去更新下一個號段,噹噹前號段用完了就切換到下一個已經緩存好的號段去使用,同時在下一個號段消耗到10%的時候,又去檢測下一個號段有沒有準備好,如此往復。
下面簡要梳理下流程:
- 當前獲取ID在buffer1中,每次獲取ID在buffer1中獲取
- 當buffer1中的Id已經使用到了100,也就是達到區間的10%
- 達到了10%,先判斷buffer2中有沒有去獲取過,若是沒有就當即發起請求獲取ID線程,此線程把獲取到的ID,設置到buffer2中。
- 若是buffer1用完了,會自動切換到buffer2
- buffer2用到10%了,也會啓動線程再次獲取,設置到buffer1中
- 依次往返
雙buffer的方案考慮的很完善,有單獨的線程去觀察下一個buffer什麼時候去更新,兩個buffer之間的切換使用也解決了臨時去數據庫更新號段可能引發的併發問題。這樣的方式可以增長JVM中業務ID的可用性,並且建議segment的長度爲業務高峯期QPS的100倍(經驗值,具體可根據本身業務來設定),這樣即便DB宕機了,業務ID的生成也可以維持至關長的時間,並且能夠有效的兼容偶爾的網絡抖動等問題。
優勢
- 基本的數據庫問題都解決了,並且行之有效。
- 基於JVM存儲雙buffer的號段,減小了數據庫查詢,減小了網絡依賴,效率更高。
缺點
- segment號段長度是固定的,業務量大時可能會頻繁更新號段,由於本來分配的號段會一會兒用完。
- 若是號段長度設置的過長,但凡緩存中有號段沒有消耗完,其餘節點從新獲取的號段與以前相比可能跨度會很大。
針對上面的缺點,美團有從新提出動態調整號段長度的方案。
動態調整Step
通常狀況下,若是你的業務不會有明顯的波峯波谷,能夠不用太在乎調整Step,由於平穩的業務量長期運行下來都基本上固定在一個步長之間,可是若是是像美團這樣有明顯的活動期,那麼Step是要具有足夠的彈性來適應業務量不一樣時間段內的暴增或者暴跌。
假設服務QPS爲Q,號段長度爲L,號段更新週期爲T,那麼Q * T = L。最開始L長度是固定的,致使隨着Q的增加,T會愈來愈小。可是本方案本質的需求是但願T是固定的。那麼若是L能夠和Q正相關的話,T就能夠趨近一個定值了。因此本方案每次更新號段的時候,會根據上一次更新號段的週期T和號段長度step,來決定下一次的號段長度nextStep,下面是一個簡單的算法,意在說明動態更新的意思:
T < 15min,nextStep = step * 2 15min < T < 30min,nextStep = step T > 30min,nextStep = step / 2
至此,知足了號段消耗穩定趨於某個時間區間的需求。固然,面對瞬時流量幾10、幾百倍的暴增,該種方案仍不能知足能夠容忍數據庫在一段時間不可用、系統仍能穩定運行的需求。由於本質上來說,此方案雖然在DB層作了些容錯方案,可是ID號段下發的方式,最終仍是須要強依賴DB,最後,仍是須要在數據庫高可用上下足工夫。
轉自:https://www.cnblogs.com/captainad/