自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

來聊聊大廠常用的分布式 ID 生成方案

開發(fā) 數(shù)據(jù)庫
本文就基于保證數(shù)據(jù)ID唯一的要求針對市面主流的幾種分布式ID算法的特點(diǎn)和適用場景展開深入探討。

隨著數(shù)據(jù)日益增加,項(xiàng)目需要進(jìn)行分庫分表來分?jǐn)倝毫?,為了保證數(shù)據(jù)ID唯一,必須有一套適配當(dāng)前分庫分表的分布式ID生成方案,而這套方案必須具備以下條件:

  • 全局唯一:分布式ID必須全局唯一,確保數(shù)據(jù)可以被唯一確定。
  • 高性能:高并發(fā)場景下分布式ID必須快速響應(yīng)生成。
  • 高并發(fā):分庫分表數(shù)據(jù)存儲大概率會出現(xiàn)高并發(fā)量的請求,所以這套方案必須在高并發(fā)場景下快速生成id。
  • 高可用: 需要無限接近百分百的可用,避免因?yàn)橐驗(yàn)榉植际絀D生成影響其他業(yè)務(wù)運(yùn)行。
  • 易用:方案必須易于使用,不大量侵入業(yè)務(wù)代碼。
  • 遞增:盡可能保證遞增,確保ID有序插入以保證插入性能和索引維護(hù)開銷。

所以本文就基于上述要求針對市面主流的幾種分布式ID算法的特點(diǎn)和適用場景展開深入探討。

1. 詳解UUID

UUID有著全球唯一的特性,只需一行簡單的代碼,即可快速生成一個UUID:

public static void main(String[] args) {
        log.info("uuid:{}", UUID.randomUUID());
    }

輸出結(jié)果:

uuid:9fea8ab3-0789-4719-aa70-8849cbc59916

符合全局唯一、高性能、高并發(fā)、高可用、易用的特性。但是它卻有以下缺點(diǎn):

  • 字符串類型:因?yàn)槭亲址愋?,占用大量存儲空間。
  • 無序:因?yàn)樯傻臒o規(guī)律,因?yàn)榇罅康碾S機(jī)添加勢必導(dǎo)致MySQL底層大量的B+ tree的節(jié)點(diǎn)分裂,耗費(fèi)大量計(jì)算資源,嚴(yán)重影響數(shù)據(jù)庫性能,進(jìn)而導(dǎo)致查詢耗時增加。

所以UUID雖在性能、唯一性、可用性上有著保證,但其生成結(jié)果會加劇數(shù)據(jù)庫的負(fù)擔(dān),所以不是很推薦用作分布式ID。

2. 利用數(shù)據(jù)庫主鍵自增

我們也可以專門使用一張數(shù)據(jù)表生成唯一自增的id,通過數(shù)據(jù)庫auto_increment特性自增唯一且有序的分布式ID:

CREATE TABLE id_allocation (
  id INT AUTO_INCREMENT PRIMARY KEY
);

這種做法優(yōu)點(diǎn)就是實(shí)現(xiàn)簡單且單調(diào)遞增,ID也是數(shù)值類型,所以查詢速度很快,但它也有以下缺點(diǎn):

  • 單點(diǎn)性能瓶頸:所有的ID都需要依賴這張數(shù)據(jù)表生成,如果出現(xiàn)單點(diǎn)故障,很可能導(dǎo)致服務(wù)癱瘓。
  • 性能瓶頸:單個數(shù)據(jù)庫一下子接收大量請求連接接入(經(jīng)過壓測8C16G在數(shù)據(jù)表設(shè)計(jì)良好的情況下TPS大約在2500~3000左右),可能會導(dǎo)致數(shù)據(jù)庫服務(wù)癱瘓:

3. 分庫分表主鍵自增

針對上述問題,我們提出使用多庫多表生成分布式ID以保證高可用,例如我們現(xiàn)在就使用雙主模式,每個庫分別存放一張id分配表(id_allocation ),表1的id從1開始,表2的id從2開始,各自的步長都為2:

-- ID分配表1,從1開始自增,步長為2
CREATE TABLE db1.id_allocation (
  id INT AUTO_INCREMENT PRIMARY KEY
) AUTO_INCREMENT=1;


-- ID分配表2,從2開始自增,步長為2
CREATE TABLE db2.id_allocation (
  id INT AUTO_INCREMENT PRIMARY KEY
) AUTO_INCREMENT=2;

為了保證壓力均攤,我們可以針對服務(wù)采用輪詢或者哈希算法請求不同的數(shù)據(jù)庫獲取唯一ID:

