一、資料庫自增(單例項)

1、方案描述

基於資料庫自增ID(auto_increment)利用其來充當分散式ID。實現方式就是用一張表來充當ID生成器,當我們需要ID時,向表中插入一條記錄返回主鍵ID。

CREATE TABLE identity_table(
  `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
  `value` VARCHAR(50) NOT NULL DEFAULT '' COMMENT 'value',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='ID資訊表';
<insert id="insertAndGetId" useGeneratedKeys="true" keyProperty="id">  
    insert into identity_table(value)  
    values(#{value})
</insert>

2、優點

(1)通過資料庫保證唯一性,實現簡單;

(2)數字ID,具備有序性。

3、缺點

(1)存在單點宕機風險;

(2)無法抗住高併發場景。

二、資料庫叢集模式

1、方案描述

基於多例項資料庫主鍵自增,通過橫向擴充套件機器,解決單點資料庫的壓力。實現方式就是在自增ID(auto_increment)的基礎上,設定step增長步長,使得不同例項中的ID不會發生碰撞。

set @@auto_increment_offset = 0;     -- 起始值
set @@auto_increment_increment = 3;  -- 步長

1630223792570-3d087bcb-aa9d-4c22-88a8-b849e83a5282

2、優點

(1)利用水平擴充套件機器,解決單點問題;

3、缺點

(1)擴充套件例項,需要修改步長,操作複雜;

(2)產生ID需要依賴資料庫,資料庫的效能依然是瓶頸。

三、Redis

當使用資料庫來生成ID效能不夠要求的時候,我們可以嘗試使用Redis來生成ID。通過Redis的原子操作 INCR和INCRBY來實現。

可以使用Redis叢集來獲取更高的吞吐量。假如一個叢集中有3臺Redis。可以初始化每臺Redis的值分別是0,1,2,然後步長都是3。


1630338567842-e2b46247-1f39-4c9a-9745-76028cfb588f

優點

(1)不依賴於資料庫,效能優於資料庫;

(2)ID是有序的。

缺點

(1)強依賴於Redis,Redis宕機也會有風險;

(2)有I/O操作,網路抖動會影響服務響應速度。

四、UUID

UUID 是由一組32位數的16進位制數字所構成,是故 UUID 理論上的總數為16^32=2^128,約等於3.4 x 10123。也就是說若每納秒產生1百萬個 UUID,要花100億年才會將所有 UUID 用完。

UUID生成版本:

version 1, date-time & MAC address
version 2, date-time & group/user id
version 3, MD5 hash & namespace
version 4, pseudo-random number
version 5, SHA-1 hash & namespace

Java實現:

/**
      Static factory to retrieve a type 4 (pseudo randomly generated) UUID.
     

      The {@code UUID} is generated using a cryptographically strong pseudo
     
 random number generator.
     
     
 @return  A randomly generated {@code UUID}
     /
    public static UUID randomUUID() {
        SecureRandom ng = Holder.numberGenerator;

        byte[] randomBytes = new byte[16];
        ng.nextBytes(randomBytes);
        randomBytes[6]  &= 0x0f;  /
 clear version        /
        randomBytes[6]  |= 0x40;  /
 set to version 4     /
        randomBytes[8]  &= 0x3f;  /
 clear variant        /
        randomBytes[8]  |= 0x80;  /
 set to IETF variant  */
        return new UUID(randomBytes);
    }

碰撞概率:

Java中的UUID使用的是版本4進行實現,128bit中有122bit是隨機產生的,產生的UUID重複概率非常低,所以在使用時可以不考慮此問題。

最終生成UUID:

123e4567-e89b-12d3-a456-426655440000

索引問題:

導致索引重排。

對於B+樹的結構:
(1)孩子數和key的數目相同


1630337385867-35d40ea1-5bc3-4cd3-b3cc-af4e15f56787

優點

(1)實現簡單;

(2)本地生成,無效能瓶頸;

(3)具備唯一性。

缺點

(1)ID是無序的;

(2)ID無特定含義;

(3)UUID是字串且長度較長,儲存與查詢效率慢。

五、號段模式

號段模式可以理解為從資料庫批量的獲取自增ID,每次從資料庫取出一個號段範圍,例如 (1,1000] 代表1000個ID,具體的業務服務將本號段,生成1~1000的自增ID並載入到記憶體。

CREATE TABLE id_generator (
  id int(10) NOT NULL,
  max_id bigint(20) NOT NULL COMMENT '當前最大id',
  step int(20) NOT NULL COMMENT '號段的布長',
  biz_type    int(20) NOT NULL COMMENT '業務型別',
  version int(20) NOT NULL COMMENT '版本號',
  PRIMARY KEY (`id`)

a. biz_type :代表不同業務型別

b. max_id :當前最大的可用id

c. step :代表號段的長度

d. version :是一個樂觀鎖,每次都更新version,保證併發時資料的正確性


img

等這批號段ID用完,再次向資料庫申請新號段,對max_id欄位做一次update操作,update max_id= max_id + step,update成功則說明新號段獲取成功,新的號段範圍是(max_id ,max_id +step]。

架構圖:


1630289552285-6cfd8f45-b82e-46c1-a12a-e32eb0bbad5d
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX

由於多業務端可能同時操作,所以採用版本號version樂觀鎖方式更新,這種分散式ID生成方式不強依賴於資料庫,不會頻繁的訪問資料庫,對資料庫的壓力小很多,查詢頻率減小到1/step。

優點

(1)ID號碼是趨勢遞增的,滿足資料庫儲存和查詢效能要求

(2)可用性高,即使ID生成伺服器不可用,也能夠使得業務在短時間內可用。

(3)可以自定義max_id的大小,方便業務遷移,方便機器橫向擴張

缺點

(1)ID號碼不夠隨機,完整的順序遞增可能帶來安全問題;

(2)DB宕機可能導致整個系統不可用,仍然存在這種風險,因為號段只能撐一段時間。

六、雪花演算法(SnowFlake)

SnowFlake演算法是Twitter開源的分散式ID生成演算法。生成長度為64bit的long型的數字作為全域性唯一ID。


1630248359241-f223465b-d7b9-40cd-b4c2-bffca3673ae5

a. 1bit

第一個bit是符號位,生成的ID是正數,所以第一個bit是0。

**b. 41bit

**41bit可以表示2^41個毫秒值,相當於69年。

c. 10bit

10bit可以表示工作機器,5bit表示(2^5=32個)機房,剩餘5bit表示(2^5=32臺)機器。表示最多可以標識(2^10=1024臺)機器

d. 12bit

用來標識同一毫秒內的(2^12 - 1 = 4095個)不同的id

優點

(1)高效能高可用:生成時不依賴於資料庫,完全在記憶體中生成;

(2)容量大:每秒中能生成數百萬的自增ID;

(3)ID自增:存入資料庫中,索引效率高。

缺點

(1)依賴系統時鐘,如果時間回撥或改變,會造成ID衝突。

七、Leaf

號段模式優化

資料庫的I/O操作(更新、查詢)會是瓶頸,所以需要對此進行優化。

如果在號段消費完之後才從資料庫獲取新的號段,就會存在2個問題:

(1)查詢DB過程出現網路抖動或慢查詢會導致系統響應時間變長;

(2)DB的I/O操作本身耗時,如果請求進來但號段未取回就會造成阻塞。

雙Buffer

為了能做到DB取號過程能做到無阻塞,當號段消費到某個點時非同步吧下一個號段載入到記憶體中。非同步更新,通過雙Buffer的方式,保證在DB服務出現問題時,仍然能夠有一個Buffer號段能夠使用。

詳細實現如下圖:


1630294365846-2153fb4e-1859-4962-9fdd-deb2ff2e0c2b

採用雙buffer的方式,Leaf服務內部有兩個號段快取區segment。當前號段已下發10%時,如果下一個號段未更新,則另啟一個更新執行緒去更新下一個號段。當前號段全部下發完後,如果下個號段準備好了則切換到下個號段為當前segment接著下發,迴圈往復。

解決時鐘問題

因為這種方案依賴時間,如果機器的時鐘發生了回撥,那麼就會有可能生成重複的ID號,需要解決時鐘回退的問題。


1630296792707-97a82d32-b19d-430b-b5ee-2e301ca0ee3f

1630255764082-f4508b14-9252-425c-8801-0376db42fbf1

參見上圖整個啟動流程圖

(1)服務啟動時首先檢查自己是否寫過ZooKeeper leaf_forever節點:

(2)若寫過,則用自身系統時間與leaf_forever/{self}時間則認為機器時間發生了大步長回撥,服務啟動失敗並報警;

(3)若未寫過,證明是新服務節點,直接建立持久節點leaf_forever/

(4)若abs( 系統時間-sum(time)/nodeSize ) < 閾值,認為當前系統時間準確,正常啟動服務,同時寫臨時節點leaf_temporary/self並寫入自身系統時間,接下來綜合對比其餘Leaf節點的系統時間來判斷自身系統時間是否準確,具體做法是取leaft​emporary下的所有臨時節點(所有執行中的Leaf−snowflake節點)的服務IP:Port,然後通過RPC請求得到所有節點的系統時間,計算sum(time)/nodeSize;(4)若abs(系統時間−sum(time)/nodeSize)<閾值,認為當前系統時間準確,正常啟動服務,同時寫臨時節點leaft​emporary/{self} 維持租約;

(5)否則認為本機系統時間發生大步長偏移,啟動失敗並報警;

(6)每隔一段時間(3s)上報自身系統時間寫入leaf_forever/${self};

參考

[1] B+樹簡歷過程(https://www.jianshu.com/p/08fe11a5fbb9)

[2] 美團Leaf原始碼分析(https://www.jianshu.com/p/92d34144dcb7)

[3] 美團點評分散式ID生成系統(https://tech.meituan.com/2017/04/21/mt-leaf.html)

[4] 美團分散式ID生成服務開源(https://tech.meituan.com/2019/03/07/open-source-project-leaf.html)