1. 程式人生 > >【死磕Sharding-jdbc】---分散式ID

【死磕Sharding-jdbc】---分散式ID

實現動機

傳統資料庫軟體開發中,主鍵自動生成技術是基本需求。而各大資料庫對於該需求也提供了相應的支援,比如MySQL的自增鍵。 對於MySQL而言,分庫分表之後,不同表生成全域性唯一的Id是非常棘手的問題。因為同一個邏輯表內的不同實際表之間的自增鍵是無法互相感知的, 這樣會造成重複Id的生成。我們當然可以通過約束表生成鍵的規則來達到資料的不重複,但是這需要引入額外的運維力量來解決重複性問題,並使框架缺乏擴充套件性。

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

基於以上的原因,最終採用了以JDBC介面來實現對於生成Id的訪問,而將底層具體的Id生成實現分離出來。

sharding-jdbc的分散式ID採用twitter開源的snowflake演算法,不需要依賴任何第三方元件,這樣其擴充套件性和維護性得到最大的簡化;但是snowflake演算法的缺陷(強依賴時間,如果時鐘回撥,就會生成重複的ID),sharding-jdbc沒有給出解決方案,如果使用者想要強化,需要自行擴充套件;

擴充套件:美團的分散式ID生成系統也是基於snowflake演算法,並且解決了時鐘回撥的問題,讀取有興趣請閱讀Leaf——美團點評分散式ID生成系統

分散式ID簡介