該方案很好的解決的DB單點(diǎn)問題,但是僅僅為了全局id而徒增這么多的硬件服務(wù)器確實(shí)是有些許浪費(fèi)。

4. 號段式id分配策略

號段式分布式ID生成是當(dāng)下比較主流的實(shí)現(xiàn)方案之一,它的整體思路是在專門建立一張數(shù)據(jù)表劃分當(dāng)前業(yè)務(wù)的ID分配段,每次加載一批號段到內(nèi)存中,然后所有的服務(wù)都從這個號碼段中獲取ID,直至這個號碼段用完,然后再到數(shù)據(jù)庫中再劃動一批到內(nèi)存中使用,對應(yīng)的建表SQL如下所示:

CREATE TABLE id_allocation (
  id int(10) NOT NULL,
  max_id bigint(20) NOT NULL COMMENT '當(dāng)前已用最大id',
  step int(20) NOT NULL COMMENT '號段分配步長',
  biz_type    int(20) NOT NULL COMMENT '業(yè)務(wù)類型',
  version int(20) NOT NULL COMMENT '版本號',
  PRIMARY KEY (`id`)
)

我們假設(shè)現(xiàn)在有一個訂單服務(wù)要采用號段式進(jìn)行ID分配,我們在這張表中初始化訂單服務(wù)的數(shù)據(jù),可以看到訂單服務(wù)初始化的數(shù)據(jù),max_id為0,即表示當(dāng)前這個業(yè)務(wù)還未分配任何id,業(yè)務(wù)類型為1,step為1000,即代表每次有服務(wù)來請求時就分配1000個id段給請求服務(wù),而version則是保證多服務(wù)請求數(shù)據(jù)一致性的樂觀鎖版本號:

INSERT INTO id_allocation (id, max_id, step, biz_type, version) VALUES (1, 0, 1000, 1, 0);

到數(shù)據(jù)查詢當(dāng)前業(yè)務(wù)的號碼段,查詢數(shù)據(jù)庫得到結(jié)果是max_id為0以及step為1000,這意味著當(dāng)前這個業(yè)務(wù)的id已經(jīng)用到了0(還未使用過),而可用范圍為1000,所以號碼段為[max_id+1,max_id+step]即1~1000,將數(shù)據(jù)庫加載到數(shù)據(jù)庫之后,攜帶版本號將數(shù)據(jù)的max_id更新為max_id+step,如此一來下次id分配就從1000開始:

update table set max_id=max_id+step

為了避免因?yàn)榉?wù)崩潰等情況導(dǎo)致內(nèi)存中的號碼段丟失,我們需要如下兩個步驟的處理避免ID沖突:

  • 每一次服務(wù)重啟時,先加載一批號碼段到內(nèi)存中,然后更新數(shù)據(jù)庫中的max_id,盡管這么做可能導(dǎo)致一部分的id浪費(fèi),但是這種即用即更新很好的解決的id沖突的問題。

  • 編寫AOP切面或者其他任何方式,當(dāng)捕獲到ID沖突異常后,直接更新內(nèi)存中的號碼段,在業(yè)務(wù)上盡可能直接解決意外沖突。

可以說這種方案通過數(shù)據(jù)庫結(jié)合內(nèi)存緩沖了數(shù)據(jù)庫的壓力,既保證的唯一性,又能夠抗住高并發(fā),對于擴(kuò)容,我們也可以通過在數(shù)據(jù)庫中增加配置,以調(diào)整id范圍。而它的缺點(diǎn)也很明顯,它很可能因?yàn)榉?wù)宕機(jī)或者內(nèi)存丟失的原因,導(dǎo)致一段id全部丟棄導(dǎo)致一定的號碼段浪費(fèi)。

5. 基于redis原子自增指令I(lǐng)NCR

按照redis官方的說法,redis單機(jī)qps為8~10w(8C16G服務(wù)器)并且因?yàn)閱尉€程的緣故,redis也能很好的保證id自增的原子性:

所以我們也可以嘗試通過redis原子操作獲取唯一id:

127.0.0.1:6379> set seq_id 1
OK
127.0.0.1:6379> INCR seq_id
(integer) 2
127.0.0.1:6379> INCR seq_id
(integer) 3
127.0.0.1:6379> INCR seq_id
(integer) 4
127.0.0.1:6379>

當(dāng)然這種方案也有著一定的缺點(diǎn):

  • 若我們使用rdb持久化機(jī)制,一旦redis宕機(jī)等原因?qū)е戮彺鎭G失,再次從redis中獲取的id很可能出現(xiàn)沖突。
  • 若使用aof會導(dǎo)致每一條命令都會進(jìn)行持久化,但也會導(dǎo)致重啟數(shù)據(jù)恢復(fù)時間過長。

