數據庫分庫分表中間件 Sharding-JDBC 源碼分析 —— 分布式主鍵
關註**微信公眾號:【芋道源碼】**有福利:
RocketMQ / MyCAT / Sharding-JDBC 所有源碼分析文章列表
RocketMQ / MyCAT / Sharding-JDBC 中文註釋源碼 GitHub 地址
您對於源碼的疑問每條留言都將得到認真回復。甚至不知道如何讀源碼也可以請教噢。
新的源碼解析文章實時收到通知。每周更新一篇左右。
認真的
源碼交流微信群。
本文主要基於 Sharding-JDBC 1.5.0 正式版
1. 概述
2.KeyGenerator
2.1 DefaultKeyGenerator
2.2 HostNameKeyGenerator
2.3 IPKeyGenerator
2.4 IPSectionKeyGenerator
666. 彩蛋
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()
實現邏輯獲得序列號。當前時間戳可獲得自增量到達最大值時,調用
#waitUntilNextTime()
獲得下一毫秒設置最後生成編號時間戳,用於校驗時間回退情況
位操作生成編號
校驗當前時間小於等於最後生成編號時間戳,避免服務器時鐘同步,可能產生時間回退,導致產生重復編號
總的來說,Twitter Snowflake 算法實現上是相對簡單易懂的,較為麻煩的是怎麽解決工作進程編號的分配?
超過 1024 個怎麽辦?
怎麽保證全局唯一?
第一個問題,將分布式主鍵生成獨立成一個發號器服務,提供生成分布式編號的功能。這個不在本文的範圍內,有興趣的同學可以 Google 下。
第二個問題,通過 Zookeeper、Consul、Etcd 等提供分布式配置功能的中間件。當然 Sharding-JDBC 也提供了不依賴這些服務的方式,我們一個一個往下看。
2.2 HostNameKeyGenerator
根據機器名最後的數字編號獲取工作進程編號。
如果線上機器命名有統一規範,建議使用此種方式。
例如,機器的 HostName 為:dangdang-db-sharding-dev-01
(公司名-部門名-服務名-環境名-編號),會截取 HostName 最後的編號 01 作為工作進程編號( workId )。
// HostNameKeyGenerator.javastatic 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.javastatic 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位限制。
針對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.javastatic 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
道友,分享一波朋友圈可好。
感謝你,技術如此只好,還關註我的公眾號。
數據庫分庫分表中間件 Sharding-JDBC 源碼分析 —— 分布式主鍵