業務量小於500W或數據容量小於2G的時候單獨一個mysql即可提供服務,再大點的時候就進行讀寫分離也可以應付過來。但當主從同步也扛不住的時候就需要分表分庫了,但分庫分表後需要有一個唯一ID來標識一條數據,且這個唯一ID還必須有規則,能輔助我們解決分庫分表的一些問題。
資料庫的自增ID顯然不能滿足需求;特別一點的如訂單、優惠券也都需要有唯一ID做標識。此時一個能夠生成全局唯一ID的系統是非常必要的,那麼這個全局唯一ID就叫分布式ID。
分布式ID需滿足那些條件
全局唯一:基本要求就是必須保證ID是全局性唯一的。好接入:遵循拿來主義原則,在系統設計和實現上要儘可能的簡單趨勢遞增:最好趨勢遞增,這個要求就得看具體業務場景了,一般不嚴格要求UUIDUUID 是指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-generatoruid-generator是由百度技術部開發,基於Snowflake算法實現,與原始snowflake算法不同在於,uid-generator支持自定義時間戳、workId和 序列號等各部分的位數。提供了兩種方式,看下面分析
DefaultUidGeneratorDefaultUidGenerator 採用用戶自定義workId的生成策略。大致原理如下:需要新增一個WORKER_NODE表,當應用啟動時會向資料庫表中插入一條數據,插入成功後返回的自增ID就是workId。對於時鐘回撥的問題,DefaultUidGenerator採用了比較簡單粗暴的方式,直接拋出錯誤
由上圖可知,UidGenerator的時間部分只有28位,這就意味著UidGenerator默認只能承受8.5年(2^28-1/86400/365)。當然,根據你業務的需求,UidGenerator可以適當調整delta seconds、worker node id和sequence佔用位數。
CachedUidGeneratorCachedUidGenerator是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