特好用!!!8種分布式ID生成方法

2021-02-18 Java編程大本營
前言

業務量小於500W或數據容量小於2G的時候單獨一個mysql即可提供服務,再大點的時候就進行讀寫分離也可以應付過來。但當主從同步也扛不住的時候就需要分表分庫了,但分庫分表後需要有一個唯一ID來標識一條數據,且這個唯一ID還必須有規則,能輔助我們解決分庫分表的一些問題。

資料庫的自增ID顯然不能滿足需求;特別一點的如訂單、優惠券也都需要有唯一ID做標識。此時一個能夠生成全局唯一ID的系統是非常必要的,那麼這個全局唯一ID就叫分布式ID。

分布式ID需滿足那些條件

全局唯一:基本要求就是必須保證ID是全局性唯一的。好接入:遵循拿來主義原則,在系統設計和實現上要儘可能的簡單趨勢遞增:最好趨勢遞增,這個要求就得看具體業務場景了,一般不嚴格要求UUID

UUID 是指Universally Unique Identifier,翻譯為中文是通用唯一識別碼,UUID 的目的是讓分布式系統中的所有元素都能有唯一的識別信息。形式為 8-4-4-4-12,總共有 36個字符。用起來非常簡單

public static void main(String[] args) {
  String uuid = UUID.randomUUID().toString().replaceAll("-","");
  System.out.println(uuid);
}

輸出結果 99a7d0925b294a53b2f4db9d5a3fb798,但UUID卻並不適用於實際的業務需求。訂單號用UUID這樣的字符串沒有絲毫的意義,看不出和訂單相關的有用信息;而對於資料庫來說用作業務主鍵ID,它不僅是太長還是字符串,存儲性能差查詢也很耗時,所以不推薦用作分布式ID。

優點: 生成足夠簡單,本地生成無網絡消耗,具有唯一性

缺點: 無序的字符串,不具備趨勢自增特性,沒有具體的業務含義。如此長的字符串當MySQL主鍵並非明智選擇。

資料庫自增ID

基於資料庫的auto_increment自增ID完全可以充當分布式ID,具體實現:需要一個單獨的MySQL實例用來生成ID,建表結構如下:

CREATE TABLE SoWhat_ID (
    `id` bigint(20) unsigned NOT NULL auto_increment, 
    `value` char(10) NOT NULL default '',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
) ENGINE=MyISAM;

當我們需要一個ID的時候,向表中插入一條記錄返回主鍵ID,但這種方式有一個比較致命的缺點,訪問量激增時MySQL本身就是系統的瓶頸,用它來實現分布式服務風險比較大,不推薦!

優點 : 實現簡單,ID單調自增,數值類型查詢速度快

缺點: DB單點存在宕機風險,無法扛住高並發場景

資料庫集群模式

前面說了單點資料庫方式不可取,那對上邊的方式做一些高可用優化,換成主從模式集群。害怕一個主節點掛掉沒法用,那就做雙主模式集群,也就是兩個Mysql實例都能單獨的生產自增ID。那這樣還會有個問題,兩個MySQL實例的自增ID都從1開始,會生成重複的ID怎麼辦?解決方案:設置起始值和自增步長

MySQL_1 配置:

set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2;  -- 步長

MySQL_2 配置:

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

這樣兩個MySQL實例的自增ID分別就是:

1、3、5、7、9 
2、4、6、8、10

但是如果兩個還是無法滿足咋辦,需要進行擴容呢?

增加第三臺MySQL實例需要人工修改一、二兩臺MySQL實例的起始值和步長,把第三臺機器的ID起始生成位置設定在比現有最大自增ID的位置遠一些,但必須在一、二兩臺MySQL實例ID還沒有增長到第三臺MySQL實例的起始ID值的時候,否則自增ID就要出現重複了,必要時可能還需要停機修改。

優點: 解決DB單點問題

缺點: 不利於後續擴容,而且實際上單個資料庫自身壓力還是大,依舊無法滿足高並發場景。

Redis模式

Redis 也同樣可以實現,原理就是Redis 是單線程的,因此我們可以利用redis的incr命令實現ID的原子性自增。

127.0.0.1:6379> set seq_id 1     // 初始化自增ID為1
OK
127.0.0.1:6379> incr seq_id      // 增加1,並返回遞增後的數值
(integer) 2

實現思路,大致和上面講資料庫的原理類似。用redis實現需要注意一點,要考慮到redis持久化的問題。redis有兩種持久化方式RDB和AOF。

號段模式

