1. 程式人生 > >資料庫分庫分表中介軟體 Sharding-JDBC 原始碼分析 —— 分散式主鍵

資料庫分庫分表中介軟體 Sharding-JDBC 原始碼分析 —— 分散式主鍵

������關注微信公眾號:【芋道原始碼】有福利:
1. RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
3. 您對於原始碼的疑問每條留言將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢
4. 新的原始碼解析文章實時收到通知。每週更新一篇左右
5. 認真的原始碼交流微信群。

本文主要基於 Sharding-JDBC 1.5.0 正式版

1. 概述

本文分享 Sharding-JDBC 分散式主鍵

實現。

官方文件《分散式主鍵》對其介紹及使用方式介紹很完整,強烈先閱讀。下面先引用下分散式主鍵的實現動機

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

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

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

**Sharding-JDBC 正在收集使用公司名單:傳送門
�� 你的登記,會讓更多人蔘與和使用 Sharding-JDBC。傳送門
Sharding-JDBC 也會因此,能夠覆蓋更多的業務場景。傳送門
登記吧,騷年!傳送門**

2. KeyGenerator

KeyGenerator,主鍵生成器介面。實現類通過實現 #generateKey() 方法對外提供生成主鍵的功能。

2.1 DefaultKeyGenerator

DefaultKeyGenerator,預設的主鍵生成器。該生成器採用 Twitter Snowflake 演算法實現,生成 64 Bits

Long 型編號。國內另外一款資料庫中介軟體 MyCAT 分散式主鍵也是基於該演算法實現。國內很多大型網際網路公司發號器服務基於該演算法加部分改造實現。所以 DefaultKeyGenerator 必須是根正苗紅。如果你對分散式主鍵感興趣,可以看看逗比筆者整理的《談談 ID》

咳咳咳,有點跑題了。編號由四部分組成,從高位到低位(從左到右)分別是:

Bits 名字 說明
1 符號位 等於 0
41 時間戳 從 2016/11/01 零點開始的毫秒數,支援 2 ^41 /365/24/60/60/1000=69.7年
10 工作程序編號 支援 1024 個程序
12 序列號 每毫秒從 0 開始自增,支援 4096 個編號

* 每個工作程序每秒可以產生 4096000 個編號。是不是灰常牛比 ��

// 
public final class DefaultKeyGenerator implements KeyGenerator {

    /**
     * 時間偏移量,從2016年11月1日零點開始
     */
    public static final long EPOCH;

    /**
     * 自增量佔用位元
     */
    private static final long SEQUENCE_BITS = 12L;
    /**
     * 工作程序ID位元
     */
    private static final long WORKER_ID_BITS = 10L;
    /**
     * 自增量掩碼(最大值)
     */
    private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
    /**
     * 工作程序ID左移位元數(位數)
     */
    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;
    /**
     * 工作程序ID最大值
     */
    private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS;

    @Setter
    private static TimeService timeService = new TimeService();

    /**
     * 工作程序ID
     */
    private static long workerId;

    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.
     * 
     * @param workerId 工作程序Id
     */
    public static void setWorkerId(final long workerId) {
        Preconditions.checkArgument(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE);
        DefaultKeyGenerator.workerId = workerId;
    }

