7 种分布式全局 ID 生成策略,你更爱哪种?
上了微服務之后,很多原本很簡單的問題現在都變復雜了,例如全局 ID 這事!
最近工作中剛好用到這塊內容,于是調研了市面上幾種常見的全局 ID 生成策略,稍微做了一下對比,供小伙伴們參考。
當數據庫分庫分表之后,原本的主鍵自增就不方便繼續使用了,需要找到一個新的合適的方案,我的需求就是在這樣的情況下提出的。
接下來我們一起來捋一捋。
1. 兩種思路
整體上來說,這個問題有兩種不同的思路:
讓數據庫自己搞定
Java 代碼來處理主鍵,然后直接插入數據庫中即可。
這兩種思路又對應了不同的方案,我們一個一個來看。
2. 數據庫自己搞定
數據庫自己搞定,就是說我在數據插入的時候,依然不考慮主鍵的問題,希望繼續使用數據庫的主鍵自增,但是很明顯,原本默認的主鍵自增現在沒法用了,我們必須有新的方案。
2.1 修改數據庫配置
數據庫分庫分表之后的結構如下圖(假設數據庫中間件用的 MyCat):
此時如果原本的 db1、db2、db3 繼續各自主鍵自增,那么對于 MyCat 而言,主鍵就不是自增了,主鍵就會重復,用戶從 MyCat 中查詢到的數據主鍵就有問題。
找到問題的原因,那么剩下的就好解決了。
我們可以直接修改 MySQL 數據庫主鍵自增的起始值和步長。
首先我們可以通過如下 SQL 查看與此相關的兩個變量的取值:
SHOW?VARIABLES?LIKE?'auto_increment%'可以看到,主鍵自增的起始值和步長都是 1。
起始值好改,在定義表的時候就可以設置,步長我們可以通過修改這個配置實現:
set?@@auto_increment_increment=9;修改后,再去查看對應的變量值,發現已經變了:
此時我們再去插入數據,主鍵自增就不是每次自增 1,而是每次自增 9 了。
至于自增起始值其實很好設置,創建表的時候就可以設置了。
create?table?test01(id?integer?PRIMARY?KEY?auto_increment,username?varchar(255))?auto_increment=8;既然 MySQL 可以修改自增的起始值和每次增長的步長,現在假設我有 db1、db2 和 db3,我就可以分別設置這三個庫中表的自增起始值為 1、2、3,然后自增步長都是 3,這樣就可以實現自增了。
但是很明顯這種方式不夠優雅,而且處理起來很麻煩,將來擴展也不方便,因此不推薦。
2.2 MySQL+MyCat+ZooKeeper
如果大家分庫分表工具恰好使用的是 MyCat,那么結合 Zookeeper 也能很好的實現主鍵全局自增。
MyCat 作為一個分布式數據庫中間,屏蔽了數據庫集群的操作,讓我們操作數據庫集群就像操作單機版數據庫一樣,對于主鍵自增,它有自己的方案:
通過本地文件實現
通過數據庫實現
通過本地時間戳實現
通過分布式 ZK ID 生成器實現
通過 ZK 遞增方式實現
這里我們主要來看方案 4。
配置步驟如下:
首先修改主鍵自增方式為 4 ,4 表示使用 zookeeper 實現主鍵自增。
server.xml
配置表自增,并且設置主鍵
schema.xml
設置主鍵自增,并且設置主鍵為 id 。
配置 zookeeper 的信息
在 myid.properties 中配置 zookeeper 信息:
配置要自增的表
sequence_conf.properties
注意,這里表名字要大寫。
TABLE.MINID 某線程當前區間內最小值
TABLE.MAXID 某線程當前區間內最大值
TABLE.CURID 某線程當前區間內當前值
文件配置的MAXID以及MINID決定每次取得區間,這個對于每個線程或者進程都有效
文件中的這三個屬性配置只對第一個進程的第一個線程有效,其他線程和進程會動態讀取 ZK
重啟 MyCat 測試
最后重啟 MyCat ,刪掉之前創建的表,然后創建新表進行測試即可。
這種方式就比較省事一些,而且可擴展性也比較強,如果選擇了 MyCat 作為分庫分表工具,那么這種不失為一種最佳方案。
前面介紹這兩種都是在數據庫或者數據庫中間件層面來處理主鍵自增,我們 Java 代碼并不需要額外工作。
接下來我們再來看幾種需要在 Java 代碼中進行處理的方案。
3. Java 代碼處理
3.1 UUID
最容易想到的就是 UUID (Universally Unique Identifier) 了, UUID 的標準型式包含 32 個 16 進制數字,以連字號分為五段,形式為 8-4-4-4-12 的 36 個字符,這個是 Java 自帶的,用著也簡單,最大的優勢就是本地生成,沒有網絡消耗,但是但凡在公司做開發的小伙伴都知道這個東西在公司項目中使用并不多。原因如下:
字符串太長,對于 MySQL 而言,不利于索引。
UUID 的隨機性對于 I/O 密集型的應用非常不友好!它會使得聚簇索引的插入變得完全隨機,使得數據沒有任何聚集特性。
信息不安全:基于 MAC 地址生成 UUID 的算法可能會造成 MAC 地址泄露,這個漏洞曾被用于尋找梅麗莎病毒的制作者位置。
因此,UUID 并非最佳方案。
3.2 SNOWFLAKE
雪花算法是由 Twitter 公布的分布式主鍵生成算法,它能夠保證不同進程主鍵的不重復性,以及相同進程主鍵的有序性。在同一個進程中,它首先是通過時間位保證不重復,如果時間相同則是通過序列位保證。
同時由于時間位是單調遞增的,且各個服務器如果大體做了時間同步,那么生成的主鍵在分布式環境可以認為是總體有序的,這就保證了對索引字段的插入的高效性。
例如 MySQL 的 Innodb 存儲引擎的主鍵。使用雪花算法生成的主鍵,二進制表示形式包含 4 部分,從高位到低位分表為:1bit 符號位、41bit 時間戳位、10bit 工作進程位以及 12bit 序列號位。
符號位 (1bit)
預留的符號位,恒為零。
時間戳位 (41bit)
41 位的時間戳可以容納的毫秒數是 2 的 41 次冪,一年所使用的毫秒數是:365 * 24 * 60 * 60 * 1000。通過計算可知:Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);結果約等于 69.73 年。
ShardingSphere 的雪花算法的時間紀元從 2016 年 11 月 1 日零點開始,可以使用到 2086 年,相信能滿足絕大部分系統的要求。
工作進程位 (10bit)
該標志在 Java 進程內是唯一的,如果是分布式應用部署應保證每個工作進程的 id 是不同的。該值默認為 0,可通過屬性設置。
序列號位 (12bit)
該序列是用來在同一個毫秒內生成不同的 ID。如果在這個毫秒內生成的數量超過 4096 (2 的 12 次冪),那么生成器會等待到下個毫秒繼續生成。
?注意:該算法存在 時鐘回撥 問題,服務器時鐘回撥會導致產生重復序列,因此默認分布式主鍵生成器提供了一個最大容忍的時鐘回撥毫秒數。如果時鐘回撥的時間超過最大容忍的毫秒數閾值,則程序報錯;如果在可容忍的范圍內,默認分布式主鍵生成器會等待時鐘同步到最后一次主鍵生成的時間后再繼續工作。最大容忍的時鐘回撥毫秒數的默認值為 0,可通過屬性設置。
下面我給出一個雪花算法的工具類,大家可以參考:
public?class?IdWorker?{//?時間起始標記點,作為基準,一般取系統的最近時間(一旦確定不能變動)private?final?static?long?twepoch?=?1288834974657L;//?機器標識位數private?final?static?long?workerIdBits?=?5L;//?數據中心標識位數private?final?static?long?datacenterIdBits?=?5L;//?機器ID最大值private?final?static?long?maxWorkerId?=?-1L?^?(-1L?<<?workerIdBits);//?數據中心ID最大值private?final?static?long?maxDatacenterId?=?-1L?^?(-1L?<<?datacenterIdBits);//?毫秒內自增位private?final?static?long?sequenceBits?=?12L;//?機器ID偏左移12位private?final?static?long?workerIdShift?=?sequenceBits;//?數據中心ID左移17位private?final?static?long?datacenterIdShift?=?sequenceBits?+?workerIdBits;//?時間毫秒左移22位private?final?static?long?timestampLeftShift?=?sequenceBits?+?workerIdBits?+?datacenterIdBits;private?final?static?long?sequenceMask?=?-1L?^?(-1L?<<?sequenceBits);/*?上次生產id時間戳?*/private?static?long?lastTimestamp?=?-1L;//?0,并發控制private?long?sequence?=?0L;private?final?long?workerId;//?數據標識id部分private?final?long?datacenterId;public?IdWorker(){this.datacenterId?=?getDatacenterId(maxDatacenterId);this.workerId?=?getMaxWorkerId(datacenterId,?maxWorkerId);}/***?@param?workerId*????????????工作機器ID*?@param?datacenterId*????????????序列號*/public?IdWorker(long?workerId,?long?datacenterId)?{if?(workerId?>?maxWorkerId?||?workerId?<?0)?{throw?new?IllegalArgumentException(String.format("worker?Id?can't?be?greater?than?%d?or?less?than?0",?maxWorkerId));}if?(datacenterId?>?maxDatacenterId?||?datacenterId?<?0)?{throw?new?IllegalArgumentException(String.format("datacenter?Id?can't?be?greater?than?%d?or?less?than?0",?maxDatacenterId));}this.workerId?=?workerId;this.datacenterId?=?datacenterId;}/***?獲取下一個ID**?@return*/public?synchronized?long?nextId()?{long?timestamp?=?timeGen();if?(timestamp?<?lastTimestamp)?{throw?new?RuntimeException(String.format("Clock?moved?backwards.??Refusing?to?generate?id?for?%d?milliseconds",?lastTimestamp?-?timestamp));}if?(lastTimestamp?==?timestamp)?{//?當前毫秒內,則+1sequence?=?(sequence?+?1)?&?sequenceMask;if?(sequence?==?0)?{//?當前毫秒內計數滿了,則等待下一秒timestamp?=?tilNextMillis(lastTimestamp);}}?else?{sequence?=?0L;}lastTimestamp?=?timestamp;//?ID偏移組合生成最終的ID,并返回IDlong?nextId?=?((timestamp?-?twepoch)?<<?timestampLeftShift)|?(datacenterId?<<?datacenterIdShift)|?(workerId?<<?workerIdShift)?|?sequence;return?nextId;}private?long?tilNextMillis(final?long?lastTimestamp)?{long?timestamp?=?this.timeGen();while?(timestamp?<=?lastTimestamp)?{timestamp?=?this.timeGen();}return?timestamp;}private?long?timeGen()?{return?System.currentTimeMillis();}/***?<p>*?獲取?maxWorkerId*?</p>*/protected?static?long?getMaxWorkerId(long?datacenterId,?long?maxWorkerId)?{StringBuffer?mpid?=?new?StringBuffer();mpid.append(datacenterId);String?name?=?ManagementFactory.getRuntimeMXBean().getName();if?(!name.isEmpty())?{/**?GET?jvmPid*/mpid.append(name.split("@")[0]);}/**?MAC?+?PID?的?hashcode?獲取16個低位*/return?(mpid.toString().hashCode()?&?0xffff)?%?(maxWorkerId?+?1);}/***?<p>*?數據標識id部分*?</p>*/protected?static?long?getDatacenterId(long?maxDatacenterId)?{long?id?=?0L;try?{InetAddress?ip?=?InetAddress.getLocalHost();NetworkInterface?network?=?NetworkInterface.getByInetAddress(ip);if?(network?==?null)?{id?=?1L;}?else?{byte[]?mac?=?network.getHardwareAddress();id?=?((0x000000FF?&?(long)?mac[mac.length?-?1])|?(0x0000FF00?&?(((long)?mac[mac.length?-?2])?<<?8)))?>>?6;id?=?id?%?(maxDatacenterId?+?1);}}?catch?(Exception?e)?{System.out.println("?getDatacenterId:?"?+?e.getMessage());}return?id;} }用法如下:
IdWorker?idWorker?=?new?IdWorker(0,?0); for?(int?i?=?0;?i?<?1000;?i++)?{System.out.println(idWorker.nextId()); }3.3 LEAF
Leaf 是美團開源的分布式 ID 生成系統,最早期需求是各個業務線的訂單 ID 生成需求。在美團早期,有的業務直接通過 DB 自增的方式生成 ID,有的業務通過 Redis 緩存來生成 ID,也有的業務直接用 UUID 這種方式來生成 ID。以上的方式各自有各自的問題,因此美團決定實現一套分布式 ID 生成服務來滿足需求目前 Leaf 覆蓋了美團點評公司內部金融、餐飲、外賣、酒店旅游、貓眼電影等眾多業務線。在4C8G VM 基礎上,通過公司 RPC 方式調用,QPS 壓測結果近 5w/s,TP999 1ms(TP=Top Percentile,Top 百分數,是一個統計學里的術語,與平均數、中位數都是一類。TP50、TP90 和 TP99 等指標常用于系統性能監控場景,指高于 50%、90%、99% 等百分線的情況)。
目前 LEAF 的使用有兩種不同的思路,號段模式和 SNOWFLAKE 模式,你可以同時開啟兩種方式,也可以指定開啟某種方式(默認兩種方式為關閉狀態)。
我們從 GitHub 上 Clone LEAF 之后,它的配置文件在 leaf-server/src/main/resources/leaf.properties 中,各項配置的含義如下:
。
可以看到,如果使用號段模式,需要數據庫支持;如果使用 SNOWFLAKE 模式,需要 Zookeeper 支持。
3.3.1 號段模式
號段模式還是基于數據庫,但是思路有些變化,如下:
利用 proxy server 從數據庫中批量獲取 id,每次獲取一個 segment (step 決定其大小) 號段的值,用完之后再去數據庫獲取新的號段,可以大大的減輕數據庫的壓力。
各個業務不同的發號需求用 biz_tag 字段來區分,每個 biz-tag 的 ID 獲取相互隔離,互不影響。
如果有新的業務需要擴區 ID,只需要增加表記錄即可。
如果使用號段模式,我們首先需要創建一張數據表,腳本如下:
CREATE?DATABASE?leaf CREATE?TABLE?`leaf_alloc`?(`biz_tag`?varchar(128)??NOT?NULL?DEFAULT?'',`max_id`?bigint(20)?NOT?NULL?DEFAULT?'1',`step`?int(11)?NOT?NULL,`description`?varchar(256)??DEFAULT?NULL,`update_time`?timestamp?NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?ON?UPDATE?CURRENT_TIMESTAMP,PRIMARY?KEY?(`biz_tag`) )?ENGINE=InnoDB;insert?into?leaf_alloc(biz_tag,?max_id,?step,?description)?values('leaf-segment-test',?1,?2000,?'Test?leaf?Segment?Mode?Get?Id')這張表中各項字段的含義如下:
biz_tag:業務標記(不同業務可以有不同的號段序列)
max_id:當前號段下的最大 id
step:每次取號段的步長
description:描述信息
update_time:更新時間
配置完成后,啟動項目,訪問 http://localhost:8080/api/segment/get/leaf-segment-test 路徑(路徑最后面的 leaf-segment-test 是業務標記),即可拿到 ID。
可以通過如下地址訪問到號段模式的監控頁面 http://localhost:8080/cache。
號段模式優缺點:
優點
Leaf 服務可以很方便的線性擴展,性能完全能夠支撐大多數業務場景。
ID 號碼是趨勢遞增的 8byte 的 64 位數字,滿足上述數據庫存儲的主鍵要求。
容災性高:Leaf 服務內部有號段緩存,即使 DB 宕機,短時間內 Leaf 仍能正常對外提供服務。
可以自定義 max_id 的大小,非常方便業務從原有的 ID 方式上遷移過來。
缺點
ID 號碼不夠隨機,能夠泄露發號數量的信息,不太安全。
DB 宕機會造成整個系統不可用。
3.3.2 SNOWFLAKE 模式
SNOWFLAKE 模式需要配合 Zookeeper 一起,不過 SNOWFLAKE 對 Zookeeper 的依賴是弱依賴,把 Zookeeper 啟動之后,我們可以在 SNOWFLAKE 中配置 Zookeeper 信息,如下:
leaf.snowflake.enable=true leaf.snowflake.zk.address=192.168.91.130 leaf.snowflake.port=2183然后重新啟動項目,啟動成功后,通過如下地址可以訪問到 ID:
http://localhost:8080/api/snowflake/get/test3.4 Redis 生成
這個主要是利用 Redis 的 incrby 來實現,這個我覺得沒啥好說的。
3.5 Zookeeper 處理
zookeeper 也能做,但是比較麻煩,不推薦。
4. 小結
綜上,如果項目中恰好使用了 MyCat,那么可以使用 MyCat+Zookeeper,否則建議使用 LEAF,兩種模式皆可。
往期推薦實戰:隱藏SpringBoot中的私密數據!
@Autowired的這些騷操作,你都知道嗎?
SpringBoot時間格式化的5種方法!
總結
以上是生活随笔為你收集整理的7 种分布式全局 ID 生成策略,你更爱哪种?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: __asm___错误:“”前应有'=',
- 下一篇: Java IdentityHashMap