阿飛Javaer,轉載請註明原創出處,謝謝!
實現動機
傳統數據庫軟件開發中,主鍵自動生成技術是基本需求。而各大數據庫對於該需求也提供了相應的支持,比如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
- 根據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
- 根據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
- 根據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);
}