MySQL分布式環境下生成全局自增有序ID(雪花算法Snowflake)


1 MySQL全局ID

1.1 前言

系統唯一ID是我們在設計一個系統的時候常常會遇見的問題,也常常為這個問題而糾結。

為什么需要分布式全局唯一ID以及分布式ID的業務需求
在復雜分布式系統中,往往需要對大量的數據和消息進行唯一標識,如在美團點評的金融、支付、餐飲、酒店;貓眼電影等產品的系統中數據逐漸增長,對數據庫分庫分表后需要有一個唯一ID來標識一條數據或信息;特別的訂單、騎手、優惠券都需要有唯一ID做標識
此時一個能夠生成全局唯一ID的系統是非常必要的

在這里插入圖片描述

1.2 ID生成要求

1.2.1 ID生成規則部分硬性要求

ID生成規則部分硬性要求:

  • 全局唯一
  • 趨勢遞增
    MySQLInnoDB引擎中使用的是聚集索引,由於多數RDBMS使用Btree的數據結構來存儲索引,在主鍵的選擇上面我們應該盡量使用有序的主鍵保證寫入性能
  • 單調遞增
    保證下一個ID一定大於上一個ID,例如事務版本號、IM增量消息、排序等特殊需求
  • 信息安全
    如果ID是連續,惡意用戶的爬取工作就非常容易做了,直接按照順序下載指定URL即可,如果是訂單號就危險了,競爭對手可以直接知道我們一天的單量,所以在一些應用場景下,需要ID無規則不規則,讓競爭對手不好猜
  • 含時間戳
    一樣能夠快速在開發中了解這個分布式ID什么時候生成的

1.2.2 ID號生成系統可用性要求

ID號生成系統可用性要求:

  • 高可用
    發布一個獲取分布式ID請求,服務器就要保證99.999%的情況下給我創建一個唯一分布式ID
  • 低延遲
    發一個獲取分布式ID的請求,服務器就要快,極速
  • 高QPS
    例如並發一口氣10萬個創建分布式ID請求同時殺過來,服務器要頂得住且一下子成功創建10萬個分布式ID

1.3 一般通用解決方案

1.3.1 UUID

1.3.1.1 使用

UUID.randomUUID(), UUID的標准型包含3216進制數字,以連字號分為五段,形式為 8-4-4-4-1236字符,性能非常高,本地生成,沒有網絡消耗。

1.3.1.2 存在問題

  • 入數據庫性能差,因為UUID是無序的
    無序,無法預測他的生成順序,不能生成遞增有序的數字
    首先分布式id一般都會作為主鍵,但是按照mysql官方推薦主鍵盡量越短越好,UUID每一個都很長,所以不是很推薦。

  • 主鍵,ID作為主鍵時,在特定的環境下會存在一些問題
    比如做DB主鍵的場景下,UUID就非常不適用,MySQL官方有明確的說明

  • 索引,B+樹索引的分裂
    既然分布式ID是主鍵,然后主鍵是包含索引的,而mysql的索引是通過B+樹來實現的,每一次新的UUID數據的插入,為了查詢的優化,都會對索引底層的B+樹進行修改,因為UUID數據是無序的,所以每一次UUID數據的插入都會對主鍵的B+樹進行很大的修改,這一點很不好,插入完全無序,不但會導致一些中間節點產生分裂,也會白白創造出很多不飽和節點,這樣大大降低了數據庫插入的性能。

UUID只能保證全局唯一性,不滿足后面的趨勢遞增,單調遞增

1.3.2 數據庫自增主鍵

1.3.2.1 單機

在分布式里面,數據庫的自增ID機制的主要原理是:數據庫自增IDmysql數據庫的replace into實現的,這里的replace intoinsert功能 類似,不同點在於:replace into首先嘗試插入數據列表中,如果發現表中已經有此行數據(根據主鍵或唯一索引判斷)則先刪除,再插入,否則直接插入新數據。

REPLACE INTO的含義是插入一條記錄,如果表中唯一索引的值遇到沖突,則替換老數據
在這里插入圖片描述

REPLACE into t_test(stub) values('b');
select LAST_INSERT_ID();

