@[toc]
上了微服務(wù)之后两入,很多原本很簡單的問題現(xiàn)在都變復(fù)雜了,例如全局 ID 這事敲才!
松哥最近工作中剛好用到這塊內(nèi)容裹纳,于是調(diào)研了市面上幾種常見的全局 ID 生成策略,稍微做了一下對比紧武,供小伙伴們參考剃氧。
當(dāng)數(shù)據(jù)庫分庫分表之后,原本的主鍵自增就不方便繼續(xù)使用了阻星,需要找到一個新的合適的方案朋鞍,松哥的需求就是在這樣的情況下提出的。
接下來我們一起來捋一捋。
1. 兩種思路
整體上來說滥酥,這個問題有兩種不同的思路:
- 讓數(shù)據(jù)庫自己搞定
- Java 代碼來處理主鍵更舞,然后直接插入數(shù)據(jù)庫中即可。
這兩種思路又對應(yīng)了不同的方案坎吻,我們一個一個來看缆蝉。
2. 數(shù)據(jù)庫自己搞定
數(shù)據(jù)庫自己搞定,就是說我在數(shù)據(jù)插入的時候瘦真,依然不考慮主鍵的問題返奉,希望繼續(xù)使用數(shù)據(jù)庫的主鍵自增,但是很明顯吗氏,原本默認(rèn)的主鍵自增現(xiàn)在沒法用了芽偏,我們必須有新的方案。
2.1 修改數(shù)據(jù)庫配置
數(shù)據(jù)庫分庫分表之后的結(jié)構(gòu)如下圖(假設(shè)數(shù)據(jù)庫中間件用的 MyCat):
此時如果原本的 db1弦讽、db2污尉、db3 繼續(xù)各自主鍵自增,那么對于 MyCat 而言往产,主鍵就不是自增了被碗,主鍵就會重復(fù),用戶從 MyCat 中查詢到的數(shù)據(jù)主鍵就有問題仿村。
找到問題的原因锐朴,那么剩下的就好解決了。
我們可以直接修改 MySQL 數(shù)據(jù)庫主鍵自增的起始值和步長蔼囊。
首先我們可以通過如下 SQL 查看與此相關(guān)的兩個變量的取值:
SHOW VARIABLES LIKE 'auto_increment%'
可以看到焚志,主鍵自增的起始值和步長都是 1。
起始值好改畏鼓,在定義表的時候就可以設(shè)置酱酬,步長我們可以通過修改這個配置實現(xiàn):
set @@auto_increment_increment=9;
修改后,再去查看對應(yīng)的變量值云矫,發(fā)現(xiàn)已經(jīng)變了:
此時我們再去插入數(shù)據(jù)膳沽,主鍵自增就不是每次自增 1,而是每次自增 9 了让禀。
至于自增起始值其實很好設(shè)置挑社,創(chuàng)建表的時候就可以設(shè)置了。
create table test01(id integer PRIMARY KEY auto_increment,username varchar(255)) auto_increment=8;
既然 MySQL 可以修改自增的起始值和每次增長的步長巡揍,現(xiàn)在假設(shè)我有 db1痛阻、db2 和 db3,我就可以分別設(shè)置這三個庫中表的自增起始值為 1吼肥、2录平、3麻车,然后自增步長都是 3缀皱,這樣就可以實現(xiàn)自增了斗这。
但是很明顯這種方式不夠優(yōu)雅,而且處理起來很麻煩啤斗,將來擴(kuò)展也不方便表箭,因此不推薦。
2.2 MySQL+MyCat+ZooKeeper
如果大家分庫分表工具恰好使用的是 MyCat钮莲,那么結(jié)合 Zookeeper 也能很好的實現(xiàn)主鍵全局自增免钻。
MyCat 作為一個分布式數(shù)據(jù)庫中間,屏蔽了數(shù)據(jù)庫集群的操作崔拥,讓我們操作數(shù)據(jù)庫集群就像操作單機(jī)版數(shù)據(jù)庫一樣极舔,對于主鍵自增,它有自己的方案:
- 通過本地文件實現(xiàn)
- 通過數(shù)據(jù)庫實現(xiàn)
- 通過本地時間戳實現(xiàn)
- 通過分布式 ZK ID 生成器實現(xiàn)
- 通過 ZK 遞增方式實現(xiàn)
這里我們主要來看方案 4链瓦。
配置步驟如下:
- 首先修改主鍵自增方式為 4 拆魏,4 表示使用 zookeeper 實現(xiàn)主鍵自增。
server.xml
- 配置表自增慈俯,并且設(shè)置主鍵
schema.xml
設(shè)置主鍵自增渤刃,并且設(shè)置主鍵為 id 。
- 配置 zookeeper 的信息
在 myid.properties 中配置 zookeeper 信息:
- 配置要自增的表
sequence_conf.properties
注意贴膘,這里表名字要大寫卖子。
- TABLE.MINID 某線程當(dāng)前區(qū)間內(nèi)最小值
- TABLE.MAXID 某線程當(dāng)前區(qū)間內(nèi)最大值
- TABLE.CURID 某線程當(dāng)前區(qū)間內(nèi)當(dāng)前值
- 文件配置的MAXID以及MINID決定每次取得區(qū)間,這個對于每個線程或者進(jìn)程都有效
- 文件中的這三個屬性配置只對第一個進(jìn)程的第一個線程有效刑峡,其他線程和進(jìn)程會動態(tài)讀取 ZK
- 重啟 MyCat 測試
最后重啟 MyCat 洋闽,刪掉之前創(chuàng)建的表,然后創(chuàng)建新表進(jìn)行測試即可突梦。
這種方式就比較省事一些喊递,而且可擴(kuò)展性也比較強(qiáng),如果選擇了 MyCat 作為分庫分表工具阳似,那么這種不失為一種最佳方案骚勘。
前面介紹這兩種都是在數(shù)據(jù)庫或者數(shù)據(jù)庫中間件層面來處理主鍵自增,我們 Java 代碼并不需要額外工作撮奏。
接下來我們再來看幾種需要在 Java 代碼中進(jìn)行處理的方案俏讹。
3. Java 代碼處理
3.1 UUID
最容易想到的就是 UUID (Universally Unique Identifier) 了,
UUID 的標(biāo)準(zhǔn)型式包含 32 個 16 進(jìn)制數(shù)字畜吊,以連字號分為五段泽疆,形式為 8-4-4-4-12 的 36 個字符,這個是 Java 自帶的玲献,用著也簡單殉疼,最大的優(yōu)勢就是本地生成梯浪,沒有網(wǎng)絡(luò)消耗,但是但凡在公司做開發(fā)的小伙伴都知道這個東西在公司項目中使用并不多瓢娜。原因如下:
- 字符串太長挂洛,對于 MySQL 而言,不利于索引眠砾。
- UUID 的隨機(jī)性對于 I/O 密集型的應(yīng)用非常不友好虏劲!它會使得聚簇索引的插入變得完全隨機(jī),使得數(shù)據(jù)沒有任何聚集特性褒颈。
- 信息不安全:基于 MAC 地址生成 UUID 的算法可能會造成 MAC 地址泄露柒巫,這個漏洞曾被用于尋找梅麗莎病毒的制作者位置。
因此谷丸,UUID 并非最佳方案堡掏。
3.2 SNOWFLAKE
雪花算法是由 Twitter 公布的分布式主鍵生成算法,它能夠保證不同進(jìn)程主鍵的不重復(fù)性刨疼,以及相同進(jìn)程主鍵的有序性泉唁。在同一個進(jìn)程中,它首先是通過時間位保證不重復(fù)币狠,如果時間相同則是通過序列位保證游两。
同時由于時間位是單調(diào)遞增的,且各個服務(wù)器如果大體做了時間同步漩绵,那么生成的主鍵在分布式環(huán)境可以認(rèn)為是總體有序的贱案,這就保證了對索引字段的插入的高效性。
例如 MySQL 的 Innodb 存儲引擎的主鍵止吐。使用雪花算法生成的主鍵宝踪,二進(jìn)制表示形式包含 4 部分,從高位到低位分表為:1bit 符號位碍扔、41bit 時間戳位瘩燥、10bit 工作進(jìn)程位以及 12bit 序列號位。
- 符號位 (1bit)
預(yù)留的符號位不同,恒為零厉膀。
- 時間戳位 (41bit)
41 位的時間戳可以容納的毫秒數(shù)是 2 的 41 次冪,一年所使用的毫秒數(shù)是:365 * 24 * 60 * 60 * 1000二拐。通過計算可知:Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);
結(jié)果約等于 69.73 年服鹅。
ShardingSphere 的雪花算法的時間紀(jì)元從 2016 年 11 月 1 日零點開始,可以使用到 2086 年百新,相信能滿足絕大部分系統(tǒng)的要求企软。
- 工作進(jìn)程位 (10bit)
該標(biāo)志在 Java 進(jìn)程內(nèi)是唯一的,如果是分布式應(yīng)用部署應(yīng)保證每個工作進(jìn)程的 id 是不同的饭望。該值默認(rèn)為 0仗哨,可通過屬性設(shè)置形庭。
- 序列號位 (12bit)
該序列是用來在同一個毫秒內(nèi)生成不同的 ID。如果在這個毫秒內(nèi)生成的數(shù)量超過 4096 (2 的 12 次冪)厌漂,那么生成器會等待到下個毫秒繼續(xù)生成萨醒。
注意: 該算法存在 時鐘回?fù)?/strong> 問題,服務(wù)器時鐘回?fù)軙?dǎo)致產(chǎn)生重復(fù)序列桩卵,因此默認(rèn)分布式主鍵生成器提供了一個最大容忍的時鐘回?fù)芎撩霐?shù)验靡。 如果時鐘回?fù)艿臅r間超過最大容忍的毫秒數(shù)閾值倍宾,則程序報錯雏节;如果在可容忍的范圍內(nèi),默認(rèn)分布式主鍵生成器會等待時鐘同步到最后一次主鍵生成的時間后再繼續(xù)工作高职。 最大容忍的時鐘回?fù)芎撩霐?shù)的默認(rèn)值為 0钩乍,可通過屬性設(shè)置。
下面松哥給出一個雪花算法的工具類怔锌,大家可以參考:
public class IdWorker {
// 時間起始標(biāo)記點寥粹,作為基準(zhǔn),一般取系統(tǒng)的最近時間(一旦確定不能變動)
private final static long twepoch = 1288834974657L;
// 機(jī)器標(biāo)識位數(shù)
private final static long workerIdBits = 5L;
// 數(shù)據(jù)中心標(biāo)識位數(shù)
private final static long datacenterIdBits = 5L;
// 機(jī)器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 數(shù)據(jù)中心ID最大值
private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 毫秒內(nèi)自增位
private final static long sequenceBits = 12L;
// 機(jī)器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 數(shù)據(jù)中心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);
/* 上次生產(chǎn)id時間戳 */
private static long lastTimestamp = -1L;
// 0埃元,并發(fā)控制
private long sequence = 0L;
private final long workerId;
// 數(shù)據(jù)標(biāo)識id部分
private final long datacenterId;
public IdWorker(){
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* @param workerId
* 工作機(jī)器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) {
// 當(dāng)前毫秒內(nèi)涝涤,則+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 當(dāng)前毫秒內(nèi)計數(shù)滿了,則等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移組合生成最終的ID岛杀,并返回ID
long 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>
* 數(shù)據(jù)標(biāo)識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 是美團(tuán)開源的分布式 ID 生成系統(tǒng)阔拳,最早期需求是各個業(yè)務(wù)線的訂單 ID 生成需求。在美團(tuán)早期类嗤,有的業(yè)務(wù)直接通過 DB 自增的方式生成 ID糊肠,有的業(yè)務(wù)通過 Redis 緩存來生成 ID,也有的業(yè)務(wù)直接用 UUID 這種方式來生成 ID遗锣。以上的方式各自有各自的問題货裹,因此美團(tuán)決定實現(xiàn)一套分布式 ID 生成服務(wù)來滿足需求目前 Leaf 覆蓋了美團(tuán)點評公司內(nèi)部金融、餐飲精偿、外賣弧圆、酒店旅游、貓眼電影等眾多業(yè)務(wù)線笔咽。在4C8G VM 基礎(chǔ)上搔预,通過公司 RPC 方式調(diào)用,QPS 壓測結(jié)果近 5w/s拓轻,TP999 1ms(TP=Top Percentile斯撮,Top 百分?jǐn)?shù),是一個統(tǒng)計學(xué)里的術(shù)語扶叉,與平均數(shù)勿锅、中位數(shù)都是一類帕膜。TP50、TP90 和 TP99 等指標(biāo)常用于系統(tǒng)性能監(jiān)控場景溢十,指高于 50%垮刹、90%、99% 等百分線的情況)张弛。
目前 LEAF 的使用有兩種不同的思路荒典,號段模式和 SNOWFLAKE 模式,你可以同時開啟兩種方式吞鸭,也可以指定開啟某種方式(默認(rèn)兩種方式為關(guān)閉狀態(tài))寺董。
我們從 GitHub 上 Clone LEAF 之后,它的配置文件在 leaf-server/src/main/resources/leaf.properties
中刻剥,各項配置的含義如下:
遮咖。
可以看到,如果使用號段模式造虏,需要數(shù)據(jù)庫支持御吞;如果使用 SNOWFLAKE 模式,需要 Zookeeper 支持漓藕。
3.3.1 號段模式
號段模式還是基于數(shù)據(jù)庫陶珠,但是思路有些變化,如下:
- 利用 proxy server 從數(shù)據(jù)庫中批量獲取 id享钞,每次獲取一個 segment (step 決定其大小) 號段的值揍诽,用完之后再去數(shù)據(jù)庫獲取新的號段,可以大大的減輕數(shù)據(jù)庫的壓力嫩与。
- 各個業(yè)務(wù)不同的發(fā)號需求用 biz_tag 字段來區(qū)分寝姿,每個 biz-tag 的 ID 獲取相互隔離,互不影響划滋。
- 如果有新的業(yè)務(wù)需要擴(kuò)區(qū) ID饵筑,只需要增加表記錄即可。
如果使用號段模式处坪,我們首先需要創(chuàng)建一張數(shù)據(jù)表根资,腳本如下:
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:業(yè)務(wù)標(biāo)記(不同業(yè)務(wù)可以有不同的號段序列)
- max_id:當(dāng)前號段下的最大 id
- step:每次取號段的步長
- description:描述信息
- update_time:更新時間
配置完成后,啟動項目同窘,訪問 http://localhost:8080/api/segment/get/leaf-segment-test
路徑(路徑最后面的 leaf-segment-test 是業(yè)務(wù)標(biāo)記)玄帕,即可拿到 ID。
可以通過如下地址訪問到號段模式的監(jiān)控頁面 http://localhost:8080/cache
想邦。
號段模式優(yōu)缺點:
優(yōu)點
- Leaf 服務(wù)可以很方便的線性擴(kuò)展裤纹,性能完全能夠支撐大多數(shù)業(yè)務(wù)場景。
- ID 號碼是趨勢遞增的 8byte 的 64 位數(shù)字丧没,滿足上述數(shù)據(jù)庫存儲的主鍵要求鹰椒。
- 容災(zāi)性高:Leaf 服務(wù)內(nèi)部有號段緩存锡移,即使 DB 宕機(jī),短時間內(nèi) Leaf 仍能正常對外提供服務(wù)漆际。
- 可以自定義 max_id 的大小淆珊,非常方便業(yè)務(wù)從原有的 ID 方式上遷移過來。
缺點
- ID 號碼不夠隨機(jī)奸汇,能夠泄露發(fā)號數(shù)量的信息施符,不太安全。
- DB 宕機(jī)會造成整個系統(tǒng)不可用擂找。
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/test
3.4 Redis 生成
這個主要是利用 Redis 的 incrby 來實現(xiàn),這個我覺得沒啥好說的且警。
3.5 Zookeeper 處理
zookeeper 也能做粉捻,但是比較麻煩,不推薦斑芜。
4. 小結(jié)
綜上肩刃,如果項目中恰好使用了 MyCat,那么可以使用 MyCat+Zookeeper杏头,否則建議使用 LEAF盈包,兩種模式皆可。