4. sharding-jdbc源碼之分布式ID

阿飛Javaer架曹,轉(zhuǎn)載請注明原創(chuàng)出處灯抛,謝謝!

實現(xiàn)動機

傳統(tǒng)數(shù)據(jù)庫軟件開發(fā)中音瓷,主鍵自動生成技術(shù)是基本需求。而各大數(shù)據(jù)庫對于該需求也提供了相應(yīng)的支持夹抗,比如MySQL的自增鍵绳慎。 對于MySQL而言,分庫分表之后漠烧,不同表生成全局唯一的Id是非常棘手的問題杏愤。因為同一個邏輯表內(nèi)的不同實際表之間的自增鍵是無法互相感知的, 這樣會造成重復(fù)Id的生成已脓。我們當然可以通過約束表生成鍵的規(guī)則來達到數(shù)據(jù)的不重復(fù)珊楼,但是這需要引入額外的運維力量來解決重復(fù)性問題,并使框架缺乏擴展性度液。

目前有許多第三方解決方案可以完美解決這個問題厕宗,比如UUID等依靠特定算法自生成不重復(fù)鍵(由于InnoDB采用的B+Tree索引特性,UUID生成的主鍵插入性能較差)堕担,或者通過引入Id生成服務(wù)等已慢。 但也正因為這種多樣性導(dǎo)致了Sharding-JDBC如果強依賴于任何一種方案就會限制其自身的發(fā)展。

基于以上的原因霹购,最終采用了以JDBC接口來實現(xiàn)對于生成Id的訪問佑惠,而將底層具體的Id生成實現(xiàn)分離出來。

摘自sharding-jdbc分布式主鍵

sharding-jdbc的分布式ID采用twitter開源的snowflake算法齐疙,不需要依賴任何第三方組件膜楷,這樣其擴展性和維護性得到最大的簡化;但是snowflake算法的缺陷(強依賴時間贞奋,如果時鐘回撥赌厅,就會生成重復(fù)的ID),sharding-jdbc沒有給出解決方案忆矛,如果用戶想要強化察蹲,需要自行擴展请垛;

擴展:美團的分布式ID生成系統(tǒng)也是基于snowflake算法,并且解決了時鐘回撥的問題洽议,讀取有興趣請閱讀Leaf——美團點評分布式ID生成系統(tǒng)

分布式ID簡介

github上對分布式ID這個特性的描述是:Distributed Unique Time-Sequence Generation宗收,兩個重要特性是:分布式唯一時間序;基于Twitter Snowflake算法實現(xiàn)亚兄,長度為64bit混稽;64bit組成如下:

  • 1bit sign bit.
  • 41bits timestamp offset from 2016.11.01(Sharding-JDBC distributed primary key published data) to now.
  • 10bits worker process id.
  • 12bits auto increment offset in one mills.

分布式ID源碼分析

核心源碼在sharding-jdbc-core模塊中的com.dangdang.ddframe.rdb.sharding.keygen.DefaultKeyGenerator.java中:

public final class DefaultKeyGenerator implements KeyGenerator {
    
    public static final long EPOCH;
    
    // 自增長序列的長度(單位是位時的長度)
    private static final long SEQUENCE_BITS = 12L;
    
    // workerId的長度(單位是位時的長度)
    private static final long WORKER_ID_BITS = 10L;
    
    private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
    
    private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
    
    private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
    
    // 位運算計算workerId的最大值(workerId占10位,那么1向左移10位就是workerId的最大值)
    private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS;
    
    @Setter
    private static TimeService timeService = new TimeService();
    
    private static long workerId;
    
    // EPOCH就是起始時間审胚,從2016-11-01 00:00:00開始的毫秒數(shù)
    static {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2016, Calendar.NOVEMBER, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        EPOCH = calendar.getTimeInMillis();
    }
    
    private long sequence;
    
    private long lastTime;
    
    /**
     * 得到分布式唯一ID需要先設(shè)置workerId匈勋,workId的值范圍[0, 1024)
     * @param workerId work process id
     */
    public static void setWorkerId(final long workerId) {
        // google-guava提供的入?yún)z查方法:workerId只能在0~WORKER_ID_MAX_VALUE之間;
        Preconditions.checkArgument(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE);
        DefaultKeyGenerator.workerId = workerId;
    }
    
    /**
     * 調(diào)用該方法膳叨,得到分布式唯一ID
     * @return key type is @{@link Long}.
     */
    @Override
    public synchronized Number generateKey() {
        long currentMillis = timeService.getCurrentMillis();
        // 每次取分布式唯一ID的時間不能少于上一次取時的時間
        Preconditions.checkState(lastTime <= currentMillis, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, currentMillis);
        // 如果同一毫秒范圍內(nèi)洽洁,那么自增,否則從0開始
        if (lastTime == currentMillis) {
            // 如果自增后的sequence值超過4096菲嘴,那么等待直到下一個毫秒
            if (0L == (sequence = ++sequence & SEQUENCE_MASK)) {
                currentMillis = waitUntilNextTime(currentMillis);
            }
        } else {
            sequence = 0;
        }
        // 更新lastTime的值饿自,即最后一次獲取分布式唯一ID的時間
        lastTime = currentMillis;
        // 從這里可知分布式唯一ID的組成部分;
        return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
    }
    