號段模式是當下分布式ID生成器的主流實現方式之一,號段模式可以理解為從資料庫(當然這邊存儲層也可用其他的,比如redis、Mongdb等)批量的獲取自增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`)
)

version :是一個樂觀鎖,每次都更新version,保證並發時數據的正確性

等這批號段ID用完,再次向資料庫申請新號段,對max_id欄位做一次update操作

update max_id= max_id + step

update成功則說明新號段獲取成功,新的號段範圍是(max_id ,max_id +step]。

由於多業務端可能同時操作,所以採用版本號 version 樂觀鎖方式更新。

優點: 這種分布式ID生成方式不強依賴於資料庫,不會頻繁的訪問資料庫,對資料庫的壓力小很多

缺點: 如果遇到了雙十一或者秒殺類似的活動還是會對資料庫有比較高的訪問,且如果再申請新號段的時候,遇到資料庫不可用時,ID生成也會出現問題。

雪花算法模式

SnowFlake 算法,是 Twitter 開源的分布式 id 生成算法。其核心思想就是:使用一個 64 bit 的 long 型的數字作為全局唯一 id。在分布式系統中的應用十分廣泛,且ID 引入了時間戳,為什麼叫雪花算法呢?眾所周知世界上沒有一對相同的雪花。雪花算法基本上保持自增的。

這 64 個 bit 中,其中1個bit是不用的,然後用其中的 41 bit 作為毫秒數,用 10 bit 作為工作機器 id,12 bit 作為序列號。舉例如上圖:

第一個部分是1個bit:0,這個是無意義的。因為二進位裡第一個 bit 位如果是 1,那麼都是負數,但是我們生成的 id 都是正數,所以第一個 bit 統一都是 0。

第二個部分是41個bit:表示的是時間戳。單位是毫秒。41 bit 可以表示的數字多達 2^41 - 1,也就是可以標識 2 ^ 41 - 1 個毫秒值,換算成年就是表示 69 年的時間。

第三個部分是10個bit:5個bit代表機房,意思就是最多代表2^5個機房(32個機房)。另5個bit代表機器id,意思就是每個機房裡可以代表2^5個機器(32臺機器)

第四個部分是12個bit:表示自增序號,就是某個機房某臺機器上這一毫秒內同時生成的 id 的序號。12bit可以代表的最大正整數是2^12-1=4096,也就是說可以用這個12bit代表的數字來區分同一個毫秒內的4096個不同的id。

總結的來說,就是用一個 64 bit位來設置不同的標誌位,區分每一個 id。

SnowFlake 算法的實現代碼 https://github.com/souyunku/SnowFlake

優點

缺點

依賴與系統時間的一致性,如果系統時間被回調,或者改變可能會造成id衝突或者重複。

實際中我們的機房並沒有那麼多,我們可以改進改算法,將10bit的機器id優化成業務ID或者業務系統實例數

後面講講業內一些公司基於以上幾種最基本的ID生成方式,進行的部分優化。

百度uid-generator

uid-generator是由百度技術部開發,基於Snowflake算法實現,與原始snowflake算法不同在於,uid-generator支持自定義時間戳、workId和 序列號等各部分的位數。提供了兩種方式,看下面分析

DefaultUidGenerator

DefaultUidGenerator 採用用戶自定義workId的生成策略。大致原理如下:需要新增一個WORKER_NODE表,當應用啟動時會向資料庫表中插入一條數據,插入成功後返回的自增ID就是workId。對於時鐘回撥的問題,DefaultUidGenerator採用了比較簡單粗暴的方式,直接拋出錯誤

由上圖可知,UidGenerator的時間部分只有28位,這就意味著UidGenerator默認只能承受8.5年(2^28-1/86400/365)。當然,根據你業務的需求,UidGenerator可以適當調整delta seconds、worker node id和sequence佔用位數。

CachedUidGenerator

CachedUidGenerator是UidGenerator的重要改進實現,workerId的獲取方式也和DefaultUidGenerator是一樣。核心優化的兩個點是:

利用RingBuffer數據結構預先生成若干個分布式ID並保存。通過時間值遞增得到時間值(lastSecond.incrementAndGet()),而不是System.currentTimeMillis()這種方式,保證了時間回撥的問題。

注意的是,CachedUidGenerator分布式ID中的時間信息可能並不是這個ID真正產生的時間點。

github地址:https://github.com/baidu/uid-generator

美團(Leaf)

Leaf支持號段模式和雪花算法模式,可以切換使用。

號段模式

思想和我們上面講的一致,它存儲採用資料庫。

雪花算法模式

Leaf的snowflake模式依賴於ZooKeeper,不同於原始snowflake算法是在workId的生成上。Leaf中workId是基於ZooKeeper的順序Id來生成的,每個應用在使用Leaf-snowflake時,啟動時都會都在Zookeeper中生成一個順序Id,相當於一臺機器對應一個順序節點,也就是一個workId。對於時間回撥的問題,美團採取的策略是等待5ms後重新獲取,如果發現時間還未追上,那麼進行告警。

github地址:https://github.com/Meituan-Dianping/Leaf

相關焦點

  • 分布式ID | 這六種分布式ID生成方法,總有一款適合你
    我是小小,我們又見面了,我們今天的話題是六種分布式ID生成算法。分布式ID簡介什麼是分布式ID在數據量不大的時候,單庫單表完全可以支撐現有業務,數據量再大一點搞個MySql主從同步也可以。數據量增長,到後期,需要進行分庫分表,顯然,這個時候需要一個全局唯一ID,而這個訂單號就是分布式ID。
  • 分布式ID生成服務,真的有必要搞一個
    很多時候為了圖方便可能就是寫一個簡單的 ID 生成工具類,直接開用。做的好點的可能單獨出一個 Jar 包讓其他項目依賴,做的不好的很有可能就是 Copy 了 N 份一樣的代碼。單獨搞一個獨立的 ID 生成服務非常有必要,當然我們也沒必要自己做造輪子,有現成開源的直接用就是了。如果人手夠,不差錢,自研也可以。
  • 最常用的分布式 ID 解決方案,都在這裡了!
    「二、分布式ID實現方案」下表為一些常用方案對比:描述優點缺點UUIDUUID是通用唯一標識碼的縮寫,其目的是上分布式系統中的所有元素都有唯一的辨識信息,而不需要通過中央控制器來指定唯一標識。1. 降低全局節點的壓力,使得主鍵生成速度更快;2. 生成的主鍵全局唯一;3.
  • 最常用的分布式ID解決方案,你知道幾個?
    二、分布式ID實現方案下表為一些常用方案對比:描述優點缺點UUIDUUID是通用唯一標識碼的縮寫,其目的是上分布式系統中的所有元素都有唯一的辨識信息,而不需要通過中央控制器來指定唯一標識。1. 降低全局節點的壓力,使得主鍵生成速度更快;2.
  • 40張圖帶你看懂分布式追蹤系統原理及實踐
    全局 trace_id:這是顯然的,這樣才能把每一個子調用與最初的請求關聯起來span_id: 圖中的 0,1,1.1,2,這樣就能標識是哪一個調用parent_span_id:比如 b 調用 d 的  span_id 是 1.1,那麼它的 parent_span_id 即為 a 調用 b 的 span_id 即 1,這樣才能把兩個緊鄰的調用關聯起來。
  • 分布式深度學習最佳入門(踩坑)指南
    ProcessGroupGloo,ProcessGroupNCCL和ProcessGroupMPI這3種分布式通訊實現分別對應:即本質上,pytorch的分布式多機訓練,依賴於以上這3種通信庫。的分布式訓練源碼為例,簡單講解下pytorch分布式訓練相關方法和參數。
  • 分庫分表之後,id 主鍵如何處理?
    這個方案的好處就是方便簡單,誰都會用;缺點就是單庫生成自增 id,要是高並發的話,就會有瓶頸的;如果你硬是要改進一下,那麼就專門開一個服務出來,這個服務每次就拿到當前 id 最大值,然後自己遞增幾個 id,一次性返回一批 id,然後再把當前最大 id 值修改成遞增幾個 id 之後的一個值;但是無論如何都是基於單個資料庫。
  • 分布式事務(一) 兩階段提交及JTA
    兩階段提交是保證分布式事務中原子性的重要方法。
  • 分布式SQL查詢引擎之Presto
    解決方法1: 避免單節點處理雖然Presto 是分布式查詢引擎,但是一些操作是必須在單節點中處理的。解決方法6:用大表取JOIN 小表下面這種用小數據表去JOIN 大數據表的查詢會極度消耗內存.SELECT * FROM small_table, large_tableWHERE small_table.id = large_table.idPresto 會默認執行廣播式的JOIN 操作,它會將左表拆分到幾個工作節點上,然後發送整個右表分別到已拆分好的處理左表的工作節點上。
  • 微服務 SpringCloud Alibaba Seata處理分布式事務
    二、Seata簡介1.是什麼Seata 是一款開源的分布式事務解決方案,致力於在微服務架構下提供高性能和簡單易用的分布式事務服務。官網地址:http://seata.io/zh-cn/2.主要功能一個典型的分布式事務過程分布式事務處理過程:一ID+三組件模型
  • 機器學習的分布式優化方法
    其中,前兩篇文章屬於經典的機器學習分布式優化方法(通信方式、內存分配方法),第三篇文章則是關於一種新的用於機器學習的具有高度系統性和設備(統計、數據)異質性的分布式方法--聯邦學習。然而,這仍然有一個局限性,即不能用這兩組連結構造一個統一的拓撲。作者通過構造兩個獨立的樹集來解決這個問題,一個在 PCIe 鏈路上,另一個在 NVLinks 上。這種方法的難點之一是平衡在每種鏈路類型上傳輸的數據量,作者使用的方法是最小化每個傳輸所花費的最大時間,即最小化 MAX(T_pCIe, T_NVL),其中 T_pCIe 和 T_NVL 表示每條鏈路上的數據總數。
  • xsequence 1.5 發布,分布式序列號生成組件
    這裡我提供了兩種業界常用的解決方案來實現這個分布式序列號生成組件。使用集中式存儲功能取步長進行分配。目前支持資料庫(Mysql)、Redis使用雪花算法獲取連續序列號,保證多伺服器集群不重複組件存在的目的就是屏蔽序列號底層實現,支持多樣化的算法。讓用戶開箱即用。方便開發。
  • PHP唯一ID生成模塊 Ukey V0.2 發布
    Ukey是一個生成唯一ID的PHP擴展模塊, 其按照Twitter的 Snowflake算法來生成ID, 所以效率非常高, 而且唯一性非常好.
  • PHP唯一ID生成模塊 Ukey V0.1 發布
    Ukey是一個生成唯一ID的PHP擴展模塊, 其安裝Twitter的 Snowflake算法來生成ID, 所以效率非常高, 而且唯一性非常好
  • 運維丨MySQL 生成隨機數字、字符串、日期、驗證碼及 UUID的方法
    例如:1234567891011SELECT emp_id,emp_nameFROM employeeORDER BY rand(1)LIMIT 5;emp_id|emp_name該方法需要為表中的每行數據都生成一個隨機數,然後進行排序;所以會隨著表中的數據量增加而逐漸變慢。如果表中存在自增主鍵,也可以基於主鍵生成一個隨機數據。
  • 如何生成高性能的短連結?
    那麼為啥要用短鍊表示,直接用長鏈不行嗎,用短鏈的話有如下好外1、連結變短,在對內容長度有限制的平臺發文,可編輯的文字就變多了短鏈生成的幾種方法1、哈希算法怎樣才能生成短鏈,仔細觀察上例中的短鏈,顯然它是由固定短鏈域名 + 長鏈映射成的一串字母組成,那麼長鏈怎麼才能映射成一串字母呢,哈希函數不就用來幹這事的嗎,於是我們有了以下設計思路
  • 博士論文摘要| 仇阿根:基於分布式內存計算的空間數據近似查詢處理方法
    根源在於空間資料庫中地理要素的查詢結果是精確、唯一的;查詢時間和數據量只與要素本身相關;查詢時地理要素無法根據條件動態生成。而在實際應用中,地理要素可以是近似的、變化的;查詢時間和數據量可以作為查詢約束條件;地理要素可以根據查詢條件動態生成。為此,本文提出以空間近似查詢結果表達地理要素,即通過頂點採樣實時生成要素並報告近似誤差,實現查詢時間和數據量的靈活控制。
  • 如何快速生成100萬不重複的8位編號
    首頁 > 語言 > 關鍵詞 > 最新資訊 > 正文 如何快速生成100萬不重複的8位編號
  • Node.js 中實踐基於 Redis 的分布式鎖實現
    那麼多線程編程模式下,例如 Java 你可能很熟悉一個詞 synchronized,通常也是 Java 中解決並發編程最簡單的一種方式,synchronized 可以保證在同一時刻僅有一個線程去執行某個方法或某塊代碼。
  • ACL2016最佳論文:通過整合基於路徑的方法和分布式的方法,改善詞對...
    分布式方法:其監督式的變體是目前最好的任務執行器;基於路徑的方法:它只受到少許的研究關注。我們發現,改善後的基於路徑的算法——其依賴的路徑(dependency path)通過遞歸神經網絡進行編碼——與分布式方法相比應該能達到理想結果。然後,我們將所用方法延伸為整合基於路徑的和分布式的信號,這顯著地將此任務上的性能提高到了當前最佳的水平。