所以我們建議通過redis混合持久化機(jī)制結(jié)合redis高可用架構(gòu)保證分布式id生成的可靠性,并在業(yè)務(wù)處理上對這類id沖突進(jìn)行一定的兜底處理。

6. 雪花算法

(1) 雪花id算法介紹

雪花算法是twitter開發(fā)是一中id生成算法,它是由如下幾個因子構(gòu)成:

  • 1位正數(shù)位,即高位為0。
  • 41位時間戳
  • 5位workerid記錄用戶維護(hù)當(dāng)前服務(wù)器的id號。
  • 5位的服務(wù)id
  • 12位的自增序列

對應(yīng)結(jié)構(gòu)圖如下所示:

通過雪花算法在毫秒級情況下可以生成大量id,性能出色,且是有序自增的,類型也是long類型,且包含日期和workerid等具備基因特性的因子,可以滿足大部分業(yè)務(wù)場景的需求,只不過雪花算法也有一些考慮的問題:

  • 時鐘回?fù)軉栴}:我們都知道雪花算法有41位是時間戳組成,在高并發(fā)場景下回?fù)軙r間很可能出現(xiàn)相同的時間戳,很可能造成id沖突的場景。
  • 無法滿足多分片鍵場景,雪花算法雖能保證id唯一,對于單一條件綽綽有余,一旦遇到多分片鍵依賴單id的場景就顯得力不從心了。

舉個例子:在商城下單后,我們通過雪花算法生成唯一訂單號,根據(jù)hash算法id%table 得到分表名稱。這樣的話,我們通過訂單號可以快速定位到分表并查詢到數(shù)據(jù)。但是從用戶角度來說,他希望通過自己的uid定位到訂單號就無跡可尋了,所以針對多分片鍵的場景使用雪花算法可以需要結(jié)合基因法進(jìn)行一番改造。

(2) 使用示例

以下是開源工具類hutool提供的雪花工具類,只需兩行代碼即可生成唯一鍵。

@Slf4j
public class Main {
    public static void main(String[] args) {

        Snowflake snowflake = new Snowflake();
        log.info("snowflake:{}", snowflake.nextId());

    }
}

輸出結(jié)果如下:

12:23:24.412 [main] INFO com.sharkchili.distIdFactory.Main - snowflake:1740226920726044672

為了驗(yàn)證唯一性,筆者在單位時間內(nèi)連續(xù)生成100w的雪花id:

@Slf4j
public class Main {
    public static void main(String[] args) {
        Set<Long> idSet = new HashSet<>();
        Snowflake snowflake = new Snowflake();

        for (int i = 0; i < 100_0000; i++) {
            idSet.add(snowflake.nextId());
        }
        log.info("idSe sizet:{}", idSet.size());
    }
}

從輸出結(jié)果來看,常規(guī)用法下沒有出現(xiàn)id碰撞:

12:35:27.167 [main] INFO com.sharkchili.distIdFactory.Main - idSe sizet:1000000

(3) 雪花id與時鐘回?fù)軉栴}

我們都知道雪花id是通過時間戳結(jié)合自增序列等方式保證分布式id的唯一性,但是這種方案其實(shí)也存在一定的缺陷。

我們假設(shè)這樣一個場景,我們現(xiàn)在的雪花id工具類在服務(wù)器1上,所以在這臺服務(wù)器上的id對應(yīng)的機(jī)器id和服務(wù)id都是相等的。唯一性都是通過時間戳+自增序列來保證,試想一下如果我們在雪花id工具工作期間,將操作系統(tǒng)時間回?fù)?,這就可能出現(xiàn)之前用過時間戳+自增序列重復(fù),進(jìn)而導(dǎo)致數(shù)據(jù)入庫失?。?/p>

對此著名的java工具類庫hutool就做的很好,它會在每次生成雪花id時,維護(hù)一下生成的時間戳,每次生成時通過比對本次時間戳和上次時間戳的差值判斷是否出現(xiàn)時鐘回?fù)埽?/p>