github上對分散式ID這個特性的描述是:Distributed Unique Time-Sequence Generation,兩個重要特性是:分散式唯一時間序;基於Twitter Snowflake演算法實現,長度為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開始的毫秒數
    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需要先設定workerId,workId的值範圍[0, 1024)
     * @param workerId work process id
     */
    public static void setWorkerId(final long workerId) {
        // google-guava提供的入參檢查方法:workerId只能在0~WORKER_ID_MAX_VALUE之間;
        Preconditions.checkArgument(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE);
        DefaultKeyGenerator.workerId = workerId;
    }

    /**
     * 呼叫該方法,得到分散式唯一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);
        // 如果同一毫秒範圍內,那麼自增,否則從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;
    }

    // 獲取下一毫秒的方法:死迴圈獲取當前毫秒與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. 根據hostname獲取,原始碼如下(HostNameKeyGenerator.java):
/**
 * 根據機器名最後的數字編號獲取工作程序Id.如果線上機器命名有統一規範,建議使用此種方式.
 * 例如機器的HostName為:dangdang-db-sharding-dev-01(公司名-部門名-服務名-環境名-編號)
 * ,會擷取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!");
    }
    // 先得到伺服器的hostname,例如JTCRTVDRA44,linux上可通過命令"cat /proc/sys/kernel/hostname"檢視;
    String hostName = address.getHostName();
    try {
        // 計算workerId的方式:
        // 第一步hostName.replaceAll("\\d+$", ""),即去掉hostname後純數字部分,例如JTCRTVDRA44去掉後就是JTCRTVDRA
        // 第二步hostName.replace(第一步的結果, ""),即將原hostname的非數字部分去掉,得到純數字部分,就是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. 根據IP獲取,原始碼如下(IPKeyGenerator.java):
/**
 * 根據機器IP獲取工作程序Id,如果線上機器的IP二進位制表示的最後10位不重複,建議使用此種方式
 * ,列如機器的IP為192.168.1.108,二進位制表示:11000000 10101000 00000001 01101100
 * ,擷取最後10位 01 01101100,轉為十進位制364,設定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[]陣列形式,這個byte陣列的長度是4,陣列0~3下標對應的值分別是192,168,1,108
    byte[] ipAddressByteArray = address.getAddress();
    // 由這裡計算workerId原始碼可知,workId由兩部分組成:
    // 第一部分(ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE:ipAddressByteArray[ipAddressByteArray.length - 2]即取byte[]倒數第二個值,即1,然後&0B11,即只取最後2位(IP段倒數第二個段取2位,IP段最後一位取全部8位,總計10位),然後左移Byte.SIZE,即左移8位(因為這一部分取得的是IP段中倒數第二個段的值);
    // 第二部分(ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF):ipAddressByteArray[ipAddressByteArray.length - 1]即取byte[]最後一位,即108,然後&0xFF,即通過位運算將byte轉為int;
    // 最後將第一部分得到的值加上第二部分得到的值就是最終的workId
    DefaultKeyGenerator.setWorkerId((long) (((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF)));
}

IPSectionKeyGenerator

  1. 根據IP段獲取,原始碼如下(IPSectionKeyGenerator.java):
/**
 * 瀏覽 {@link IPKeyGenerator} workerId生成的規則後,感覺對伺服器IP後10位(特別是IPV6)數值比較約束.
 * 
 * <p>
 * 有以下優化思路:
 * 因為workerId最大限制是2^10,我們生成的workerId只要滿足小於最大workerId即可。
 * 1.針對IPV4:
 * ....IP最大 255.255.255.255。而(255+255+255+255) < 1024。
 * ....因此採用IP段數值相加即可生成唯一的workerId,不受IP位限制。
 * 2.針對IPV6:
 * ....IP最大ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
 * ....為了保證相加生成出的workerId < 1024,思路是將每個bit位的後6位相加。這樣在一定程度上也可以滿足workerId不重複的問題。
 * </p>
 * 使用這種IP生成workerId的方法,必須保證IP段相加不能重複
 *
 * @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段數值相加得到的結果就是workerId
    if (ipAddressByteArray.length == 4) {
        for (byte byteNum : ipAddressByteArray) {
            workerId += byteNum & 0xFF;
        }
        //如果是IPV6,計算方式是遍歷byte[],然後把每個IP段後6位(& 0B111111 就是得到後6位)數值相加得到的結果就是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,只需伺服器按照標準統一配置好hostname即可;這種方案有點類似spring-boot:約定至上;並能夠讓架構最簡化,不依賴任何第三方元件;

歡迎掃一掃我的公眾號關注 — 及時得到部落格訂閱哦!

這裡寫圖片描述

相關推薦

Sharding-jdbc---分散式ID

實現動機 傳統資料庫軟體開發中,主鍵自動生成技術是基本需求。而各大資料庫對於該需求也提供了相應的支援,比如MySQL的自增鍵。 對於MySQL而言,分庫分表之後,不同表生成全域性唯一的Id是非常棘手的問題。因為同一個邏輯表內的不同實際表之間的自增鍵

Sharding-jdbc---準備工作

接下來對sharding-jdbc原始碼的分析基於tag為1.5.4.1原始碼,根據sharding-jdbc Features深入學習sharding-jdbc的幾個主要特性是如何實現的; 概況 sharding-jdbc原始碼主要有以下幾個模組:

springboot2.0springboot基於web開發

宣告,使用 maven3.5.4,springboot2.0,JDK8 ,idea2018.2 模組目錄結構: main 主方法: @SpringBootApplication public class WebApplication { public static voi

springboot2.0@restcontroller與 @controller的 區別;

@restcontroller *原始碼如下:其包含@Controller 、@ResponseBody * @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller

Java併發-----Java記憶體模型之happens-before

在上篇部落格(【死磕Java併發】—–深入分析volatile的實現原理)LZ提到過由於存線上程本地記憶體和主記憶體的原因,再加上重排序,會導致多執行緒環境下存在可見性的問題。那麼我們正確使用同步、鎖的情況下,執行緒A修改了變數a何時對執行緒B可見? 我們無法就所有場景來規

銘毅天下(Elasticsearch知識星球http://t.cn/RmwM3N9;Elasticsearch微信公眾號銘毅天下;ElasticsearchQQ群626036393)

銘毅天下 【死磕Elasticsearch知識星球】http://t.cn/RmwM3N9;【Elasticsearch微信公眾號】銘毅天下;【死磕ElasticsearchQQ群】626036393...

Java併發- 深入分析volatile的實現原理

通過前面一章我們瞭解了synchronized是一個重量級的鎖,雖然JVM對它做了很多優化,而下面介紹的volatile則是輕量級的synchronized。如果一個變數使用volatile,則它比使用synchronized的成本更加低,因為它不會引起執行緒上下文的切換和排程。Java語言

Java併發-----J.U.C之AQS:阻塞和喚醒執行緒

此篇部落格所有原始碼均來自JDK 1.8 線上程獲取同步狀態時如果獲取失敗,則加入CLH同步佇列,通過通過自旋的方式不斷獲取同步狀態,但是在自旋的過程中則需要判斷當前執行緒是否需要阻塞,其主要方法在acquireQueued(): if (sho

Java併發-----Java記憶體模型之分析volatile

volatile可見性;對一個volatile的讀,總可以看到對這個變數最終的寫; volatile原子性;volatile對單個讀/寫具有原子性(32位Long、Double),但是複合操作除外,例如i++; JVM底層採用“記憶體屏障”來實現volat

Java併發-----J.U.C之阻塞佇列:ArrayBlockingQueue

ArrayBlockingQueue,一個由陣列實現的有界阻塞佇列。該佇列採用FIFO的原則對元素進行排序新增的。 ArrayBlockingQueue為有界且固定,其大小在構造時由建構函式來決定,確認之後就不能再改變了。ArrayBlockingQueu

Java併發—– J.U.C之併發工具類:Semaphore

此篇部落格所有原始碼均來自JDK 1.8訊號量Semaphore是一個控制訪問多個共享資源的計數

Java併發--Java記憶體模型之happens-before

在上篇部落格(【死磕Java併發】—–深入分析volatile的實現原理)LZ提到過由於存線上程本地記憶體和主記憶體的原因,再加上重排序,會導致多執行緒環境下存在可見性的問題。那麼我們正確使用同步、鎖的情況下,執行緒A修改了變數a何時對執行緒B可見?我們無法就所有場景來規定某

Java併發-----Java記憶體模型之總結

經過四篇部落格闡述,我相信各位對Java記憶體模型有了最基本認識了,下面LZ就做一個比較簡單的總結。 總結 JMM規定了執行緒的工作記憶體和主記憶體的互動關係,以及執行緒之間的可見性和程式的執行順序。一方面,要為程式設計師提供足夠強的記憶體可見性保證;另

Java併發--深入分析volatile的實現原理

通過前面一章我們瞭解了synchronized是一個重量級的鎖,雖然JVM對它做了很多優化,而下面介紹的volatile則是輕量級的synchronized。如果一個變數使用volatile,則它比使用synchronized的成本更加低,因為它不會引起執行緒上下文的切換和排

Java併發—– J.U.C之AQS:同步狀態的獲取與釋放

此篇部落格所有原始碼均來自JDK 1.8在前面提到過,AQS是構建Java同步元件的基礎,我們期

Java併發-----J.U.C之併發工具類:Exchanger

此篇部落格所有原始碼均來自JDK 1.8 前面三篇部落格分別介紹了CyclicBarrier、CountDownLatch、Semaphore,現在介紹併發工具類中的最後一個Exchange。Exchange是最簡單的也是最複雜的,簡單在於API非常簡

Java併發-----J.U.C之Condition

此篇部落格所有原始碼均來自JDK 1.8 在沒有Lock之前,我們使用synchronized來控制同步,配合Object的wait()、notify()系列方法可以實現等待/通知模式。在Java SE5後,Java提供了Lock介面,相對於Synch

Java併發-----J.U.C之重入鎖:ReentrantLock

此篇部落格所有原始碼均來自JDK 1.8 ReentrantLock,可重入鎖,是一種遞迴無阻塞的同步機制。它可以等同於synchronized的使用,但是ReentrantLock提供了比synchronized更強大、靈活的鎖機制,可以減少死鎖發生

Java併發-----J.U.C之阻塞佇列:DelayQueue

DelayQueue是一個支援延時獲取元素的無界阻塞佇列。裡面的元素全部都是“可延期”的元素,列頭的元素是最先“到期”的元素,如果佇列裡面沒有元素到期,是不能從列頭獲取元素的,哪怕有元素也不行。也就是說只有在延遲期到時才能夠從佇列中取元素。 DelayQu

Java併發----- Java 併發精品合集

點選上方“芋道原始碼”,選擇“置頂公眾號”技術文章第一時間送達!原始碼精品專欄 【死磕 Java