    // 獲取下一毫秒的方法:死循環(huán)獲取當前毫秒與lastTime比較龄坪,直到大于lastTime的值昭雌;
    private long waitUntilNextTime(final long lastTime) {
        long time = timeService.getCurrentMillis();
        while (time <= lastTime) {
            time = timeService.getCurrentMillis();
        }
        return time;
    }
}

獲取workerId的三種方式

sharding-jdbc的sharding-jdbc-plugin模塊中,提供了三種方式獲取workerId的方式健田,并提供接口獲取分布式唯一ID的方法--generateKey()烛卧,接下來對各種方式如何生成workerId進行分析;

HostNameKeyGenerator

  1. 根據(jù)hostname獲取妓局,源碼如下(HostNameKeyGenerator.java):
/**
 * 根據(jù)機器名最后的數(shù)字編號獲取工作進程Id.如果線上機器命名有統(tǒng)一規(guī)范,建議使用此種方式.
 * 例如機器的HostName為:dangdang-db-sharding-dev-01(公司名-部門名-服務(wù)名-環(huán)境名-編號)
 * ,會截取HostName最后的編號01作為workerId.
 *
 * @author DonneyYoung
 **/
 static void initWorkerId() {
    InetAddress address;
    Long workerId;
    try {
        address = InetAddress.getLocalHost();
    } catch (final UnknownHostException e) {
        throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");
    }
    // 先得到服務(wù)器的hostname总放,例如JTCRTVDRA44,linux上可通過命令"cat /proc/sys/kernel/hostname"查看好爬;
    String hostName = address.getHostName();
    try {
        // 計算workerId的方式:
        // 第一步hostName.replaceAll("\\d+$", "")间聊,即去掉hostname后純數(shù)字部分,例如JTCRTVDRA44去掉后就是JTCRTVDRA
        // 第二步hostName.replace(第一步的結(jié)果, "")抵拘,即將原h(huán)ostname的非數(shù)字部分去掉哎榴,得到純數(shù)字部分,就是workerId
        workerId = Long.valueOf(hostName.replace(hostName.replaceAll("\\d+$", ""), ""));
    } catch (final NumberFormatException e) {
        throw new IllegalArgumentException(String.format("Wrong hostname:%s, hostname must be end with number!", hostName));
    }
    DefaultKeyGenerator.setWorkerId(workerId);
}

IPKeyGenerator

  1. 根據(jù)IP獲取僵蛛,源碼如下(IPKeyGenerator.java):
/**
 * 根據(jù)機器IP獲取工作進程Id,如果線上機器的IP二進制表示的最后10位不重復(fù),建議使用此種方式
 * ,列如機器的IP為192.168.1.108,二進制表示:11000000 10101000 00000001 01101100
 * ,截取最后10位 01 01101100,轉(zhuǎn)為十進制364,設(shè)置workerId為364.
 */
