歡迎大家關注Java經驗分享,裡面大量BATJ面試題,Java技術乾貨、行業雜談,也歡迎大家投稿~
java經驗分享?zhuanlan.zhihu.com
傳統的單體架構的時候,我們基本是單庫然後業務單表的結構。每個業務表的ID一般我們都是從1增,通過AUTO_INCREMENT=1設置自增起始值,但是在分散式服務架構模式下分庫分表的設計,使得多個庫或多個表存儲相同的業務數據。這種情況根據資料庫的自增ID就會產生相同ID的情況,不能保證主鍵的唯一性。
AUTO_INCREMENT=1
如上圖,如果第一個訂單存儲在 DB1 上則訂單 ID 為1,當一個新訂單又入庫了存儲在 DB2 上訂單 ID 也為1。我們系統的架構雖然是分散式的,但是在用戶層應是無感知的,重複的訂單主鍵顯而易見是不被允許的。那麼針對分散式系統如何做到主鍵唯一性呢?
UUID (Universally Unique Identifier),通用唯一識別碼的縮寫。UUID是由一組32位數的16進位數字所構成,所以UUID理論上的總數為 1632=2128,約等於 3.4 x 10^38。也就是說若每納秒產生1兆個UUID,要花100億年才會將所有UUID用完。
生成的UUID是由 8-4-4-4-12格式的數據組成,其中32個字元和4個連字元 - ,一般我們使用的時候會將連字元刪除 uuid.toString().replaceAll("-","")。
8-4-4-4-12
uuid.toString().replaceAll("-","")
目前UUID的產生方式有5種版本,每個版本的演算法不同,應用範圍也不同。
org.apache.logging.log4j.core.util
UuidUtil.getTimeBasedUuid()
我們 Java中 JDK自帶的 UUID產生方式就是版本4根據隨機數生成的 UUID 和版本3基於名字的 UUID,有興趣的可以去看看它的源碼。
得到的UUID結果,
59f51e7ea5ca453bbfaf2c1579f09f1d 7f49b84d0bbc38e9a493718013baace6
雖然 UUID 生成方便,本地生成沒有網路消耗,但是使用起來也有一些缺點,
是不是一定要基於外界的條件才能滿足分散式唯一ID的需求呢,我們能不能在我們分散式資料庫的基礎上獲取我們需要的ID?
由於分散式資料庫的起始自增值一樣所以才會有衝突的情況發生,那麼我們將分散式系統中資料庫的同一個業務表的自增ID設計成不一樣的起始值,然後設置固定的步長,步長的值即為分庫的數量或分表的數量。
以MySQL舉例,利用給欄位設置 auto_increment_increment和 auto_increment_offset來保證ID自增。
auto_increment_increment
auto_increment_offset
假設有三臺機器,則DB1中order表的起始ID值為1,DB2中order表的起始值為2,DB3中order表的起始值為3,它們自增的步長都為3,則它們的ID生成範圍如下圖所示:
通過這種方式明顯的優勢就是依賴於資料庫自身不需要其他資源,並且ID號單調自增,可以實現一些對ID有特殊要求的業務。
但是缺點也很明顯,首先它強依賴DB,當DB異常時整個系統不可用。雖然配置主從複製可以儘可能的增加可用性,但是數據一致性在特殊情況下難以保證。主從切換時的不一致可能會導致重複發號。還有就是ID發號性能瓶頸限制在單臺MySQL的讀寫性能。
Redis實現分散式唯一ID主要是通過提供像 INCR 和 INCRBY 這樣的自增原子命令,由於Redis自身的單線程的特點所以能保證生成的 ID 肯定是唯一有序的。
但是單機存在性能瓶頸,無法滿足高並發的業務需求,所以可以採用集羣的方式來實現。集羣的方式又會涉及到和資料庫集羣同樣的問題,所以也需要設置分段和步長來實現。
為了避免長期自增後數字過大可以通過與當前時間戳組合起來使用,另外為了保證並發和業務多線程的問題可以採用 Redis + Lua的方式進行編碼,保證安全。
Redis 實現分散式全局唯一ID,它的性能比較高,生成的數據是有序的,對排序業務有利,但是同樣它依賴於redis,需要系統引進redis組件,增加了系統的配置複雜性。
當然現在Redis的使用性很普遍,所以如果其他業務已經引進了Redis集羣,則可以資源利用考慮使用Redis來實現。
以上列出了部分的分散式ID生成方式,其實大致分類的話可以分為兩類:
一種是類DB型的,根據設置不同起始值和步長來實現趨勢遞增,需要考慮服務的容錯性和可用性。
另一種是類snowflake型,這種就是將64位劃分為不同的段,每段代表不同的涵義,基本就是時間戳、機器ID和序列數。這種方案就是需要考慮時鐘回撥的問題以及做一些 buffer的緩衝設計提高性能。
而且可通過將三者(時間戳,機器ID,序列數)劃分不同的位數來改變使用壽命和並發數。
例如對於並發數要求不高、期望長期使用的應用,可增加時間戳位數,減少序列數的位數. 例如配置成{"workerBits":23,"timeBits":31,"seqBits":9}時, 可支持28個節點以整體並發量14400 UID/s的速度持續運行68年.
對於節點重啟頻率頻繁、期望長期使用的應用, 可增加工作機器位數和時間戳位數, 減少序列數位數. 例如配置成{"workerBits":27,"timeBits":30,"seqBits":6}時, 可支持37個節點以整體並發量2400 UID/s的速度持續運行34年.
java經驗分享?zhuanlan.zhihu.com 推薦閱讀: