在傳統數據庫軟件開發過程中,主鍵自動生成技術是基本需求。各個數據庫對該需求也提供了相應的支持,比如 MySQL 的自增鍵,Oracle 的自增序列等。而在分片場景下,問題就變得有點復雜,我們不能依靠單個實例上的自增鍵來實現不同數據節點之間的全局唯一主鍵,這時分布式主鍵的需求就應運而生。ShardingSphere 作為一款優秀的分庫分表開源軟件,同樣提供了分布式主鍵的實現機制,今天,我們就對這一機制的基本原理和實現方式展開討論。
ShardingSphere 中的自動生成鍵方案
在介紹 ShardingSphere 提供的具體分布式主鍵實現方式之前,我們有必要先對框架中抽象的自動生成鍵 GeneratedKey 方案進行討論,從而幫助你明確分布式主鍵的具體使用場景和使用方法。
ShardingSphere 中的 GeneratedKey
GeneratedKey 並不是 ShardingSphere 所創造的概念。如果你熟悉 Mybatis 這種 ORM 框架,對它就不會陌生。事實上,我們在《數據分片:如何實現分庫、分表、分庫+分表以及強制路由(上)?》中已經介紹了在 Mybatis 中嵌入 GeneratedKey 的實現方法。通常,我們會在 Mybatis 的 Mapper 文件中設置 useGeneratedKeys 和 keyProperty 屬性:
<insert id="addEntity" useGeneratedKeys="true" keyProperty="recordId" >
INSERT INTO health_record (user_id, level_id, remark)
VALUES (#{userId,jdbcType=INTEGER}, #{levelId,jdbcType=INTEGER},
#{remark,jdbcType=VARCHAR})
</insert>
在執行這個 insert 語句時,返回的對象中自動包含了生成的主鍵值。當然,這種方式能夠生效的前提是對應的數據庫本身支持自增長的主鍵。
當我們使用 ShardingSphere 提供的自動生成鍵方案時,開發過程以及效果和上面描述的完全一致。在 ShardingSphere 中,同樣實現了一個 GeneratedKey 類。請注意,該類位於 sharding-core-route 工程下。我們先看該類提供的 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.");
}
//從TableRule中獲取ShardingKeyGenerator並生成分布式主鍵
ShardingKeyGenerator shardingKeyGenerator = null == tableRule.get().getShardingKeyGenerator() ? defaultShardingKeyGenerator : tableRule.get().getShardingKeyGenerator();
return shardingKeyGenerator.generateKey();
}
首先,根據傳入的 logicTableName 找到對應的 TableRule,基於 TableRule 找到其包含的 ShardingKeyGenerator,然后通過 ShardingKeyGenerator 的 generateKey 來生成主鍵。從設計模式上講,ShardingRule 也只是一個外觀類,真正創建 ShardingKeyGenerator 的過程應該是在 TableRule 中。而這里的 ShardingKeyGenerator 顯然就是真正生成分布式主鍵入口,讓我們來看一下。
ShardingKeyGenerator
接下來我們分析 ShardingKeyGenerator 接口,從定義上看,該接口繼承了 TypeBasedSPI 接口:
public interface ShardingKeyGenerator extends TypeBasedSPI {
Comparable<?> generateKey();
}
來到 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);
}
}
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 包下。
ShardingSphere 中的分布式主鍵實現方案
在 ShardingSphere 中,ShardingKeyGenerator 接口存在一批實現類。除了前面提到的 SnowflakeShardingKeyGenerator 和UUIDShardingKeyGenerator,還實現了 LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator 類,但這兩個類的實現過程有些特殊,我們一會再具體展開。
UUIDShardingKeyGenerator
我們先來看最簡單的 ShardingKeyGenerator,即 UUIDShardingKeyGenerator。UUIDShardingKeyGenerator 的實現非常容易理解,直接采用 UUID.randomUUID() 的方式產生分布式主鍵:
@Getter
@Setter
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("-", "");
}
}
SnowflakeShardingKeyGenerator
再來看 SnowFlake(雪花)算法,SnowFlake 是 ShardingSphere 默認的分布式主鍵生成策略。它是 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 算法依賴於時間戳,所以還需要考慮時鍾回撥這種場景。所謂時鍾回撥,是指服務器因為時間同步,導致某一部分機器的時鍾回到了過去的時間點。顯然,時間戳的回滾會導致生成一個已經使用過的 ID,因此默認分布式主鍵生成器提供了一個最大容忍的時鍾回撥毫秒數。如果時鍾回撥的時間超過最大容忍的毫秒數閾值,則程序報錯;如果在可容忍的范圍內,默認分布式主鍵生成器會等待時鍾同步到最后一次主鍵生成的時間后再繼續工作。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 算法的具體實現。
LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator
事實上,如果實現類似 SnowflakeShardingKeyGenerator 這樣的 ShardingKeyGenerator 是比較困難的,而且也屬於重復造輪子。因此,盡管 ShardingSphere 在 4.X 版本中也提供了 LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator 這兩個 ShardingKeyGenerator 的完整實現類。但在正在開發的 5.X 版本中,這兩個實現類被移除了。
目前,ShardingSphere 專門提供了 OpenSharding 這個代碼倉庫來存放新版本的 LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator。新版本的實現類直接采用了第三方美團提供的 Leaf 開源實現。
Leaf 提供兩種生成 ID 的方式,一種是號段(Segment)模式,一種是前面介紹的 Snowflake 模式。無論使用哪種模式,我們都需要提供一個 leaf.properties 文件,並設置對應的配置項。無論是使用哪種方式,應用程序都需要設置一個 leaf.key:
# for keyGenerator key
leaf.key=sstest
# for LeafSnowflake
leaf.zk.list=localhost:2181
如果使用號段模式,需要依賴於一張數據庫表來存儲運行時數據,因此需要在 leaf.properties 文件中添加數據庫的相關配置:
# for LeafSegment
leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC&useSSL=false
leaf.jdbc.username=root
leaf.jdbc.password=123456
基於這些配置,我們就可以創建對應的 DataSource,並進一步創建用於生成分布式 ID 的 IDGen 實現類,這里創建的是基於號段模式的 SegmentIDGenImpl 實現類:
//通過DruidDataSource構建數據源並設置屬性
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(properties.getProperty(LeafPropertiesConstant.LEAF_JDBC_URL));
dataSource.setUsername(properties.getProperty(LeafPropertiesConstant.LEAF_JDBC_USERNAME));
dataSource.setPassword(properties.getProperty(LeafPropertiesConstant.LEAF_JDBC_PASSWORD));
dataSource.init();
//構建數據庫訪問Dao組件
IDAllocDao dao = new IDAllocDaoImpl(dataSource);
//創建IDGen實現類
this.idGen = new SegmentIDGenImpl();
//將Dao組件綁定到IDGen實現類
((SegmentIDGenImpl) this.idGen).setDao(dao);
this.idGen.init();
this.dataSource = dataSource;
一旦我們成功創建了 IDGen 實現類,可以通過該類來生成目標 ID,LeafSegmentKeyGenerator 類中包含了所有的實現細節:
Result result = this.idGen.get(properties.getProperty(LeafPropertiesConstant.LEAF_KEY));
return result.getId();
介紹完 LeafSegmentKeyGenerator 之后,我們再來看 LeafSnowflakeKeyGenerator。LeafSnowflakeKeyGenerator 的實現依賴於分布式協調框架 Zookeeper,所以在配置文件中需要指定 Zookeeper 的目標地址:
# for LeafSnowflake
leaf.zk.list=localhost:2181
創建用於 LeafSnowflake 的 IDGen 實現類 SnowflakeIDGenImpl 相對比較簡單,我們直接在構造函數中設置 Zookeeper 地址就可以了:
IDGen idGen = new SnowflakeIDGenImpl(properties.getProperty(LeafPropertiesConstant.LEAF_ZK_LIST), 8089);
同樣,通過 IDGen 獲取模板 ID 的方式是一致的:
idGen.get(properties.getProperty(LeafPropertiesConstant.LEAF_KEY)).getId();
顯然,基於 Leaf 框架實現號段模式和 Snowflake 模式下的分布式 ID 生成方式非常簡單,Leaf 框架為我們屏蔽了內部實現的復雜性。
從源碼解析到日常開發
相比 ShardingSphere 中其他架構設計上的思想和實現方案,分布式主鍵非常獨立,所以今天介紹的各種分布式主鍵的實現方式完全可以直接套用到日常開發過程中。無論是 ShardingSphere 自身實現的 SnowflakeShardingKeyGenerator,還是基於第三方框架實現的 LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator,都為我們使用分布式主鍵提供了直接的解決方案。當然,我們也可以在這些實現方案的基礎上,進一步挖掘同類型的其他方案。
總結
在分布式系統的開發過程中,分布式主鍵是一種基礎需求。而對於與數據庫相關的操作而言,我們往往需要將分布式主鍵與數據庫的主鍵自動生成機制關聯起來。我們就從 ShardingSphere 的自動生成鍵方案說起,引出了分布式主鍵的各種實現方案。這其中包括最簡單的 UUID,也包括經典的雪花算法,以及雪花算法的改進方案 LeafSegment 和 LeafSnowflake 算法。