歡迎光臨
每天分享高質量文章

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

點選上方“Java技術驛站”,選擇“置頂公眾號”。

有內涵、有價值的文章第一時間送達!

實現動機

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

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

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

摘自sharding-jdbc分散式主鍵

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

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

分散式ID簡介

github上對分散式ID這個特性的描述是: DistributedUniqueTime-SequenceGeneration,兩個重要特性是:分散式唯一時間序;基於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中:

  1. public final class DefaultKeyGenerator implements KeyGenerator {

  2.    public static final long EPOCH;

  3.    // 自增長序列的長度(單位是位時的長度)

  4.    private static final long SEQUENCE_BITS = 12L;

  5.    // workerId的長度(單位是位時的長度)

  6.    private static final long WORKER_ID_BITS = 10L;

  7.    private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;

  8.    private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;

  9.    private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;

  10.    // 位運算計算workerId的最大值(workerId佔10位,那麼1向左移10位就是workerId的最大值)

  11.    private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS;

  12.    @Setter

  13.    private static TimeService timeService = new TimeService();

  14.    private static long workerId;

  15.    // EPOCH就是起始時間,從2016-11-01 00:00:00開始的毫秒數

  16.    static {

  17.        Calendar calendar = Calendar.getInstance();

  18.        calendar.set(2016, Calendar.NOVEMBER, 1);

  19.        calendar.set(Calendar.HOUR_OF_DAY, 0);

  20.        calendar.set(Calendar.MINUTE, 0);

  21.        calendar.set(Calendar.SECOND, 0);

  22.        calendar.set(Calendar.MILLISECOND, 0);

  23.        EPOCH = calendar.getTimeInMillis();

  24.    }

  25.    private long sequence;

  26.    private long lastTime;

  27.    /**

  28.     * 得到分散式唯一ID需要先設定workerId,workId的值範圍[0, 1024)

  29.     * @param workerId work process id

  30.     */

  31.    public static void setWorkerId(final long workerId) {

  32.        // google-guava提供的入參檢查方法:workerId只能在0~WORKER_ID_MAX_VALUE之間;

  33.        Preconditions.checkArgument(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE);

  34.        DefaultKeyGenerator.workerId = workerId;

  35.    }

  36.    /**

  37.     * 呼叫該方法,得到分散式唯一ID

  38.     * @return key type is @{@link Long}.

  39.     */

  40.    @Override

  41.    public synchronized Number generateKey() {

  42.        long currentMillis = timeService.getCurrentMillis();

  43.        // 每次取分散式唯一ID的時間不能少於上一次取時的時間

  44.        Preconditions.checkState(lastTime <= currentMillis, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, currentMillis);

  45.        // 如果同一毫秒範圍內,那麼自增,否則從0開始

  46.        if (lastTime == currentMillis) {

  47.            // 如果自增後的sequence值超過4096,那麼等待直到下一個毫秒

  48.            if (0L == (sequence = ++sequence & SEQUENCE_MASK)) {

  49.                currentMillis = waitUntilNextTime(currentMillis);

  50.            }

  51.        } else {

  52.            sequence = 0;

  53.        }

  54.        // 更新lastTime的值,即最後一次獲取分散式唯一ID的時間

  55.        lastTime = currentMillis;

  56.        // 從這裡可知分散式唯一ID的組成部分;

  57.        return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;

  58.    }

  59.    // 獲取下一毫秒的方法:死迴圈獲取當前毫秒與lastTime比較,直到大於lastTime的值;

  60.    private long waitUntilNextTime(final long lastTime) {

  61.        long time = timeService.getCurrentMillis();

  62.        while (time <= lastTime) {

  63.            time = timeService.getCurrentMillis();

  64.        }

  65.        return time;

  66.    }

  67. }

獲取workerId的三種方式

sharding-jdbc的 sharding-jdbc-plugin模組中,提供了三種方式獲取workerId的方式,並提供介面獲取分散式唯一ID的方法-- generateKey(),接下來對各種方式如何生成workerId進行分析;

