框架篇:分散式全域性唯一ID

潛行前行發表於2021-06-27

前言

每一次HTTP請求,資料庫的事務的執行,我們追蹤程式碼執行的過程中,需要一個唯一值和這些業務操作相關聯,對於單機的系統,可以用資料庫的自增ID或者時間戳加一個在本機遞增值,即可實現唯一值。但在分散式,又該如何實現唯一性的ID

  • 分散式ID的特性
  • 資料庫自增的ID
  • Redis分散式ID
  • Zookeeper分散式ID
  • 全域性唯一UUID的優缺點
  • Twitter的雪花演算法生成分散式ID

關注公眾號,一起交流,微信搜一搜: 潛行前行

github地址,感謝star

分散式ID的特性

  • 全域性唯一性,必須性
  • 冪等性,如果是根據某些資訊生成,則需要保障冪等性
  • 注意安全性,ID裡隱藏一些資訊,不能被猜出來,也不能被猜出來 ID 如何生成
  • 趨勢遞增性,在查詢比較時,可以判斷業務操作的時間順序

資料庫自增的ID

  • 實現簡單,ID單調自增,數值型別查詢速度快,但是單點DB存在當機風險,無法扛住高併發場景
CREATE TABLE FLIGHT_ORDER (
    id int(11) unsigned NOT NULL auto_increment, #自增ID
    PRIMARY KEY (id),
) ENGINE=innodb;

叢集下如何保證資料庫ID的唯一性

  • 當隨著業務發展,服務擴充到多臺的大叢集時,為了解決單點資料庫的壓力,資料庫也會相應的變成一個叢集,那如何保證叢集下資料庫ID的唯一性
  • 每一臺資料庫例項都設定一個起始值和增長步長
    捕獲1.PNG
  • 缺點:不利於後續擴容,如果後續需要擴容還需要人工介入修改 起始值和增長步長

Redis 分散式ID

  • 假如系統有億萬的資料,依靠資料庫的自增ID在分表分庫之後,需要人工修改每臺資料庫例項,擴容性差,維護性不好

基於Redis INCR 命令生成分散式全域性唯一ID

  • 服務向redis獲取Id,ID則和資料庫解耦,可以解決ID和分表分庫的問題,而且redis比資料庫效能更快,可以支撐叢集服務併發獲取ID的需求
  • redis的INCR命令具備了 INCR AND GET 的原子操作;redis是單程式單執行緒架構,INCR 命令不會出現 ID 重複
 @Autowired
 private StringRedisTemplate stringRedisTemplate;
 private static final String ID_KEY = "id_good_order";
 public Long incrementId() {
     return stringRedisTemplate.opsForValue().increment(ID_KEY);
 }

HINCRBY 命令

  • 實際上,為了儲存序列號的更多相關資訊,可以使用了 Redis 的 Hash 資料結構,Redis 同樣為 Hash 提供 HINCRBY 命令來實現 “INCR AND GET” 原子操作
//KEY_NAME 是 hash結構對應的Key,FIELD_NAME 是hash結構的欄位,INCR_BY_NUMBER是增量值
redis 127.0.0.1:6379> HINCRBY KEY_NAME FIELD_NAME INCR_BY_NUMBER 

當機序列號恢復問題

  • redis是記憶體資料庫,在沒有開啟RDB或AOF持久化的情況下,一旦當機ID資料將會有丟失。即便開啟了RDB持久化,由於最近一次快照時間和最新一條 HINCRBY 命令的時間有可能存在時間差,當機後通過RDB快照恢復資料集會發生ID取值重複的情況
  • redis當機序列號恢復方案
    • 利用關係型資料庫來記錄一個短時內 最大可取序列號 MAX_ID,從redis獲取ID時只能取小於 MAX_ID 的序列號
    • 為了計算最大值,需要一個定時任務定期計算ID消費速度RATE,存於redis。當客戶端取得 CUR_ID、RATE 和 MAX_ID,則根據 ID 消費速度 RATE 計算 CUR_ID 是否逼近MAX_ID,如果是則更新資料庫的MAX_ID

Zookeeper 分散式ID

  • 利用zookeeper的永續性有序節點,可以實現自增的分散式ID,而且zookeeper是個高可用的叢集服務,提交成功的訊息具有永續性,因此不怕機器當機問題,或者單機問題
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.2.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.2.0</version>
</dependency>
  • 示例
