大家好,我是小富~
本文是《ShardingSphere5.x分庫分表原理與實戰》系列的第七篇,目前系列的前幾篇製作成了PDF,需要的可以在文末獲取下載方式,持續更新中。今天咱們繼續一起來探究下,分佈式ID在分庫分表中起到的作用以及如何使用,ShardingSphere-jdbc
中已經爲我們提供了多種分佈式主鍵ID生成策略。接下來將分別介紹這些策略的優缺點,看看它們在實際應用中的場景和效果。
小富的技術站:https://xiaofucode.com
爲什麼用分佈式主鍵ID
在傳統的單庫單表結構時,通常可以使用自增主鍵來保證數據的唯一性。但在分庫分表的情況下,每個表的默認自增步長爲1,這導致了各個庫、表之間可能存在重疊的主鍵範圍,從而使得主鍵字段失去了其唯一性的意義。
爲了解決這一問題,我們需要引入專門的分佈式 ID 生成器來生成全局唯一的ID,並將其作爲每條記錄的主鍵,以確保全局唯一性。通過這種方式,我們能夠有效地避免數據衝突和重複插入的問題,從而保障系統的正常運行。
除了滿足唯一性的基本要求外,作爲主鍵 ID,我們還需要關注主鍵字段的數據類型、長度對性能的影響。因爲主鍵字段的數據類型、長度直接影響着數據庫的查詢效率和整體系統性能表現,這一點也是我們在選方案時需要考慮的因素。
內置算法
在ShardingSphere 5.X
版本後進一步豐富了其框架內部的主鍵生成策略方案。此前僅提供了UUID
和Snowflake
兩種策略,現在又陸續提供了NanoID
、CosId
、CosId-Snowflake
三種策略。下面我們將逐個的過一下。
注意:SQL中不要主動拼接主鍵字段(包括持久化工具自動拼接的)否則一律走默認的Snowflake
策略!!!
ShardingSphere
中爲分片表設置主鍵生成策略後,執行插入操作時,會自動在SQL
中拼接配置的主鍵字段和生成的分佈式ID值。所以,在創建分片表時主鍵字段無需再設置 自增 AUTO_INCREMENT。同時,在插入數據時應避免爲主鍵字段賦值,否則會覆蓋主鍵策略生成的ID。
CREATE TABLE `t_order` (
`id` bigint NOT NULL,
`order_id` bigint NOT NULL,
`user_id` bigint NOT NULL,
`order_number` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`customer_id` bigint NOT NULL,
`order_date` datetime DEFAULT NULL,
`interval_value` varchar(125) COLLATE utf8mb4_general_ci DEFAULT NULL,
`total_amount` decimal(10,2) NOT NULL,
PRIMARY KEY (`order_id`) USING BTREE
) ;
UUID
想要獲得一個具有唯一性的ID,大概率會先想到UUID,因爲它不僅具有全球唯一的特性使用還簡單。但並不推薦將其作爲主鍵ID。
-
UUID的無序性。在插入新行數據後,
InnoDB
無法像插入有序數據那樣直接將新行追加到表尾,而是需要爲新行尋找合適的位置來分配空間。由於ID無序,頁分裂操作變得不可避免,導致大量數據的移動。頻繁的頁分裂會導致數據碎片化(即數據在物理存儲上分散分佈)。這種隨機的ID分配過程需要大量的額外操作,導致頻繁的對數據進行無序的訪問,導致磁盤尋道時間增加。數據的無序性進一步加劇了數據碎片化,降低了數據訪問效率。 -
UUID字符串類型。字符串比數字類型佔用更多的存儲空間,對存儲和查詢性能造成較大的消耗;字符串類型的長度可變,可變長度的數據行會破壞索引的連續性,導致索引查找性能下降。
算法類型:UUID
spring:
shardingsphere:
rules:
sharding:
key-generators: # 分佈式序列算法配置
# UUID生成算法
uu-id-gen:
type: UUID
tables:
t_order: # 邏輯表名稱
actual-data-nodes: db$->{0..1}.t_order_${0..2} # 數據節點:數據庫.分片表
database-strategy: # 分庫策略
standard:
sharding-column: order_id
sharding-algorithm-name: t_order_database_mod
table-strategy: # 分表策略
standard:
sharding-column: order_id
sharding-algorithm-name: t_order_table_mod
key-generate-strategy: # 分佈式主鍵生成策略
column: id
keyGeneratorName: uu-id-gen
NanoID
或許很多人都不熟悉 NanoID
,它是一款用類似 UUID 生成唯一標識符的輕量級庫。不過,與 UUID 不同的是 NanoID 生成的字符串ID長度較短,僅爲21位。但仍然不推薦將它作爲主鍵ID,理由和UUID一樣。
算法類型:NANOID
spring:
shardingsphere:
rules:
sharding:
key-generators: # 分佈式序列算法配置
# nanoid生成算法
nanoid-gen:
type: NANOID
tables:
t_order: # 邏輯表名稱
actual-data-nodes: db$->{0..1}.t_order_${0..2} # 數據節點:數據庫.分片表
key-generate-strategy: # 分佈式主鍵生成策略
column: id
keyGeneratorName: nanoid-gen
定製雪花算法
雪花算法是比較主流的分佈式ID生成方案,在 ShardingSphere 中的Snowflake
算法生成的是 Long 類型的 ID,通常作爲默認的主鍵生成策略使用。
內置的雪花算法生成的ID主要由時間戳
、工作機器IDworkId
、序列號sequence
三部分組成。
@Override
public synchronized Long generateKey() {
..........
return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
定製 Snowflake 算法有三個可配置的屬性:
worker-id
:工作機器唯一標識,單機模式下會直接取此屬性值計算ID,默認是0;集羣模式下則由系統自動生成,此屬性無效
max-vibration-offset
:最大抖動上限值,範圍[0, 4096)
,默認是1。那麼如何理解這個屬性呢? 這個屬性是用來控制上邊生成雪花ID中的sequence
。通過限制抖動範圍,同一毫秒內生成的ID中引入微小的變化,讓數據更均勻地分散到不同的分片上。
private void vibrateSequenceOffset() {
sequenceOffset = sequenceOffset >= maxVibrationOffset ? 0 : sequenceOffset + 1;
}
若使用此算法生成值作分片值,建議配置此屬性。此算法在不同毫秒內所生成的 key 取模 2^n (2^n一般爲分庫或分表數) 之後結果總爲 0 或 1。爲防止上述分片問題,建議將此屬性值配置爲 (2^n)-1
max-tolerate-time-difference-milliseconds
:最大容忍時鐘回退時間(毫秒)。服務器在校對時間時可能會發生時鐘回撥的情況(當前時間回退),由於根據時間戳參與計算ID,這可能導致生成相同的ID,而這對系統來說是不可接受的。
ShardingSphere 雪花算法針對時鐘回撥場景進行了處理,記錄最後一次生成ID的時間 lastMilliseconds
,並與回撥後的當前時間 currentMilliseconds
進行比對。如果時間差超過了設置的最大容忍時鐘回退時間,系統將直接拋出異常;如果未超過,則系統會休眠等待兩者時間差的時長,核心原則確保不會發放重複的ID。
@SneakyThrows(InterruptedException.class)
private boolean waitTolerateTimeDifferenceIfNeed(final long currentMilliseconds) {
if (lastMilliseconds <= currentMilliseconds) {
return false;
}
long timeDifferenceMilliseconds = lastMilliseconds - currentMilliseconds;
Preconditions.checkState(timeDifferenceMilliseconds < maxTolerateTimeDifferenceMilliseconds,
"Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastMilliseconds, currentMilliseconds);
Thread.sleep(timeDifferenceMilliseconds);
return true;
}
算法類型:SNOWFLAKE
spring:
shardingsphere:
rules:
sharding:
key-generators: # 分佈式序列算法配置
# 雪花ID生成算法
snowflake-gen:
type: SNOWFLAKE
props:
worker-id: # 工作機器唯一標識
max-vibration-offset: 1024 # 最大抖動上限值,範圍[0, 4096)。注:若使用此算法生成值作分片值,建議配置此屬性。此算法在不同毫秒內所生成的 key 取模 2^n (2^n一般爲分庫或分表數) 之後結果總爲 0 或 1。爲防止上述分片問題,建議將此屬性值配置爲 (2^n)-1
max-tolerate-time-difference-milliseconds: 10 # 最大容忍時鐘回退時間,單位:毫秒
tables:
t_order: # 邏輯表名稱
actual-data-nodes: db$->{0..1}.t_order_${0..2} # 數據節點:數據庫.分片表
key-generate-strategy: # 分佈式主鍵生成策略
column: id
keyGeneratorName: snowflake-gen
CosId
CosId 是一個高性能的分佈式ID生成器框架,Shardingsphere 將其引入到自身的框架內,只簡單的使用了 CosId 算法。但目前親測 5.2.0版本該算法處於不可用狀態!!!我已經給官方提了issue,看看他們咋回覆吧。
CosId 框架內提供了 3 種算法:
SnowflakeId
: 單機 TPS 性能:409W/s , 主要解決時鐘回撥問題 、機器號分配問題並且提供更加友好、靈活的使用體驗。SegmentId
: 每次獲取一段 (Step) ID,來降低號段分發器的網絡IO請求頻次提升性能,提供多種存儲後端:關係型數據庫、Redis、Zookeeper 供用戶選擇。SegmentChainId
(推薦): SegmentChainId (lock-free) 是對 SegmentId 的增強。性能可達到近似 AtomicLong 的 TPS 性能 12743W+/s。
該算法使用對外提供了兩個屬性:
id-name
:ID 生成器名稱。as-string
:是否生成字符串類型ID,將 long 類型 ID 轉換成 62 進制 String 類型(Long.MAX_VALUE 最大字符串長度11位),並保證字符串 ID 有序性。
算法類型:COSID
spring:
shardingsphere:
rules:
sharding:
key-generators: # 分佈式序列算法配置
# COSID生成算法
cosId-gen:
type: COSID
props:
id-name: share
as-string: false
tables:
t_order: # 邏輯表名稱
actual-data-nodes: db$->{0..1}.t_order_${0..2} # 數據節點:數據庫.分片表
key-generate-strategy: # 分佈式主鍵生成策略
column: id
keyGeneratorName: cosId-gen
CosId-Snowflake
CosId-Snowflake 是 CosId 框架內提供的 Snowflake 算法,它的實現原理和上邊的定製版雪花算法類似,ID主要也是由時間戳
、工作機器ID、序列號sequence
三部分組成。同樣處理了時鐘回撥等問題。
public synchronized long generate() {
long currentTimestamp = this.getCurrentTime();
if (currentTimestamp < this.lastTimestamp) {
throw new ClockBackwardsException(this.lastTimestamp, currentTimestamp);
} else {
if (currentTimestamp > this.lastTimestamp && this.sequence >= this.sequenceResetThreshold) {
this.sequence = 0L;
}
this.sequence = this.sequence + 1L & this.maxSequence;
if (this.sequence == 0L) {
currentTimestamp = this.nextTime();
}
this.lastTimestamp = currentTimestamp;
long diffTimestamp = currentTimestamp - this.epoch;
if (diffTimestamp > this.maxTimestamp) {
throw new TimestampOverflowException(this.epoch, diffTimestamp, this.maxTimestamp);
} else {
return diffTimestamp << (int)this.timestampLeft | this.machineId << (int)this.machineLeft | this.sequence;
}
}
}
這個算法提供了兩個屬性:
epoch
:固定的起始時間點,雪花ID算法的 epoch 變量值,默認值:1477929600000。用它的目的提高生成的ID的時間戳部分的可讀性、穩定性和範圍限制,使得生成的ID更加可靠和易於管理。as-string
:是否生成字符串類型ID,將 long 類型 ID 轉換成 62 進制 String 類型(Long.MAX_VALUE 最大字符串長度11位),並保證字符串 ID 有序性。
算法類型:COSID_SNOWFLAKE
spring:
shardingsphere:
rules:
sharding:
key-generators: # 分佈式序列算法配置
# cosId-snowflake生成算法
cosId-snowflake-gen:
type: COSID_SNOWFLAKE
props:
epoch: 1477929600000
as-string: false
tables:
t_order: # 邏輯表名稱
actual-data-nodes: db$->{0..1}.t_order_${0..2} # 數據節點:數據庫.分片表
key-generate-strategy: # 分佈式主鍵生成策略
column: id
keyGeneratorName: cosId-snowflake-gen
自定義分佈式主鍵
上邊咱們介紹了 ShardingSphere 內提供的 5 種生成主鍵的ID算法,這些算法基本可以滿足大部分的業務場景。不過,在某些情況下,我們可能會要求生成的ID具有特殊的含義或遵循特定的規則。ShardingSphere 也支持我們自定義生成主鍵ID,來滿足定製的業務需求。
實現接口
要實現自定義的主鍵生成算法,首先需要實現 KeyGenerateAlgorithm
接口,並實現內部 4 個方法, 其中有兩個方法比較關鍵:
getType()
:我們自定義的算法類型,方便配置使用;generateKey()
:處理主鍵生成的核心邏輯,我們可以根據業務需求選擇合適的主鍵生成算法,比如美團的 Leaf、滴滴的 TinyId 等。
@Data
@Slf4j
public class SequenceAlgorithms implements KeyGenerateAlgorithm {
// 這個方法用於指定我們自定義的算法的類型。它會返回一個字符串,表示所使用算法的類型,方便在配置和識別時使用。
@Override
public String getType() {
// 返回算法類型表示
return "custom";
}
// 這是生成主鍵的核心邏輯所在。在這個方法內部,我們可以根據業務需求選擇合適的主鍵生成算法,比如美團的Leaf、滴滴的TinyId等。這個方法的具體實現會根據所選算法的特點和要求來設計
@Override
public Comparable<?> generateKey() {
return null;
}
@Override
public Properties getProps() {
return null;
}
// 這個方法用於初始化主鍵生成算法所需的資源或配置
@Override
public void init(Properties properties) {
}
}
在引入外部的分佈式ID生成器時,應儘量遵循以下原則:
- 全局唯一:必須保證ID是全局性唯一的,基本要求
- 高性能:高可用低延時,ID生成響應要塊,否則反倒會成爲業務瓶頸
- 高可用:100%的可用性是騙人的,但是也要無限接近於100%的可用性
- 好接入:要秉着拿來即用的設計原則,在系統設計和實現上要儘可能的簡單
SPI 註冊
通過 SPI 方式加載我們自定義的主鍵算法,需要在 resource/META-INF/services
目錄下創建一個文件,文件名爲 org.apache.shardingsphere.sharding.spi.KeyGenerateAlgorithm
,並將我們自定義的主鍵算法的完整類路徑放入文件內,每行一個。在系統啓動時會自動加載到這個文件,讀取其中的類路徑,然後通過反射機制實例化對應的類,完成主鍵算法的註冊和加載。
resource
|_META-INF
|_services
|_org.apache.shardingsphere.sharding.spi.KeyGenerateAlgorithm
配置使用
上邊完成了自定義算法的邏輯,使用上與其他的算法一致。只需將我們剛剛定義的算法類型 custom
配置上即可。
spring:
shardingsphere:
rules:
sharding:
key-generators: # 分佈式序列算法配置
# 自定義ID生成策略
xiaofu-id-gen:
type: custom
tables:
t_order: # 邏輯表名稱
actual-data-nodes: db$->{0..1}.t_order_${0..2} # 數據節點:數據庫.分片表
key-generate-strategy: # 分佈式主鍵生成策略
column: id
keyGeneratorName: xiaofu-id-gen
當執行插入操作時,debug 看已經進入到了定義的主鍵算法內了。
總結
我們介紹了 ShardingSphere 的幾種內置主鍵生成策略以及如何自定義主鍵生成策略,市面上還有許多優秀的分佈式ID框架都可以整合進來,但具體選擇何種策略還是要取決於自身的業務需求。關於分佈式 ID 生成器,我曾經撰寫過一篇 一口氣說出 9種 分佈式ID生成方式,詳細介紹了多種生成器的優缺點,大家可以作爲參考。