分佈式 ID 生成 一些常見思路和實例

在公衆號上看到一篇寫得不錯的分佈式 Id 生成方式,特意搬過來,提升下 CSDN 的技術水平 ,並且加了自己的實踐,不能算是抄襲啊,原作者如果想要刪除本文,請私信我


本人實踐項目:https://gitee.com/sanri/example/tree/master/testdistributedid


爲什麼要用分佈式 ID

在業務數據量不大的時候,這時我們單表可以解決,我認爲 mysql 單表最好不要超過 500 萬,這個時候可以使用 Mysql 的自增 ID ,但如果業務起來了,分庫分表後,多庫生成的 Id 就會衝突,主鍵的唯一性就得不到保證了。此時一個能夠生成 全局唯一ID 的系統是非常必要的。那麼這個全局唯一ID就叫分佈式ID

分佈式 ID 需要滿足什麼條件

  • 全局唯一:必須保證ID是全局性唯一的,基本要求
  • 高性能:高可用低延時,ID生成響應要塊,否則反倒會成爲業務瓶頸
  • 高可用:100%的可用性是騙人的,但是也要無限接近於100%的可用性
  • 好接入:要秉着拿來即用的設計原則,在系統設計和實現上要儘可能的簡單
  • 趨勢遞增:最好趨勢遞增,這個要求就得看具體業務場景了,一般不嚴格要求

分佈式 ID 都有哪些生成方式

今天主要分析一下以下9種,分佈式ID生成器方式以及優缺點:

  • UUID
  • 數據庫自增ID
  • 數據庫多主模式
  • 號段模式
  • Redis
  • 雪花算法(SnowFlake)
  • 滴滴出品(TinyID)
  • 百度 (Uidgenerator)
  • 美團(Leaf)

基於UUID

在Java的世界裏,想要得到一個具有唯一性的ID,首先被想到可能就是UUID,畢竟它有着全球唯一的特性。那麼UUID可以做分佈式ID嗎?答案是可以的,但是並不推薦!

public String generateId(int bizType) {
    return UUID.randomUUID().toString().replace("-","");
}

UUID的生成簡單到只有一行代碼,輸出結果 c2b8c2b9e46c47e3b30dca3b0d447718,但UUID卻並不適用於實際的業務需求。像用作訂單號UUID這樣的字符串沒有絲毫的意義,看不出和訂單相關的有用信息;而對於數據庫來說用作業務主鍵ID,它不僅是太長還是字符串,存儲性能差查詢也很耗時,所以不推薦用作分佈式ID

優點:

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

缺點:

  • 無序的字符串,不具備趨勢自增特性
  • 沒有具體的業務含義
  • 長度過長16 字節128位,36位長度的字符串,存儲以及查詢對MySQL的性能消耗較大,MySQL官方明確建議主鍵要儘量越短越好,作爲數據庫主鍵 UUID 的無序性會導致數據位置頻繁變動,嚴重影響性能。

基於數據庫自增 ID

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

create table SEQUENCE_ID(
	id bigint(20) unsigned not null auto_increment,
	value char(1) not null default '',
	primary key (id)
) engine= innodb;

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

優點:

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

缺點:

  • DB單點存在宕機風險,無法扛住高併發場景

基於數據庫集羣模式

前邊說了單點數據庫方式不可取,那對上邊的方式做一些高可用優化,換成主從模式集羣。害怕一個主節點掛掉沒法用,那就做雙主模式集羣,也就是兩個Mysql實例都能單獨的生產自增ID。

那這樣還會有個問題,兩個MySQL實例的自增ID都從1開始,會生成重複的ID怎麼辦?

解決方案:設置起始值自增步長

mysql1 產生的 id :1,3,5,7

mysql2 產生的 id :2,4,6,8

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

優點:

  • 解決DB單點問題

缺點:

  • 不利於後續擴容,而且實際上單個數據庫自身壓力還是大,依舊無法滿足高併發場景。

基於數據庫號段模式

號段模式是當下分佈式ID生成器的主流實現方式之一,號段模式可以理解爲從數據庫批量的獲取自增ID,每次從數據庫取出一個號段範圍,例如 (1,1000] 代表1000個ID,具體的業務服務將本號段,生成1~1000的自增ID並加載到內存,這種方式比較重要,我也將主要介紹這種方式。 建立數據表

CREATE TABLE id_generator (
  id int(10) NOT NULL auto_increment,
  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`)
) engine= innodb;

考慮到併發修改,使用 version 做一個樂觀鎖,也可以使用事務鎖,但性能會降低

首次使用時需要初始化號段,可使用 sql , 也可以在應用啓動時候去初始化某個業務的號段

-- 初始化號段表
insert into id_generator(max_id,step,biz_type,version) values (0,1000,1000,0);

業務端每次去數據庫取一個號段,並更新數據版本和 max_id

-- 取某個業務的 max_id 
select id,max_id,step,biz_type,version from id_generator where biz_type = ?
-- 更新 max_id 取到號段
update id_generator set max_id = max_id + step ,version = version + 1 where id = ? and max_id = ? and version = ?

業務端取完之後,可以將所有 id 存在一個 BlockQueue 中,當 blockQueue 中的 id 量不夠的時候(我們可以設置一個 閾值如 50% ),向數據庫異步發起一個請求獲取 id ,這個時候如果消費端消息完了,可以用到 blockQueue 的阻塞功能 ,blockQueue 也支持併發修改

這種方式不能分庫分表,但可以使用主從,因爲這種分佈式ID生成方式不強依賴於數據庫,不會頻繁的訪問數據庫,對數據庫的壓力小很多,如果主庫掛了,業務端還是在一段時間內是有 id 可用的,可以在這段時間內快速做主從切換

基於 Redis 模式

可以用 redis 的 incr 命令實現 id 的原子性自增

set seq_id 1 
incr seq_id 

那如果 redis down 了怎麼辦,可以使用 redis 主從模式+哨兵模式,如果在高峯時候 redis down 了 , 在切換從庫的時候怎麼辦呢,在切換這個期間也是有空檔的,如果非要這樣的高可用,則可以使用 redis 的號段模式,incr 可以加一個號段 , 並把加載到的 id 存到應用本地,如果 redis down 機,應用在 redis 切換從庫還是有 id 可用的。

incrby seq_id 1000

基於雪花算法 (Snowflake)

雪花算法(Snowflake)是twitter公司內部分佈式項目採用的ID生成算法,開源後廣受國內大廠的好評,在該算法影響下各大公司相繼開發出各具特色的分佈式生成器。

Snowflake生成的是Long類型的ID,一個Long類型佔8個字節,每個字節佔8比特,也就是說一個Long類型佔64個比特。

Snowflake ID組成結構:正數位(佔1比特)+ 時間戳(佔41比特)+ 機器ID(佔5比特)+ 數據中心(佔5比特)+ 自增值(佔12比特),總共64比特組成的一個Long類型。

  • 第一個bit位(1bit):Java中long的最高位是符號位代表正負,正數是0,負數是1,一般生成ID都爲正數,所以默認爲0。
  • 時間戳部分(41bit):毫秒級的時間,不建議存當前時間戳,而是用(當前時間戳 - 固定開始時間戳)的差值,可以使產生的ID從更小的值開始;41位的時間戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
  • 工作機器id(10bit):也被叫做workId,這個可以靈活配置,機房或者機器號組合都可以。
  • 序列號部分(12bit),自增值支持同一毫秒內同一個節點可以生成4096個ID

根據這個算法的邏輯,只需要將這個算法用Java語言實現出來,封裝爲一個工具方法,那麼各個業務應用可以直接使用該工具方法來獲取分佈式ID,只需保證每個業務應用有自己的工作機器id即可,而不需要單獨去搭建一個獲取分佈式ID的應用。

雪花算法的 Java 版本在我的 實踐項目 有,這裏就不貼代碼了

基於大廠寫好的分佈式 ID 生成工具

TinyId , Uidgenerator ,Leaf 大都是基於雪花算法的改進或使用號段模式,我們沒必要重複造輪子,有開源好用的,就直接拿來用。

我的實踐中集成了百度的 UID 生成,百度是基於雪花算法的,其它的就沒弄了,好像是要單獨啓服務的

這種公共服務可以部署成服務,使用多實例來提供高可用性,多臺server之間因爲生成算法的原子性,而保證每臺server上的可用號段不重,從而使id生成不重。

img

友情鏈接

我的一個工具 sanri-tools , 可以做 kafka 監控(主題,消費組,分區,反序列化) , redis 數據查看(可以反序列化字段信息看到真實數據),數據表管理,代碼生成

sanri-tools

我的博客文章大綱

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章