HostNameKeyGenerator

  1. 根據hostname獲取,原始碼如下(HostNameKeyGenerator.java):

  1. /**

  2. * 根據機器名最後的數字編號獲取工作行程Id.如果線上機器命名有統一規範,建議使用此種方式.

  3. * 例如機器的HostName為:dangdang-db-sharding-dev-01(公司名-部門名-服務名-環境名-編號)

  4. * ,會擷取HostName最後的編號01作為workerId.

  5. *

  6. * @author DonneyYoung

  7. **/

  8. static void initWorkerId() {

  9.    InetAddress address;

  10.    Long workerId;

  11.    try {

  12.        address = InetAddress.getLocalHost();

  13.    } catch (final UnknownHostException e) {

  14.        throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");

  15.    }

  16.    // 先得到伺服器的hostname,例如JTCRTVDRA44,linux上可透過命令"cat /proc/sys/kernel/hostname"檢視;

  17.    String hostName = address.getHostName();

  18.    try {

  19.        // 計算workerId的方式:

  20.        // 第一步hostName.replaceAll("\\d+$", ""),即去掉hostname後純數字部分,例如JTCRTVDRA44去掉後就是JTCRTVDRA

  21.        // 第二步hostName.replace(第一步的結果, ""),即將原hostname的非數字部分去掉,得到純數字部分,就是workerId

  22.        workerId = Long.valueOf(hostName.replace(hostName.replaceAll("\\d+$", ""), ""));

  23.    } catch (final NumberFormatException e) {

  24.        throw new IllegalArgumentException(String.format("Wrong hostname:%s, hostname must be end with number!", hostName));

  25.    }

  26.    DefaultKeyGenerator.setWorkerId(workerId);

  27. }

IPKeyGenerator

  1. 根據IP獲取,原始碼如下(IPKeyGenerator.java):

  1. /**

  2. * 根據機器IP獲取工作行程Id,如果線上機器的IP二進製表示的最後10位不重覆,建議使用此種方式

  3. * ,列如機器的IP為192.168.1.108,二進製表示:11000000 10101000 00000001 01101100

  4. * ,擷取最後10位 01 01101100,轉為十進位制364,設定workerId為364.

  5. */

  6. static void initWorkerId() {

  7.    InetAddress address;

  8.    try {

  9.        // 首先得到IP地址,例如192.168.1.108

  10.        address = InetAddress.getLocalHost();

  11.    } catch (final UnknownHostException e) {

  12.        throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");

  13.    }

  14.    // IP地址byte[]陣列形式,這個byte陣列的長度是4,陣列0~3下標對應的值分別是192,168,1,108

  15.    byte[] ipAddressByteArray = address.getAddress();

  16.    // 由這裡計算workerId原始碼可知,workId由兩部分組成:

  17.    // 第一部分(ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE:ipAddressByteArray[ipAddressByteArray.length - 2]即取byte[]倒數第二個值,即1,然後&0B11,即只取最後2位(IP段倒數第二個段取2位,IP段最後一位取全部8位,總計10位),然後左移Byte.SIZE,即左移8位(因為這一部分取得的是IP段中倒數第二個段的值);

  18.    // 第二部分(ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF):ipAddressByteArray[ipAddressByteArray.length - 1]即取byte[]最後一位,即108,然後&0xFF,即透過位運算將byte轉為int;

  19.    // 最後將第一部分得到的值加上第二部分得到的值就是最終的workId

  20.    DefaultKeyGenerator.setWorkerId((long) (((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF)));

  21. }

IPSectionKeyGenerator

  1. 根據IP段獲取,原始碼如下(IPSectionKeyGenerator.java):

  1. /**

  2. * 瀏覽 {@link IPKeyGenerator} workerId生成的規則後,感覺對伺服器IP後10位(特別是IPV6)數值比較約束.

  3. *

  4. *

  5. * 有以下最佳化思路:

  6. * 因為workerId最大限制是2^10,我們生成的workerId只要滿足小於最大workerId即可。

  7. * 1.針對IPV4:

  8. * ....IP最大 255.255.255.255。而(255+255+255+255) < 1024。

  9. * ....因此採用IP段數值相加即可生成唯一的workerId,不受IP位限制。

  10. * 2.針對IPV6:

  11. * ....IP最大ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff

  12. * ....為了保證相加生成出的workerId < 1024,思路是將每個bit位的後6位相加。這樣在一定程度上也可以滿足workerId不重覆的問題。

  13. *

  • * 使用這種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:約定至上;並能夠讓架構最簡化,不依賴任何第三方元件;

    END

    贊(0)

    分享創造快樂

    © 2024 知識星球   網站地圖