public synchronized long nextId() {
  long timestamp = genTime();
  //判斷本次回?fù)艿臅r間是否大于timeOffset(默認(rèn)2s),即回?fù)艹^2s就報錯
  if (timestamp < this.lastTimestamp) {
   if (this.lastTimestamp - timestamp < timeOffset) {
    // 容忍指定的回?fù)埽苊釴TP校時造成的異常
    timestamp = lastTimestamp;
   } else {
    // 如果服務(wù)器時間有問題(時鐘后退) 報錯。
    throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
   }
  }

  //......
  //維護(hù)本次使用的時間戳
  lastTimestamp = timestamp;

  return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT)
    | (dataCenterId << DATA_CENTER_ID_SHIFT)
    | (workerId << WORKER_ID_SHIFT)
    | sequence;
 }

(4) 基于雪花算法下的帶有基因的分布式ID

如上所說,雪花算法中時間戳和workerId這幾個字段都算是有跡可循的因子,作為特定基因可以保證分庫分表不同維度的查詢需求,例如我們現(xiàn)在有這樣一個場景:

  • 按照業(yè)務(wù)評估我們需要分出3個庫。
  • 每個庫12張表,每張表代表存儲對應(yīng)月份的數(shù)據(jù)。
  • 插入時按照月份決定插入的分表,并且數(shù)據(jù)要盡可能均攤分散到不同的庫中。
  • 查詢時能夠基于id定位到庫源并將數(shù)據(jù)查詢出來。

由上文提及雪花算法有41位代表生成id的時間戳,所以針對時間分表讀寫都是可以實(shí)現(xiàn)的。而上述需求中有3個庫,按照雪花算法的規(guī)則,它有5位的workerid,我們完全可以用這幾個bit存儲當(dāng)前數(shù)據(jù)所存儲的庫源信息。

我們都知道分庫分表主要是為了解決并發(fā)請求和海量數(shù)據(jù),所以針對這樣的業(yè)務(wù)場景,我們勢必會針對該場景設(shè)計(jì)出相應(yīng)的集群節(jié)點(diǎn),我們假設(shè)當(dāng)前集群有3個節(jié)點(diǎn),按照分庫分表的設(shè)計(jì),我們可以將分庫分表業(yè)務(wù)對應(yīng)的服務(wù)程序各自初始化一個雪花id生成器,服務(wù)的workerId分別對應(yīng)1、2、3。

假設(shè)某用戶請求到的服務(wù)1且日期是2025.1.25,由于服務(wù)器的workerId為1,所以雪花id的機(jī)器id就標(biāo)記為1,同時基于當(dāng)前時間點(diǎn)得出41位的時間戳為2025.1.25也就是分表-1,所以我們數(shù)據(jù)就會寫入到庫1的分表1中:

因?yàn)閕d藏有庫源信息和日期信息,后續(xù)定位也非常方便,就像下面這段代碼示例:

//指明workerId為1,即當(dāng)表當(dāng)前服務(wù)的數(shù)據(jù)全部存入庫1
        Snowflake snowflake = new Snowflake(1);
        long id = snowflake.nextId();
        //后續(xù)數(shù)據(jù)讀寫都可以基于id定位到
        DateTime date = DateUtil.date(snowflake.getGenerateDateTime(id));
        log.info("分布式id對應(yīng)的庫源:{} 日期:{}", snowflake.getWorkerId(id), date);

對應(yīng)的我們也給出這段代碼的輸出結(jié)果:

01:13:43.446 [main] INFO com.sharkChili.runner.ESRunner - 分布式id對應(yīng)的庫源:1 日期:2025-01-25 01:13:43
責(zé)任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關(guān)推薦

2016-11-29 09:12:21

數(shù)據(jù)庫分布式ID

2017-06-19 17:55:22

CASID分布式

2024-02-22 17:02:09

IDUUID雪花算法

2020-11-10 07:44:18

分庫分表生成

2021-06-04 20:09:19

ID分布式設(shè)計(jì)

2023-09-03 22:14:23

分布式ID

2023-01-12 17:46:37

分庫分表id如何生成

2017-07-01 16:02:39

分布式ID生成器

2022-09-07 08:18:26

分布式灰度方案分支號

2019-09-05 13:06:08

雪花算法分布式ID

2023-03-05 18:23:38

分布式ID節(jié)點(diǎn)

2020-09-23 09:52:01

分布式WebSocketMQ

2017-07-04 16:18:15

分布式云應(yīng)用導(dǎo)圖

2024-05-20 08:08:00

分布式系統(tǒng)緩存C#

2024-11-19 15:55:49

2022-02-23 07:09:30

分布式ID雪花算法

2023-11-29 10:26:52

分布式數(shù)據(jù)

2022-04-08 08:27:08

分布式鎖系統(tǒng)

2021-02-01 09:35:53

關(guān)系型數(shù)據(jù)庫模型

2018-04-03 09:27:42

分布式架構(gòu)系統(tǒng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號