分布式id解決方案

最近在做分布式任務調度系統(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)點
  1. 實現(xiàn)簡單歼争,不依賴第三方組件拜马,,java語言就自帶了uuid的實現(xiàn)
  2. 性能高,在單機上通過算法直接生成沐绒,速度非沉┟В快
  3. 全球唯一
缺點
  1. 生成的id是36個字符的字符串,占用空間太大乔遮,不適合用在數(shù)據庫主鍵上面扮超,也不適合作為索引字段
  2. 由于是字符串,不適用于需要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)點
  1. 實現(xiàn)簡單巷蚪,直接利用數(shù)據庫語句就可以了
  2. 全局id單調遞增,適合對id有特殊要求的業(yè)務
缺點
  1. 每次獲取id都要進行一次IO,性能較差
  2. 高并發(fā)場景下性能也比較差
  3. 重度依賴于DB濒翻,如果DB掛了屁柏,那么獲取id的功能就完全不能用了
  4. 每個業(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)點

  1. 所有的業(yè)務用一張表就可以實現(xiàn)清蚀,擴展方便
  2. 滿足id遞增的需求
  3. 有一定的容災能力匕荸,假如DB掛掉了,緩存的號碼段還能用一段時間
  4. 可以自定義max_id,方便業(yè)務從其他地方遷移過來
缺點
  1. 在號段用完時會進行一次IO獲取新的號段枷邪,所以在性能曲線上會尖刺
  2. 還是依賴于DB榛搔,如果DB長時間掛了就無法工作了
  3. 號碼段不夠隨機,這也是所有id自增方案的風險。競爭對手很可能通過id來推斷一天的數(shù)據量践惑。
  4. 實現(xiàn)稍微麻煩寫腹泌,現(xiàn)實使用中還要考慮持久化號段使用情況信息,也就是記錄下當前使用到了哪里尔觉,以便重啟后可以繼續(xù)使用該號段凉袱,避免號碼的浪費
優(yōu)化方案
  1. 針對缺點1,我們可以使用雙號段緩存穷娱,當一個號段用完馬上可以使用第二個绑蔫,同時開啟一個異步線程再去獲取一個號段,保證系統(tǒng)同時維護兩個號段
  2. 針對缺點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)點
  1. id是遞增的桑寨,且數(shù)值隨機,安全性較高
  2. 性能高忿檩,直接在單機上通過算法計算出來,不用跨網絡跨IO
  3. 不用依賴第三方組件尉尾,健壯性好
  4. 根據業(yè)務特性分配bit位,非常靈活
缺點
  1. 強依賴于系統(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)點
  1. 實現(xiàn)簡單先誉,直接執(zhí)行redis的一個命令就可以獲取到id
  2. 性能較高,redis是內存型數(shù)據庫的烁,獲取id時不需要經過io褐耳,速度很快
  3. 獲取的id是遞增的,可以滿足對id有要求的場景
缺點
  1. 重度依賴于redis渴庆,假如redis掛了铃芦,系統(tǒng)也就不能正常運行了
  2. 同樣由于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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市茁彭,隨后出現(xiàn)的幾起案子总寒,更是在濱河造成了極大的恐慌,老刑警劉巖尉间,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偿乖,死亡現(xiàn)場離奇詭異,居然都是意外死亡哲嘲,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門媳禁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來眠副,“玉大人,你說我怎么就攤上這事竣稽〈雅拢” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵毫别,是天一觀的道長娃弓。 經常有香客問我,道長岛宦,這世上最難降的妖魔是什么台丛? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮砾肺,結果婚禮上挽霉,老公的妹妹穿的比我還像新娘。我一直安慰自己变汪,他們只是感情好侠坎,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著裙盾,像睡著了一般实胸。 火紅的嫁衣襯著肌膚如雪他嫡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天庐完,我揣著相機與錄音钢属,去河邊找鬼。 笑死假褪,一個胖子當著我的面吹牛署咽,可吹牛的內容都是我干的。 我是一名探鬼主播生音,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼宁否,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了缀遍?” 一聲冷哼從身側響起慕匠,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎域醇,沒想到半個月后台谊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡譬挚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年锅铅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片减宣。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡盐须,死狀恐怖,靈堂內的尸體忽然破棺而出漆腌,到底是詐尸還是另有隱情贼邓,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布闷尿,位于F島的核電站塑径,受9級特大地震影響,放射性物質發(fā)生泄漏填具。R本人自食惡果不足惜统舀,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望灌旧。 院中可真熱鬧绑咱,春花似錦、人聲如沸枢泰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽衡蚂。三九已至窿克,卻和暖如春骏庸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背年叮。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工具被, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人只损。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓一姿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親跃惫。 傳聞我的和親對象是個殘疾皇子叮叹,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344