1. 程式人生 > 其它 >五種生成唯一id方式的對比

五種生成唯一id方式的對比

Java生成隨機的字串uuid & 資料庫自增主鍵 & redis的id生成策略 & 雪花演算法 & 百度的UidGenerator演算法

一、分散式ID的業務需求

在複雜的分散式系統中,往往需要對大量的資料和訊息進行唯一標識。能夠生成全域性唯一ID的系統是非常必要的。

二、生成id的硬性要求

  1. 全域性唯一:不能出現重複的id號,既然是唯一標識,這是最基本的要求。
  2. 趨勢遞增:在mysql的innoDB引擎中使用的是聚集索引,由於多數RDBMS使用BTree的資料結構來儲存索引資料。因此在主鍵的選擇上我們應該儘量使用有序的主鍵保證寫入效能。
  3. 單調遞增:保證下一個ID一定大於上一個ID,例如事務版本號,IM增量訊息、排序等特殊需求。
  4. 資訊保安:如果id是連續的,惡意獲取使用者工作就非常容易做了,直接按照順序下載指定的URL即可;如果是訂單號就更危險了,競爭對手可以直接知道我們一天的單量,所以在一些應用場景下,需要無規則的id。
  5. 含時間戳:這樣能夠在開發中快速瞭解分散式id的生成時間。

三、id生成系統的可用性要求

  1. 高可用:發一個獲取分散式id的請求,伺服器就可以保證99.99%的情況下給我建立一個唯一的分散式id。
  2. 低延遲:發一個獲取分散式id的請求,伺服器響應速度要快。
  3. 高QPS:假如併發10萬個建立分散式id請求,伺服器要頂得住並能成功建立10萬個唯一的分散式id。

四、Java生成隨機的字串uuid

uuid效能非常高,本地生成,沒有網路消耗,如果只考慮唯一性UUID是OK的,但是入資料庫的效能較差。

為什麼無序的uuid會導致資料庫效能變差?

  1. 無序:無法預測它的生成順序,不能生成遞增有序的數字。首先分散式id一般都會作為主鍵,uuid太長,佔用儲存空間比較大,如果是海量資料庫,就需要考慮儲存量的問題。
  2. uuid往往是使用字串儲存:查詢的效率比較低。傳輸資料量大,且不可讀。
  3. 索引,B+樹索引的分裂:既然分散式id是主鍵,主鍵是包括索引的,然後mysql的索引是通過b+樹來實現的,因為uuid資料是無序的,每一次新的uuid資料的插入,為了查詢的優化,都會對索引“底層的B+樹進行修改,這一點很不好。插入完全無序,不但會導致一些中間節點產生分裂,也會白白創造出很多不飽和的節點,這樣大大降低了資料庫插入的效能。

五、資料庫自增主鍵

(1)資料庫自增主鍵原理是:基於資料庫自增id和MySQL資料庫的replace into 實現的,這裡的replace into 跟insert功能類似,不同點在於replace into首先嚐試把資料插入列表中,如果發現表中已經有此行資料(根據主鍵或唯一索引判斷)則先刪除再插入,否則直接插入新資料。

(2)不適合做分散式id:系統水平擴充套件比較困難如果是單機的話就OK,如果是分散式多臺機子的話就很難實現了。資料庫壓力很大,每次獲取id都需要讀取一次資料庫,效能低。

六、redis的id生成策略

redis是單執行緒的天生保證原子性,可以使用原子操作INCR和NCRBY來實現。

但是缺點同上面的mysql一樣,不利於平行擴容。同時key一定要設定有效期,不過可以使用redis叢集來獲取更高的吞吐量。

假如一個叢集中有5臺Redis,可以初始化每臺Redis的值分別是1,2,3,4,5,然後步長都是5
各個Redis生成的ID為:
A: 1,6,11,16,21
B: 2,7,12,17,22
C:3.8.13.18.23
D: 4,9,14,19,24
E: 5,10,15,20,25

七、雪花演算法

使用一個 64 bit 的 long 型的數字作為全域性唯一 ID。

1)第一個部分,是 1 個 bit:0,這個是無意義的;二進位制裡第一個bit如果是1,那麼都是負數,但是我們生成的id都是正數,所以第一個Bit統一都是0。1位sign標識位;
2)第二個部分,是 41 個 bit:表示的是時間戳;單位是毫秒。
3)第三個部分,是 5 個 bit:表示的是機房 ID,10001;5位資料中心id
4)第四個部分,是 5 個 bit:表示的是機器 ID,1 1001;5位工作機器id
5)第五個部分,是 12 個 bit:表示的序號,就是某個機房某臺機器上這一毫秒內同時生成的 ID 的序號,0000 00000000。記錄同一個毫秒內產生的不同id。12位自增序列

對於分散式系統來說雪花演算法的優缺點:

(1)優點:

  1. 毫秒數在高位,自增序列在低位,整個id都是趨勢遞增的;
  2. 不依賴資料庫等第三方系統,以服務的方式部署,穩定性更高,生成id的效能也是非常高的;
  3. 可以根據自身業務特性分配bit位,非常靈活。

(2)缺點:強依賴機器時鐘,如果機器上時鐘回撥,會導致發號重複或者服務會處於不可用狀態。

雪花演算法生成唯一id的demo示例:

匯入依賴:

<!--hutool 測試雪花演算法-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-captcha</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * 雪花演算法生成UUID,測試demo
 */
@Slf4j
@Component
public class IdGeneratorSnowFlake {

    private long workerId = 0;
    private long datacenterId = 1;
    private Snowflake snowflake = IdUtil.createSnowflake(workerId,datacenterId);

    public void init() {
        try{
            workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
            log.info("當前機器得workerId:{}",workerId);
        } catch (Exception e) {
            log.info("當前機器的workerId獲取失敗",e);
            workerId = NetUtil.getLocalhostStr().hashCode();
            log.info("當前機器workId:{}",workerId);
        }
    }

    public synchronized long snowflakeId() {
        return snowflake.nextId();
    }

    public synchronized long snowflakeId (long workerId,long datacenterId) {
        snowflake = IdUtil.createSnowflake(workerId,datacenterId);
        return snowflake.nextId();
    }

    public static void main(String[] args) {
        //1440603845439913984
        //1440603918894759936
        System.out.println(new IdGeneratorSnowFlake().snowflakeId());
    }
}

八、UidGenerator演算法

UidGenerator演算法是對雪花演算法的改進。

UidGenerator的組成sign(1bit)+ delta seconds (28bits) + worker node id (22bits) + sequence (13bits)

UidGenerator能保證“指定機器&同一時刻&某一併發序列”,是唯一,並據此生成一個64bits的唯一id(long),且預設採用上圖位元組分配方式。

UidGenerator與原版的雪花演算法不同,UidGenerator還支援自定義時間戳、工作機器id和序列號等各部位的位數,以應用於不同場景。

  • 1)sign(1bit):固定1bit符號標識,即生成的UID為正數。
  • 2)delta seconds (28 bits):當前時間,相對於時間基點"2016-05-20"的增量值,單位:秒,最多可支援約8.7年(注意:(a)這裡的單位是秒,而不是毫秒! (b)注意這裡的用詞,是“最多”可支援8.7年)。
  • 3)worker id (22 bits):機器id,最多可支援約420w次機器啟動。內建實現為在啟動時由資料庫分配,預設分配策略為用後即棄,後續可提供複用策略。
  • 4)sequence (13 bits):每秒下的併發序列,13 bits可支援每秒8192個併發(注意下這個地方,預設支援qps最大為8192個)。

UidGenerator的兩種實現方式:

(1)DefaultUidGenerator

通過DefaultUidGenerator 實現,對時鐘回撥的處理比較簡單粗暴,另外如果使用DefaultUidGenerator 方式生成分散式id,一定要根據你的業務的情況和特點,調整各個欄位佔用的位數。

(2)CachedUidGenerator

CachedUidGenerator是在DefaultUidGenerator 的基礎上進行了改進,它的核心是利用了RingBuffer,本質上是一個數組,陣列中每個項被稱為slot,CachedUidGenerator設計了兩個RingBuffer,一個儲存唯一id,一個儲存flag,RingBuffer的尺寸是2^n,n必須是正整數。

CachedUidGenerator主要通過採取如下一些措施和方案規避了時鐘回撥的問題和增強唯一性:

  • 自增列:CachedUidGenerator的workerid在例項每次重啟時初始化,且就是資料庫的自增id,從而完美的實現每個例項獲取到的workerid不會有任何衝突;
  • RingBuffer:CachedUidGenerator不再在每次取ID時都實時計算分散式ID,而是利用RingBuffer資料結構預先生成若干個分散式ID並儲存;
  • 時間遞增:傳統的SnowFlake演算法實現都是通過System.currentTimeMillis()來獲取時間並與上一次時間進行比較,這樣的實現嚴重依賴伺服器的時間。而CachedUidGenerator的時間型別是AtomicLong,且通過incrementAndGet()方法獲取下一次的時間,從而脫離了對伺服器時間的依賴,也就不會有時鐘回撥的問題(這種做法也有一個小問題,即分散式ID中的時間資訊可能並不是這個ID真正產生的時間點,例如:獲取的某分散式ID的值為3200169789968523265,它的反解析結果為{"timestamp":"2019-05-02 23:26:39","workerId":"21","sequence":"1"},但是這個ID可能並不是在"2019-05-02 23:26:39"這個時間產生的)。

CachedUidGenerator通過快取的方式預先生成一批唯一ID列表,可以解決唯一ID獲取時候的耗時。但這種方式也有不好點,一方面需要耗費記憶體來快取這部分資料,另外如果訪問量不大的情況下,提前生成的UID中的時間戳可能是很早之前的。而對於大部分的場景來說,DefaultUidGenerator 就可以滿足相關的需求了,沒必要來湊CachedUidGenerator這個熱鬧。

CachedUidGenerator測試:

不管如何配置, CachedUidGenerator總能提供600萬/s的穩定吞吐量,只是使用年限會有所減少。