在上一篇中,我們提到了在ShardingRouter的route方法中,當輸入的SQLStatement是InsertStatement時,會自動生成主鍵,代碼如下所示:
//如果是InsertStatement則自動生成主鍵
Optional<GeneratedKey> generatedKey = sqlStatement instanceof InsertStatement
? GeneratedKey.getGenerateKey(shardingRule, metaData.getTables(), parameters, (InsertStatement) sqlStatement) : Optional.<GeneratedKey>absent();
在傳統數據庫軟件開發中,主鍵自動生成技術是基本需求。而各個數據庫對於該需求也提供了相應的支持,比如MySQL的自增鍵,Oracle的自增序列等。而在分片場景下,問題就變得有點複雜,我們不能依靠單個實例上的自增鍵來實現不同數據節點之間的全局唯一主鍵。分佈式主鍵的需求就應運而生。ShardingSphere作爲一款優秀的分庫分表開源軟件,同樣提供了分佈式主鍵的實現機制,本文就對這一機制的基本原理和實現方式展開討論。
我們基於前面的這段代碼來到GeneratedKey類,請注意該類位於sharding-core-route工程的org.apache.shardingsphere.core.route.router.sharding.keygen包下。我們先看該類提供的getGenerateKey方法,如下所示:
public static Optional<GeneratedKey> getGenerateKey(final ShardingRule shardingRule, final TableMetas tableMetas, final List<Object> parameters, final InsertStatement insertStatement) {
Optional<String> generateKeyColumnName = shardingRule.findGenerateKeyColumnName(insertStatement.getTable().getTableName());
if (!generateKeyColumnName.isPresent()) {
return Optional.absent();
}
return Optional.of(containsGenerateKey(tableMetas, insertStatement, generateKeyColumnName.get())
? findGeneratedKey(tableMetas, parameters, insertStatement, generateKeyColumnName.get()) : createGeneratedKey(shardingRule, insertStatement, generateKeyColumnName.get()));
}
這段代碼的邏輯在於先從ShardingRule中找到主鍵對應的Column,然後判斷是否已經包含了主鍵,如果是則找到該主鍵,如果不是則生成新的主鍵。本文的重點是關注於分佈式主鍵的生成,所以我們直接來到createGeneratedKey方法,如下所示:
private static GeneratedKey createGeneratedKey(final ShardingRule shardingRule, final InsertStatement insertStatement, final String generateKeyColumnName) {
GeneratedKey result = new GeneratedKey(generateKeyColumnName, true);
for (int i = 0; i < insertStatement.getValueListCount(); i++) {
result.getGeneratedValues().add(shardingRule.generateKey(insertStatement.getTable().getTableName()));
}
return result;
}
在GeneratedKey中存在一個類型爲LinkedList 的generatedValues變量來保存所生成的主鍵,但我們發現這裏生成主鍵的工作實際上是轉移到了ShardingRule的generateKey方法中,因此,讓我們跳轉到ShardingRule類並找到這個generateKey方法:
public Comparable<?> generateKey(final String logicTableName) {
Optional<TableRule> tableRule = findTableRule(logicTableName);
if (!tableRule.isPresent()) {
throw new ShardingConfigurationException("Cannot find strategy for generate keys.");
}
ShardingKeyGenerator shardingKeyGenerator = null == tableRule.get().getShardingKeyGenerator() ? defaultShardingKeyGenerator : tableRule.get().getShardingKeyGenerator();
return shardingKeyGenerator.generateKey();
}
我們首先根據傳入的logicTableName找到對應的TableRule,基於TableRule找到其包含的ShardingKeyGenerator,然後通過ShardingKeyGenerator的generateKey來生成主鍵。從設計模式上講,ShardingRule也只是一個門面(Facade)類,真正創建ShardingKeyGenerator的過程應該是在TableRule中。
我們來到TableRule中,終於在它的一個構造函數中找到了ShardingKeyGenerator的創建過程,代碼如下所示:
shardingKeyGenerator = containsKeyGeneratorConfiguration(tableRuleConfig)
? new ShardingKeyGeneratorServiceLoader().newService(tableRuleConfig.getKeyGeneratorConfig().getType(), tableRuleConfig.getKeyGeneratorConfig().getProperties()) : null;
這裏我們看到有一個ShardingKeyGeneratorServiceLoader類,該類定義如下:
public final class ShardingKeyGeneratorServiceLoader extends TypeBasedSPIServiceLoader<ShardingKeyGenerator> {
static {
NewInstanceServiceLoader.register(ShardingKeyGenerator.class);
}
public ShardingKeyGeneratorServiceLoader() {
super(ShardingKeyGenerator.class);
}
}
回顧《ShardingSphere源碼解析之微內核架構(下)》一文中的介紹,我們不難理解ShardingKeyGeneratorServiceLoader類的作用。ShardingKeyGeneratorServiceLoader繼承了TypeBasedSPIServiceLoader類,然後在靜態方法中通過NewInstanceServiceLoader註冊了類路徑中所有的ShardingKeyGenerator。然後,ShardingKeyGeneratorServiceLoader的newService方法基於類型通過SPI創建實例,並賦值Properties屬性。
通過繼承TypeBasedSPIServiceLoader類來創建一個新的ServiceLoader類,然後在其靜態方法中註冊相應的SPI實現,這種寫法是ShardingSphere中應用微內核模式的常見做法,很多地方都能看到類似的處理方法。
然後,我們在sharding-core-common工程的META-INF/services目錄中看到了如下所示的SPI定義:
可以看到,這裏有兩個ShardingKeyGenerator,分別是SnowflakeShardingKeyGenerator和UUIDShardingKeyGenerator,它們都位於org.apache.shardingsphere.core.strategy.keygen包下。
在理解了SPI實例記載機制的基礎上,接下來我們分析ShardingKeyGenerator接口,該接口繼承了TypeBasedSPI接口,其定義如下所示:
public interface ShardingKeyGenerator extends TypeBasedSPI {
Comparable<?> generateKey();
}
在ShardingSphere中,ShardingKeyGenerator接口存在一批實現類,在4.0.1版本中,除了前面提到的SnowflakeShardingKeyGenerator和UUIDShardingKeyGenerator,還實現了LeafSegmentKeyGenerator和LeafSnowflakeKeyGenerator類,這兩個類位於sharding-orchestration-core工程的org.apache.shardingsphere.orchestration.internal.keygen包中。
我們先來看最簡單的ShardingKeyGenerator,即UUIDShardingKeyGenerator。UUIDShardingKeyGenerator的實現非常簡單,直接採用UUID.randomUUID()的方式產生分佈式主鍵。UUIDShardingKeyGenerator的代碼如下所示:
public final class UUIDShardingKeyGenerator implements ShardingKeyGenerator {
private Properties properties = new Properties();
@Override
public String getType() {
return "UUID";
}
@Override
public synchronized Comparable<?> generateKey() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
然後,我們再來看SnowFlake(雪花)算法,SnowFlake是ShardingSphere默認的分佈式主鍵生成策略。
SnowFlake算法是Twitter開源的分佈式ID生成算法,其核心思想是使用一個64bit的long型的數字作爲全局唯一ID,且ID 引入了時間戳,基本上能夠保持自增。SnowFlake算法在分佈式系統中的應用十分廣泛SnowFlake算法中64bit的詳細結構如下圖所示:
在上圖中,我們可以吧64bit分成了四個部分:
- 符號位
第一個部分即第一個 bit,值爲0,沒有實際意義。
- 時間戳位
第二個部分是41個bit,表示的是時間戳。41位的時間戳可以容納的毫秒數是2的41次冪,一年所使用的毫秒數是365 * 24 * 60 * 60 * 1000,即69.73年。也就是說,ShardingSphere的SnowFlake算法的時間紀元從2016年11月1日零點開始,可以使用到2086年,相信能滿足絕大部分系統的要求。
- 工作進程位
第三個部分是 10個bit,表示的是工作進程位,其中前5個bit代表機房id,後5個bit代表機器id。
- 序列號位
第四個部分是12個bit,表示的是序號,就是某個機房某臺機器上這一毫秒內同時生成的ID序號。如果在這個毫秒內生成的數量超過4096(即2的12次冪),那麼生成器會等待到下個毫秒繼續生成。
因爲SnowFlake算法依賴於時間戳,所以還有一種場景我們需要考慮,即時鐘回撥。服務器時鐘回撥會導致產生重複序列,因此默認分佈式主鍵生成器提供了一個最大容忍的時鐘回撥毫秒數。如果時鐘回撥的時間超過最大容忍的毫秒數閾值,則程序報錯;如果在可容忍的範圍內,默認分佈式主鍵生成器會等待時鐘同步到最後一次主鍵生成的時間後再繼續工作。ShardingSphere中最大容忍的時鐘回撥毫秒數的默認值爲0,可通過屬性設置。
瞭解了SnowFlake算法的基本概念之後,我們來看SnowflakeShardingKeyGenerator類的具體實現。首先在SnowflakeShardingKeyGenerator類中存在一批常量的定義,由於維護SnowFlake算法中各個bit之間的關係,同時還存在一個TimeService用於獲取當前的時間戳。
SnowflakeShardingKeyGenerator的核心方法generateKey負責生產具體的ID,代碼如下所示,我們對每行代碼都添加了註釋:
@Override
public synchronized Comparable<?> generateKey() {
//獲取當前時間戳
long currentMilliseconds = timeService.getCurrentMillis();
//如果出現了時鐘回撥,則拋出異常或進行時鐘等待
if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
currentMilliseconds = timeService.getCurrentMillis();
}
//如果上次的生成時間與本次的是同一毫秒
if (lastMilliseconds == currentMilliseconds) {
//這個位運算保證始終就是在4096這個範圍內,避免你自己傳遞的sequence超過了4096這個範圍
if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
//如果位運算結果爲0,則需要等待下一個毫秒繼續生成
currentMilliseconds = waitUntilNextTime(currentMilliseconds);
}
} else {//如果不是,則生成新的sequence
vibrateSequenceOffset();
sequence = sequenceOffset;
}
lastMilliseconds = currentMilliseconds;
//先將當前時間戳左移放到完成41個bit,然後將工作進程爲左移到10個bit,再將序號爲放到最後的12個bit
//最後拼接起來成一個64 bit的二進制數字
return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
可以看到這裏綜合考慮了時鐘回撥、同一個毫秒內請求等設計要素,從而完成了SnowFlake算法的具體實現。
更多內容可以關注我的公衆號:程序員向架構師轉型。