我們每次插入的時候,發現都會把原來的數據給替換,並且ID也會增加

這就滿足了:遞增性、單調性、唯一性

在分布式情況下,並且並發量不多的情況,可以使用這種方案來解決,獲得一個全局的唯一ID

1.3.2.2 集群分布式集群

那數據庫自增ID機制適合做分布式ID嗎?答案是不太適合

系統水平擴展比較困難,比如定義好步長和機器台數之后,如果要添加機器該怎么辦,假設現在有一台機器發號是:1,2,3,4,5,(步長是1),這個時候需要擴容機器一台,可以這樣做:把第二胎機器的初始值設置得比第一台超過很多,貌似還好,但是假設線上如果有100台機器,這個時候擴容要怎么做,簡直是噩夢,所以系統水平擴展方案復雜難以實現。

數據庫壓力還是很大,每次獲取ID都得讀寫一次數據庫,非常影響性能,不符合分布式ID里面的延遲低高QPS的規則(在高並發下,如果都去數據庫里面獲取ID,那是非常影響性能的)

1.3.2.3 自增Id用完問題

我們知道MySQL表可以定義一個自增長的id,如果我們的表沒有指定主鍵字段,那MySQL會給我們的表創建一個不可見的,長度為6個自己的row_id,然后不停地往上加步長,雖然生活中自然數是沒有上限的,但是在計算機里,我們只要定義了表示這個數的字節長度,那么它就有上限,比如在Java中,int 類型的上限值為 2^31-1 ,即2147483647MySQL無符號整數上限為2^32-1,即4294967295
有兩種情況:

  • MySQL自增id用完后,再次申請id,得到的值保持不變。插入數據會報主鍵沖突異常
  • MySQL InnoDB表未指定主鍵時,MySQL會指定一個row_id,如果row_id用完了,則會從頭開始循環。從這點來說還是建議我們創建表的時候指定主鍵的,畢竟使用row_id會發生覆蓋數據,導致原來的數據丟失,影響數據的可靠性

1.3.3 基於Redis生成全局ID策略

1.3.3.1 單機版

因為Redis單線程,天生保證原子性,可以使用原子操作INCRINCRBY來實現
INCRBY:設置增長步長

1.3.3.2 集群分布式

注意:在Redis集群情況下,同樣和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

但是存在的問題是,就是Redis集群的維護和保養比較麻煩,配置麻煩。因為要設置單點故障,哨兵值守

但是主要是的問題就是,為了一個ID,卻需要引入整個Redis集群,有種殺雞焉用牛刀的感覺

1.3.4 雪花算法

1.3.4.1 定義

Twitter的分布式自增ID算法,Snowflake

最初Twitter把存儲系統從MySQL遷移到Cassandra(由Facebook開發一套開源分布式NoSQL數據庫系統)因為Cassandra沒有順序ID生成機制,所以開發了這樣一套全局唯一ID生成服務。

Twitter的分布式雪花算法SnowFlake,經測試SnowFlake每秒可以產生26萬個自增可排序的ID

Snowflake有以下特點:

  • SnowFlake生成ID能夠按照時間有序生成
  • SnowFlake算法生成ID的結果是一個64Bit大小的整數,為一個Long型(轉換成字符串后長度最多19)
  • 分布式系統內不會產生ID碰撞(由datacenter 和 workerID做區分)並且效率較高

SnowFlake可以保證:

  • 所有生成的ID時間趨勢遞增
  • 整個分布式系統內不會產生重復ID,因為有datacenterIdworkerId來做區分

1.3.4.2 結構