RetryPolicy retryPolicy = new ExponentialBackoffRetry(500, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
      .connectString("localhost:2181")
      .connectionTimeoutMs(5000)
      .sessionTimeoutMs(5000)
      .retryPolicy(retryPolicy)
      .build();
client.start();  
String sequenceName = "root/sequence/distributedId";
DistributedAtomicLong  distAtomicLong = new DistributedAtomicLong(client, sequenceName, retryPolicy);
//使用DistributedAtomicLong生成自增序列
public Long sequence() throws Exception {
    AtomicValue<Long> sequence = this.distAtomicLong.increment();
    if (sequence.succeeded()) {
        return sequence.postValue();
    } else {
        return null;
    }
}

UUID的優缺點

  • 基於資料庫,redis,zookeeper的分散式ID都高度依賴一個外部服務,對於某些場景,假如不存在這些外部服務又該怎麼生成分散式的ID
  • JDK裡自帶一個唯一性的ID的生成器,具有全球唯一性,這就是UUID,不過它是串無意義的字串,儲存效能差,查詢也很耗時,對於訂單系統,不適合作為唯一ID,常見優化方案為轉化為兩個uint64整數儲存或者 折半儲存(折半後不能保證唯一性)
  • 但對於日誌系統,或只是為了作為資料裡可以唯一識別序列號的關聯屬性時,可以用UUID
String uuid = UUID.randomUUID().toString().replaceAll("-","");

Twitter 的雪花演算法生成分散式ID

  • 和UUID一樣,雪花演算法並不依賴外部服務
  • 雪花演算法時 Twitter 公司內部分散式專案採用的ID生成演算法,廣受國內公司好評。不依賴第三方服務,效率高
    捕獲.PNG
  • Snowflake ID組成結構:正數位(佔1位元)+ 時間戳(佔41位元)+ 機器ID(佔5位元)+ 資料中心(佔5位元)+ 自增值(佔12位元),總共64位元組成的一個Long型別。


1:第一個bit位(1bit):Java中long的最高位是符號位代表正負,正數是0,負數是1,一般生成ID都為正數,所以預設為0。

2:時間戳部分(41bit):毫秒級的時間,不建議存當前時間戳,而是用(當前時間戳 - 固定開始時間戳)的差值,可以使產生的ID從更小的值開始

3:工作機器id(10bit):也被叫做workId,這個可以靈活配置,機房或者機器號組合都可以。

4:序列號部分(12bit),自增值支援同一毫秒內同一個節點可以生成4096個ID

//Twitter的SnowFlake演算法,使用SnowFlake演算法生成一個整數
public class SnowFlakeShortUrl {
    //起始的時間戳
    static long START_TIMESTAMP = 1624698370256L;
    //每一部分佔用的位數
    static long SEQUENCE_BIT = 12;   //序列號佔用的位數
    static long MACHINE_BIT = 5;     //機器標識佔用的位數
    static long DATA_CENTER_BIT = 5; //資料中心佔用的位數
    //每一部分的最大值
    static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
    static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
    //每一部分向左的位移
    static long MACHINE_LEFT = SEQUENCE_BIT;
    static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
    //dataCenterId + machineId 等於10bit工作機器ID
    private long dataCenterId;  //資料中心
    private long machineId;     //機器標識
    private volatile long sequence = 0L; //序列號
    private volatile long lastTimeStamp = -1L;  //上一次時間戳
    private volatile long l currTimeStamp = -1L; //當前時間戳
    
    private long getNextMill() {
        long mill = System.currentTimeMillis();
        while (mill <= lastTimeStamp) mill = System.currentTimeMillis();
        return mill;
    }
    //根據指定的資料中心ID和機器標誌ID生成指定的序列號
    public SnowFlakeShortUrl(long dataCenterId, long machineId) {
        Assert.isTrue(dataCenterId >=0 && dataCenterId <= MAX_DATA_CENTER_NUM,"dataCenterId is illegal!");
        Assert.isTrue(machineId >= 0 || machineId <= MAX_MACHINE_NUM,"machineId is illegal!");
        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }
    //生成下一個ID
    public synchronized long nextId() {
        currTimeStamp = System.currentTimeMillis();
        Assert.isTrue(currTimeStamp >= lastTimeStamp,"Clock moved backwards");
        if (currTimeStamp == lastTimeStamp) {
            //相同毫秒內,序列號自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0L) { //同一毫秒的序列數已經達到最大,獲取下一個毫秒
                currTimeStamp = getNextMill();
            }
        } else { 
            sequence = 0L; //不同毫秒內,序列號置為0
        }
        lastTimeStamp = currTimeStamp;
        return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //時間戳部分
                | dataCenterId << DATA_CENTER_LEFT       //資料中心部分
                | machineId << MACHINE_LEFT             //機器標識部分
                | sequence;                             //序列號部分
    }
    
    public static void main(String[] args) {
        SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl(10, 4);
        for (int i = 0; i < (1 << 12); i++) {
            //10進位制
            System.out.println(snowFlake.nextId());
        }
    }
}

歡迎指正文中錯誤

參考文章

相關文章