歡迎大家關注Java經驗分享,裡面大量BATJ面試題,Java技術乾貨、行業雜談,也歡迎大家投稿~

java經驗分享?

zhuanlan.zhihu.com圖標

傳統的單體架構的時候,我們基本是單庫然後業務單表的結構。每個業務表的ID一般我們都是從1增,通過AUTO_INCREMENT=1設置自增起始值,但是在分散式服務架構模式下分庫分表的設計,使得多個庫或多個表存儲相同的業務數據。這種情況根據資料庫的自增ID就會產生相同ID的情況,不能保證主鍵的唯一性。

如上圖,如果第一個訂單存儲在 DB1 上則訂單 ID 為1,當一個新訂單又入庫了存儲在 DB2 上訂單 ID 也為1。我們系統的架構雖然是分散式的,但是在用戶層應是無感知的,重複的訂單主鍵顯而易見是不被允許的。那麼針對分散式系統如何做到主鍵唯一性呢?

UUID

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("-","")

目前UUID的產生方式有5種版本,每個版本的演算法不同,應用範圍也不同。

  • 基於時間的UUID - 版本1:這個一般是通過當前時間,隨機數,和本地Mac地址來計算出來,可以通過 org.apache.logging.log4j.core.util包中的 UuidUtil.getTimeBasedUuid()來使用或者其他包中工具。由於使用了MAC地址,因此能夠確保唯一性,但是同時也暴露了MAC地址,私密性不夠好。
  • DCE安全的UUID - 版本2DCE(Distributed Computing Environment)安全的UUID和基於時間的UUID演算法相同,但會把時間戳的前4位置換為POSIX的UID或GID。這個版本的UUID在實際中較少用到。
  • 基於名字的UUID(MD5)- 版本3基於名字的UUID通過計算名字和名字空間的MD5散列值得到。這個版本的UUID保證了:相同名字空間中不同名字生成的UUID的唯一性;不同名字空間中的UUID的唯一性;相同名字空間中相同名字的UUID重複生成是相同的。
  • 隨機UUID - 版本4根據隨機數,或者偽隨機數生成UUID。這種UUID產生重複的概率是可以計算出來的,但是重複的可能性可以忽略不計,因此該版本也是被經常使用的版本。JDK中使用的就是這個版本。
  • 基於名字的UUID(SHA1) - 版本5和基於名字的UUID演算法類似,只是散列值計算使用SHA1(Secure Hash Algorithm 1)演算法。

我們 Java中 JDK自帶的 UUID產生方式就是版本4根據隨機數生成的 UUID 和版本3基於名字的 UUID,有興趣的可以去看看它的源碼。

得到的UUID結果,

59f51e7ea5ca453bbfaf2c1579f09f1d
7f49b84d0bbc38e9a493718013baace6

雖然 UUID 生成方便,本地生成沒有網路消耗,但是使用起來也有一些缺點,

  • 不易於存儲:UUID太長,16位元組128位,通常以36長度的字元串表示,很多場景不適用。
  • 信息不安全:基於MAC地址生成UUID的演算法可能會造成MAC地址泄露,暴露使用者的位置。
  • 對MySQL索引不利:如果作為資料庫主鍵,在InnoDB引擎下,UUID的無序性可能會引起數據位置頻繁變動,嚴重影響性能,可以查閱 Mysql 索引原理 B+樹的知識。

資料庫生成

是不是一定要基於外界的條件才能滿足分散式唯一ID的需求呢,我們能不能在我們分散式資料庫的基礎上獲取我們需要的ID?

由於分散式資料庫的起始自增值一樣所以才會有衝突的情況發生,那麼我們將分散式系統中資料庫的同一個業務表的自增ID設計成不一樣的起始值,然後設置固定的步長,步長的值即為分庫的數量或分表的數量。

以MySQL舉例,利用給欄位設置 auto_increment_incrementauto_increment_offset來保證ID自增。

  • autoincrementoffset:表示自增長欄位從那個數開始,他的取值範圍是1 .. 65535。
  • autoincrementincrement:表示自增長欄位每次遞增的量,其默認值是1,取值範圍是1 .. 65535。

假設有三臺機器,則DB1中order表的起始ID值為1,DB2中order表的起始值為2,DB3中order表的起始值為3,它們自增的步長都為3,則它們的ID生成範圍如下圖所示:

通過這種方式明顯的優勢就是依賴於資料庫自身不需要其他資源,並且ID號單調自增,可以實現一些對ID有特殊要求的業務。

但是缺點也很明顯,首先它強依賴DB,當DB異常時整個系統不可用。雖然配置主從複製可以儘可能的增加可用性,但是數據一致性在特殊情況下難以保證。主從切換時的不一致可能會導致重複發號。還有就是ID發號性能瓶頸限制在單臺MySQL的讀寫性能。

使用redis實現

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經驗分享,裡面大量BATJ面試題,Java技術乾貨、行業雜談,也歡迎大家投稿~

java經驗分享?

zhuanlan.zhihu.com圖標
推薦閱讀:

相關文章