static void initWorkerId() {
    InetAddress address;
    try {
        // 首先得到IP地址尚蝌,例如192.168.1.108
        address = InetAddress.getLocalHost();
    } catch (final UnknownHostException e) {
        throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");
    }
    // IP地址byte[]數(shù)組形式,這個byte數(shù)組的長度是4充尉,數(shù)組0~3下標對應(yīng)的值分別是192飘言,168,1驼侠,108
    byte[] ipAddressByteArray = address.getAddress();
    // 由這里計算workerId源碼可知姿鸿,workId由兩部分組成:
    // 第一部分(ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE:ipAddressByteArray[ipAddressByteArray.length - 2]即取byte[]倒數(shù)第二個值谆吴,即1,然后&0B11苛预,即只取最后2位(IP段倒數(shù)第二個段取2位句狼,IP段最后一位取全部8位,總計10位)热某,然后左移Byte.SIZE腻菇,即左移8位(因為這一部分取得的是IP段中倒數(shù)第二個段的值);
    // 第二部分(ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF):ipAddressByteArray[ipAddressByteArray.length - 1]即取byte[]最后一位昔馋,即108筹吐,然后&0xFF,即通過位運算將byte轉(zhuǎn)為int秘遏;
    // 最后將第一部分得到的值加上第二部分得到的值就是最終的workId
    DefaultKeyGenerator.setWorkerId((long) (((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF)));
}

IPSectionKeyGenerator

  1. 根據(jù)IP段獲取丘薛,源碼如下(IPSectionKeyGenerator.java):
/**
 * 瀏覽 {@link IPKeyGenerator} workerId生成的規(guī)則后,感覺對服務(wù)器IP后10位(特別是IPV6)數(shù)值比較約束.
 * 
 * <p>
 * 有以下優(yōu)化思路:
 * 因為workerId最大限制是2^10邦危,我們生成的workerId只要滿足小于最大workerId即可榔袋。
 * 1.針對IPV4:
 * ....IP最大 255.255.255.255。而(255+255+255+255) < 1024铡俐。
 * ....因此采用IP段數(shù)值相加即可生成唯一的workerId,不受IP位限制妥粟。
 * 2.針對IPV6:
 * ....IP最大ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
 * ....為了保證相加生成出的workerId < 1024,思路是將每個bit位的后6位相加审丘。這樣在一定程度上也可以滿足workerId不重復(fù)的問題。
 * </p>
 * 使用這種IP生成workerId的方法,必須保證IP段相加不能重復(fù)
 *
 * @author DogFc
 */
static void initWorkerId() {
    InetAddress address;
    try {
        address = InetAddress.getLocalHost();
    } catch (final UnknownHostException e) {
        throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");
    }
    // 得到IP地址的byte[]形式值
    byte[] ipAddressByteArray = address.getAddress();
    long workerId = 0L;
    //如果是IPV4勾给,計算方式是遍歷byte[]滩报,然后把每個IP段數(shù)值相加得到的結(jié)果就是workerId
    if (ipAddressByteArray.length == 4) {
        for (byte byteNum : ipAddressByteArray) {
            workerId += byteNum & 0xFF;
        }
        //如果是IPV6,計算方式是遍歷byte[]播急,然后把每個IP段后6位(& 0B111111 就是得到后6位)數(shù)值相加得到的結(jié)果就是workerId
    } else if (ipAddressByteArray.length == 16) {
        for (byte byteNum : ipAddressByteArray) {
            workerId += byteNum & 0B111111;
        }
    } else {
        throw new IllegalStateException("Bad LocalHost InetAddress, please check your network!");
    }
    DefaultKeyGenerator.setWorkerId(workerId);
}

建議

大道至簡脓钾,強烈推薦HostNameKeyGenerator方式獲取workerId,只需服務(wù)器按照標準統(tǒng)一配置好hostname即可桩警;這種方案有點類似spring-boot:約定至上可训;并能夠讓架構(gòu)最簡化,不依賴任何第三方組件捶枢;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末握截,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子烂叔,更是在濱河造成了極大的恐慌谨胞,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒜鸡,死亡現(xiàn)場離奇詭異胯努,居然都是意外死亡牢裳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門叶沛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒲讯,“玉大人,你說我怎么就攤上這事恬汁×娲唬” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵氓侧,是天一觀的道長脊另。 經(jīng)常有香客問我,道長约巷,這世上最難降的妖魔是什么偎痛? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮独郎,結(jié)果婚禮上踩麦,老公的妹妹穿的比我還像新娘。我一直安慰自己氓癌,他們只是感情好谓谦,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贪婉,像睡著了一般反粥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疲迂,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天才顿,我揣著相機與錄音,去河邊找鬼尤蒿。 笑死郑气,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的腰池。 我是一名探鬼主播尾组,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼示弓!你這毒婦竟也來了演怎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤避乏,失蹤者是張志新(化名)和其女友劉穎爷耀,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拍皮,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡歹叮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年跑杭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咆耿。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡德谅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出萨螺,到底是詐尸還是另有隱情窄做,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布慰技,位于F島的核電站椭盏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏吻商。R本人自食惡果不足惜掏颊,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望艾帐。 院中可真熱鬧乌叶,春花似錦、人聲如沸柒爸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捎稚。三九已至乐横,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阳藻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工谈撒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留腥泥,地道東北人。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓啃匿,卻偏偏與公主長得像蛔外,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子溯乒,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 目錄;(一) 拆分實施策略和示例演示(二) 全局主鍵生成策略(三) 關(guān)于使用框架還是自主開發(fā)以及sharding實...
    linking12閱讀 10,463評論 1 52
  • Sharding的基本思想其實就是采用分治的思想夹厌,要把一個數(shù)據(jù)庫切分成多個部分放到不同的數(shù)據(jù)庫(server)上,...
    jiangmo閱讀 9,396評論 0 7
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法裆悄,類相關(guān)的語法矛纹,內(nèi)部類的語法,繼承相關(guān)的語法光稼,異常的語法或南,線程的語...
    子非魚_t_閱讀 31,631評論 18 399
  • 今日朗誦道德經(jīng)第57章“以正治國”心得: 當很多問題的出現(xiàn)時孩等,不是因為客觀事物,而是因為我們自己采够,所以出現(xiàn)問題的時...
    周志英閱讀 121評論 2 1
  • 文/清風徐來 季風里的故鄉(xiāng) 春夏秋冬 有不同的雨歌唱 輕重緩急都成樂章 聽雨呢喃滴答 在茅檐...
    清風徐徐來也閱讀 603評論 0 0