最近在做分布式任務調度系統(tǒng),遇到分布式id的問題,我們需要一個全局唯一的id滩租,但是服務又部署在多臺服務器上面探膊。因為之前沒有什么分布式系統(tǒng)的經驗杠愧,想當然的就是用了全局分布式鎖來保證id的唯一性。后來小組周會突想,經領導一點撥殴蹄,突然想起之前看過的一些分布式id解決方案(所以說知識需要不斷鞏固實踐以及復習,不然全忘光了- -)猾担。其實網上的分布式id的文章也很多了袭灯,但是為了讓自己理解更加深刻,決定專門寫一篇博客來記錄一下這些分布式id的解決方案绑嘹。
一稽荧、UUID
UUID 是 通用唯一識別碼(Universally Unique Identifier)的縮寫,在一個機器上生成的UUID工腋,可以保證在同一個時空下都是唯一的姨丈。UUID的生成主要和以太網卡地址畅卓、納秒級時間、芯片ID碼和許多可能的數(shù)字有關蟋恬,可以保證絕對唯一翁潘,完全滿足分布式id的唯一性原則。
java實現(xiàn)
String id = UUID.randomUUID().toString();
優(yōu)點
- 實現(xiàn)簡單歼争,不依賴第三方組件拜马,,java語言就自帶了uuid的實現(xiàn)
- 性能高,在單機上通過算法直接生成沐绒,速度非沉┟В快
- 全球唯一
缺點
- 生成的id是36個字符的字符串,占用空間太大乔遮,不適合用在數(shù)據庫主鍵上面扮超,也不適合作為索引字段
- 由于是字符串,不適用于需要id自增的場景蹋肮,也不能用來排序
二出刷、數(shù)據庫自增長序列
先給某個字段設置為自增長,然后每次插入一條記錄并獲取最新的id括尸。
mysql實現(xiàn)
先創(chuàng)建一張表 test
create table test(
id int auto_increment,
name varchar(8),
PRIMARY KEY (`id`)
)
每次要獲取id使用以下語句
begin;
REPLACE INTO test (NAME) VALUES ('a');
SELECT LAST_INSERT_ID();
commit;
優(yōu)點
- 實現(xiàn)簡單巷蚪,直接利用數(shù)據庫語句就可以了
- 全局id單調遞增,適合對id有特殊要求的業(yè)務
缺點
- 每次獲取id都要進行一次IO,性能較差
- 高并發(fā)場景下性能也比較差
- 重度依賴于DB濒翻,如果DB掛了屁柏,那么獲取id的功能就完全不能用了
- 每個業(yè)務都需要用到一張表,擴展非常不方便
三有送、數(shù)據庫分段獲取id
第二種方案每次都要去數(shù)據庫獲取id淌喻,這就意味著頻繁的IO操作造成了性能的瓶頸。所以就有人想出了優(yōu)化方案雀摘,也就是提前把一個號碼段從數(shù)據庫中取出來放到內存中裸删,之后拿到號碼段的進程再慢慢消耗這些號碼段,再去數(shù)據庫中獲取新的號碼段阵赠,這樣就大大的減少了IO次數(shù)涯塔。
mysql實現(xiàn)方案
create table test2(
id int auto_increment,
tag varchar(255) comment '用來表示業(yè)務',
max_id int comment '表示當前已取走的最大id',
step int comment '表示一次取多長的號段',
PRIMARY KEY (`id`)
);
之后用以下sql語句獲取號段
Begin;
UPDATE test2 SET max_id=max_id+step WHERE tag='test';
SELECT tag,max_id,step from test2 WHERE tag='test';
Commit;
這樣進程就拿到了step個號在進程中使用。
優(yōu)點
- 所有的業(yè)務用一張表就可以實現(xiàn)清蚀,擴展方便
- 滿足id遞增的需求
- 有一定的容災能力匕荸,假如DB掛掉了,緩存的號碼段還能用一段時間
- 可以自定義max_id,方便業(yè)務從其他地方遷移過來
缺點
- 在號段用完時會進行一次IO獲取新的號段枷邪,所以在性能曲線上會尖刺
- 還是依賴于DB榛搔,如果DB長時間掛了就無法工作了
- 號碼段不夠隨機,這也是所有id自增方案的風險。競爭對手很可能通過id來推斷一天的數(shù)據量践惑。
- 實現(xiàn)稍微麻煩寫腹泌,現(xiàn)實使用中還要考慮持久化號段使用情況信息,也就是記錄下當前使用到了哪里尔觉,以便重啟后可以繼續(xù)使用該號段凉袱,避免號碼的浪費
優(yōu)化方案
- 針對缺點1,我們可以使用雙號段緩存穷娱,當一個號段用完馬上可以使用第二個绑蔫,同時開啟一個異步線程再去獲取一個號段,保證系統(tǒng)同時維護兩個號段
- 針對缺點2泵额,可以做一些mysql主備方案來應對
四、snowflake
snowflake是Twitter開源的分布式ID生成算法携添,結果是一個long型的ID嫁盲。其核心思想是:使用41bit作為毫秒數(shù),10bit作為機器的ID(5個bit是數(shù)據中心烈掠,5個bit的機器ID)羞秤,12bit作為毫秒內的流水號(意味著每個節(jié)點在每毫秒可以產生 4096 個 ID),最后還有一個符號位左敌,永遠是0瘾蛋。
java實現(xiàn)
/**
* @author yangjb
* @since 2018-07-26 20:22
* <p>
* snowflake分布式id生成器
* 最高位在long中表示符號位,id一般都是正數(shù)矫限,所以第一位默認都用0表示
* 接下來的41位表示時間戳,注意哺哼,這里的時間戳不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截)
* 得到的值)叼风,這里的的開始時間截取董,一般是我們的id生成器開始使用的時間,由我們程序來指定的(如下下面程序類的TWEPOCH屬性)无宿。41位的時間截茵汰,可以使用69年
* 10位的數(shù)據機器位,可以部署在1024個節(jié)點孽鸡,包括5位datacenterId和5位workerId
* 12位序列號蹂午,在同一個毫秒同一個機器內,可以產生4096個ID序號使用
* 1+41+10+12 = 64彬碱,也就是一個long類型的長度豆胸。當然,不一定要嚴格使用這樣的分配來計算id堡妒,比如比如機器較少配乱,可以把機器的10位切割出幾位出來給序列號使用等等
*/
public class IdGenerator {
//id生成器的開始使用時間
private final static long TWEPOCH = 1288834974657L;
// 機器標識位數(shù)
private final static long WORKER_ID_BITS = 5L;
// 數(shù)據中心標識位數(shù)
private final static long DATA_CENTER_ID_BITS = 5L;
// 機器ID最大值 31
private final static long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
// 數(shù)據中心ID最大值 31
private final static long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
// 毫秒內自增位
private final static long SEQUENCE_BITS = 12L;
// 機器ID偏左移12位
private final static long WORKER_ID_SHIFT = SEQUENCE_BITS;
private final static long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
// 時間毫秒左移22位
private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
private final static long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);
private long lastTimestamp = -1L;
private long sequence = 0L;
private final long workerId;
private final long dataCenterId;
IdGenerator(long workerId, long dataCenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("%s must range from %d to %d", workerId, 0,
MAX_WORKER_ID));
}
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("%s must range from %d to %d", dataCenterId, 0,
MAX_DATA_CENTER_ID));
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
synchronized long nextValue() {
long timestamp = time();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards, refuse to generate id for "
+ (lastTimestamp - timestamp) + " milliseconds");
}
if (lastTimestamp == timestamp) {
// 當前毫秒內,則+1
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
// 當前毫秒內計數(shù)滿了,則等待下一秒
timestamp = untilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
// ID偏移組合生成最終的ID搬泥,并返回ID
return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence;
}
private long untilNextMillis(final long lastTimestamp) {
long timestamp = this.time();
while (timestamp <= lastTimestamp) {
timestamp = this.time();
}
return timestamp;
}
private long time() {
return System.currentTimeMillis();
}
public static void main(String[] args) throws ParseException {
long id = new IdGenerator(1, 1).nextValue();
System.out.println(Long.toBinaryString(id));
}
}
優(yōu)點
- id是遞增的桑寨,且數(shù)值隨機,安全性較高
- 性能高忿檩,直接在單機上通過算法計算出來,不用跨網絡跨IO
- 不用依賴第三方組件尉尾,健壯性好
- 根據業(yè)務特性分配bit位,非常靈活
缺點
- 強依賴于系統(tǒng)時鐘燥透,如果出現(xiàn)機器時間回撥沙咏,可能造成id重復的問題
優(yōu)化 和 使用注意點
使用snowflake最需要防止由于系統(tǒng)時鐘回撥導致的id重復問題。上面的java實現(xiàn)方案中有做一些簡單的時間戳檢查來防止大幅度的系統(tǒng)時鐘回撥班套,但是也沒有徹底的解決時鐘回撥問題肢藐。由于lastTimestamp變量是存儲在內存的,一旦程序關閉吱韭,就失去了這個值栗弟,所以最好的方案還是需要定時的把lastTimestamp持久化起來莫绣。可以考慮利用zk的節(jié)點時間來記錄這個lastTimestamp。
另外提一句诈皿,MongoDB的objectId也是用類snowflake的方案來生成id的范删。objectId由12個字節(jié)組成艾猜,4個字節(jié)表示秒級時間戳尤揣,3個字節(jié)表示機器id,2個字節(jié)表示pid姨俩,最后3個字節(jié)表示自增序列蘸拔。也就是表示,在同一秒同一臺機器的同一個進程內哼勇,可以產生4096個id都伪。
objectId最終標識成一個24長度的十六進制字符。
五积担、redis incr命令實現(xiàn)id自增
使用redis的incr命令可以很簡單的實現(xiàn)id的自增陨晶,并保證全局id唯一。
redis語句
每次要獲取id帝璧,只要執(zhí)行以下語句
incr test
優(yōu)點
- 實現(xiàn)簡單先誉,直接執(zhí)行redis的一個命令就可以獲取到id
- 性能較高,redis是內存型數(shù)據庫的烁,獲取id時不需要經過io褐耳,速度很快
- 獲取的id是遞增的,可以滿足對id有要求的場景
缺點
- 重度依賴于redis渴庆,假如redis掛了铃芦,系統(tǒng)也就不能正常運行了
- 同樣由于id是自增的雅镊,有暴露自己業(yè)務數(shù)據量的風險
總結
可以說除了第二種方案,其他各個分布式id解決方案都各自有各自的使用場景刃滓,也不能一概而論的說哪種方案是最好的仁烹。沒有最好的方案,只有最適合的方案咧虎。當我們需要用到分布式id的時候卓缰,應該要先考慮好使用場景,之后分析各個分布式id解決方案各自的優(yōu)缺點砰诵,綜合考慮后再決定使用哪種方案征唬。
本人的CSDN博客地址:
https://blog.csdn.net/u013332124/article/details/81234125
美團的leaf解決方案:
https://tech.meituan.com/MT_Leaf.html?utm_source=tool.lu