    /**
     * 生成Id.
     * 
     * @return 返回@{@link Long}型別的Id
     */
    @Override
    public synchronized Number generateKey() {
        // 保證當前時間大於最後時間。時間回退會導致產生重複id
        long currentMillis = timeService.getCurrentMillis();
        Preconditions.checkState(lastTime <= currentMillis, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, currentMillis);
        // 獲取序列號
        if (lastTime == currentMillis) {
            if (0L == (sequence = ++sequence & SEQUENCE_MASK)) { // 當獲得序號超過最大值時,歸0,並去獲得新的時間
                currentMillis = waitUntilNextTime(currentMillis);
            }
        } else {
            sequence = 0;
        }
        // 設定最後時間戳
        lastTime = currentMillis;
        if (log.isDebugEnabled()) {
            log.debug("{}-{}-{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(lastTime)), workerId, sequence);
        }
        // 生成編號
        return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
    }

    /**
     * 不停獲得時間,直到大於最後時間
     *
     * @param lastTime 最後時間
     * @return 時間
     */
    private long waitUntilNextTime(final long lastTime) {
        long time = timeService.getCurrentMillis();
        while (time <= lastTime) {
            time = timeService.getCurrentMillis();
        }
        return time;
    }
}
  • EPOCH = calendar.getTimeInMillis(); 計算 2016/11/01 零點開始的毫秒數。
  • #generateKey() 實現邏輯
    1. 校驗當前時間小於等於最後生成編號時間戳,避免伺服器時鐘同步,可能產生時間回退,導致產生重複編號
      • 獲得序列號。當前時間戳可獲得自增量到達最大值時,呼叫 #waitUntilNextTime() 獲得下一毫秒
      • 設定最後生成編號時間戳,用於校驗時間回退情況
      • 位操作生成編號

總的來說,Twitter Snowflake 演算法實現上是相對簡單易懂的,較為麻煩的是怎麼解決工作程序編號的分配

  1. 超過 1024 個怎麼辦?
  2. 怎麼保證全域性唯一?

第一個問題,將分散式主鍵生成獨立成一個發號器服務,提供生成分散式編號的功能。這個不在本文的範圍內,有興趣的同學可以 Google 下。

第二個問題,通過 Zookeeper、Consul、Etcd 等提供分散式配置功能的中介軟體。當然 Sharding-JDBC 也提供了不依賴這些服務的方式,我們一個一個往下看。

2.2 HostNameKeyGenerator

根據機器名最後的數字編號獲取工作程序編號。
如果線上機器命名有統一規範,建議使用此種方式。
例如,機器的 HostName 為: dangdang-db-sharding-dev-01(公司名-部門名-服務名-環境名-編號),會擷取 HostName 最後的編號 01 作為工作程序編號( workId )。

// HostNameKeyGenerator.java
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!");
   }
   String hostName = address.getHostName();
   try {
       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);
}

2.3 IPKeyGenerator

根據機器IP獲取工作程序編號。
如果線上機器的IP二進位制表示的最後10位不重複,建議使用此種方式。
例如,機器的IP為192.168.1.108,二進位制表示:11000000 10101000 00000001 01101100,擷取最後 10 位 01 01101100,轉為十進位制 364,設定工作程序編號為 364。

// IPKeyGenerator.java
static void initWorkerId() {
   InetAddress address;
   try {
       address = InetAddress.getLocalHost();
   } catch (final UnknownHostException e) {
       throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");
   }
   byte[] ipAddressByteArray = address.getAddress();
   DefaultKeyGenerator.setWorkerId((long) (((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE)
           + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF)));
}

2.4 IPSectionKeyGenerator

來自 DogFc 貢獻,對 IPKeyGenerator 進行改造。

瀏覽 IPKeyGenerator 工作程序編號生成的規則後,感覺對伺服器IP後10位(特別是IPV6)數值比較約束。
有以下優化思路:
因為工作程序編號最大限制是 2^10,我們生成的工程程序編號只要滿足小於 1024 即可。
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
….為了保證相加生成出的工程程序編號 < 1024,思路是將每個 Bit 位的後6位相加。這樣在一定程度上也可以滿足workerId不重複的問題。
使用這種 IP 生成工作程序編號的方法,必須保證IP段相加不能重複

對於 IPV6 :2^ 6 = 64。64 * 8 = 512 < 1024。

// IPSectionKeyGenerator.java
static void initWorkerId() {
   InetAddress address;
   try {
       address = InetAddress.getLocalHost();
   } catch (final UnknownHostException e) {
       throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!");
   }
   byte[] ipAddressByteArray = address.getAddress();
   long workerId = 0L;
   // IPV4
   if (ipAddressByteArray.length == 4) {
       for (byte byteNum : ipAddressByteArray) {
           workerId += byteNum & 0xFF;
       }
   // IPV6
   } 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);
}

666. 彩蛋

沒有彩蛋。HOHOHO

道友,分享一波朋友圈可好。

感謝你,技術如此只好,還關注我的公眾號。