雪花算法的幾個核心組成部分
在這里插入圖片描述
在這里插入圖片描述
Java64bit的證書是long類型,所以在SnowFlake算法生成的ID就是long類存儲的

  • 第一部分
    二進制中最高位符號位,1表示負數,0表示正數。生成的ID一般都是用整數,所以最高位固定為0
  • 第二部分
    第二部分是41bit時間戳位,用來記錄時間戳毫秒級
    41位可以表示 2^41 -1個數字
    如果只用來表示正整數,可以表示的范圍是:0 - 2^41 -1,減1是因為可以表示的數值范圍是從0開始計算的,而不是從1
    也就是說41位可以表示 2^41 - 1 毫秒的值,轉換成單位年則是 69.73
  • 第三部分
    第三部分為標識位5bit 數據中心 ID,5bit 工作機器 ID
    可以部署在2^10 = 1024個節點
    5位可以表示的最大正整數是 2 ^ 5 = 31個數字,來表示不同的數據中心 和 機器碼
    注意:這也是最容易出現重復的原因
  • 第四部分
    12位bit可以用來表示的正整數是 2^12 = 4095,即可以用0 1 2 … 4094 來表示同一個機器同一個時間戳內產生的4095ID序號

1.3.4.3 實現

雪花算法是由scala算法編寫的,有人使用java實現,

/**
 * twitter的snowflake算法 -- java實現
 */
public class SnowFlake {

    /**
     * 起始的時間戳
     */
    private final static long START_STMP = 1480166465631L;

    /**
     * 每一部分占用的位數
     */
    private final static long SEQUENCE_BIT = 12; //序列號占用的位數
    private final static long MACHINE_BIT = 5;   //機器標識占用的位數
    private final static long DATACENTER_BIT = 5;//數據中心占用的位數

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId;  //數據中心
    private long machineId;     //機器標識
    private long sequence = 0L; //序列號
    private long lastStmp = -1L;//上一次時間戳

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 產生下一個ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //相同毫秒內,序列號自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列數已經達到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒內,序列號置為0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分
                | datacenterId << DATACENTER_LEFT       //數據中心部分
                | machineId << MACHINE_LEFT             //機器標識部分
                | sequence;                             //序列號部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private long getNewstmp() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(2, 3);

        for (int i = 0; i < (1 << 12); i++) {
            System.out.println(snowFlake.nextId());
        }

    }
}

1.3.4.4 SpringBoot整合雪花算法

引入hutool工具類

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.1</version>
</dependency>

整合

/**
 * 雪花算法
 */
public class SnowFlakeDemo {
    private long workerId = 0;
    private long datacenterId = 1;
    private Snowflake snowFlake = IdUtil.createSnowflake(workerId, datacenterId);

    @PostConstruct
    public void init() {
        try {
            // 將網絡ip轉換成long
            workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 獲取雪花ID
     * @return
     */
    public synchronized long snowflakeId() {
        return this.snowFlake.nextId();
    }

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

    public static void main(String[] args) {
        SnowFlakeDemo snowFlakeDemo = new SnowFlakeDemo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                System.out.println(snowFlakeDemo.snowflakeId());
            }, String.valueOf(i)).start();
        }
    }
}
得到結果

1251350711346790400
1251350711346790402
1251350711346790401
1251350711346790403
1251350711346790405
1251350711346790404
1251350711346790406
1251350711346790407
1251350711350984704
1251350711350984706
1251350711350984705
1251350711350984707
1251350711350984708
1251350711350984709
1251350711350984710
1251350711350984711
1251350711350984712
1251350711355179008
1251350711355179009
1251350711355179010

1.3.4.5 優缺點

  • 優點
    毫秒數在高維,自增序列在低位,整個ID都是趨勢遞增的
    不依賴數據庫等第三方系統,以服務的方式部署,穩定性更高,生成ID的性能也是非常高的
    可以根據自身業務特性分配bit位,非常靈活
  • 缺點
    依賴機器時鍾,如果機器時鍾回撥,會導致重復ID生成
    在單機上是遞增的,但由於涉及到分布式環境,每台機器上的時鍾不可能完全同步,有時候會出現不是全局遞增的情況,此缺點可以認為無所謂,一般分布式ID只要求趨勢遞增,並不會嚴格要求遞增,90%的需求只要求趨勢遞增。

其它補充
為了解決時鍾回撥問題,導致ID重復,后面有人專門提出了解決的方案

  • UidGenerator - 百度開源的分布式唯一ID生成器
  • Leaf - 美團點評分布式ID生成系統

轉載於:https://mp.weixin.qq.com/s/cieNogGbuRXEUk4T5KJV7A


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM