在公衆號上看到一篇寫得不錯的分佈式 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生成不重。
友情鏈接
我的一個工具 sanri-tools , 可以做 kafka 監控(主題,消費組,分區,反序列化) , redis 數據查看(可以反序列化字段信息看到真實數據),數據表管理,代碼生成