搞定了六種分布式ID,分庫(kù)分表哪個(gè)適合做主鍵?
今天咱們繼續(xù)一起來(lái)探究下,分布式ID在分庫(kù)分表中起到的作用以及如何使用,ShardingSphere-jdbc中已經(jīng)為我們提供了多種分布式主鍵ID生成策略。接下來(lái)將分別介紹這些策略的優(yōu)缺點(diǎn),看看它們?cè)趯?shí)際應(yīng)用中的場(chǎng)景和效果。
圖片
小富技術(shù)站:https://xiaofucode.com
為什么用分布式主鍵ID
在傳統(tǒng)的單庫(kù)單表結(jié)構(gòu)時(shí),通常可以使用自增主鍵來(lái)保證數(shù)據(jù)的唯一性。但在分庫(kù)分表的情況下,每個(gè)表的默認(rèn)自增步長(zhǎng)為1,這導(dǎo)致了各個(gè)庫(kù)、表之間可能存在重疊的主鍵范圍,從而使得主鍵字段失去了其唯一性的意義。
為了解決這一問(wèn)題,我們需要引入專門的分布式 ID 生成器來(lái)生成全局唯一的ID,并將其作為每條記錄的主鍵,以確保全局唯一性。通過(guò)這種方式,我們能夠有效地避免數(shù)據(jù)沖突和重復(fù)插入的問(wèn)題,從而保障系統(tǒng)的正常運(yùn)行。
除了滿足唯一性的基本要求外,作為主鍵 ID,我們還需要關(guān)注主鍵字段的數(shù)據(jù)類型、長(zhǎng)度對(duì)性能的影響。因?yàn)橹麈I字段的數(shù)據(jù)類型、長(zhǎng)度直接影響著數(shù)據(jù)庫(kù)的查詢效率和整體系統(tǒng)性能表現(xiàn),這一點(diǎn)也是我們?cè)谶x方案時(shí)需要考慮的因素。
內(nèi)置算法
在ShardingSphere 5.X版本后進(jìn)一步豐富了其框架內(nèi)部的主鍵生成策略方案。此前僅提供了UUID和Snowflake兩種策略,現(xiàn)在又陸續(xù)提供了NanoID、CosId、CosId-Snowflake三種策略。下面我們將逐個(gè)的過(guò)一下。
注意:SQL中不要主動(dòng)拼接主鍵字段(包括持久化工具自動(dòng)拼接的)否則一律走默認(rèn)的Snowflake策略!!!
ShardingSphere中為分片表設(shè)置主鍵生成策略后,執(zhí)行插入操作時(shí),會(huì)自動(dòng)在SQL中拼接配置的主鍵字段和生成的分布式ID值。所以,在創(chuàng)建分片表時(shí)主鍵字段無(wú)需再設(shè)置 自增 AUTO_INCREMENT。同時(shí),在插入數(shù)據(jù)時(shí)應(yīng)避免為主鍵字段賦值,否則會(huì)覆蓋主鍵策略生成的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
想要獲得一個(gè)具有唯一性的ID,大概率會(huì)先想到UUID,因?yàn)樗粌H具有全球唯一的特性使用還簡(jiǎn)單。但并不推薦將其作為主鍵ID。
? UUID的無(wú)序性。在插入新行數(shù)據(jù)后,InnoDB無(wú)法像插入有序數(shù)據(jù)那樣直接將新行追加到表尾,而是需要為新行尋找合適的位置來(lái)分配空間。由于ID無(wú)序,頁(yè)分裂操作變得不可避免,導(dǎo)致大量數(shù)據(jù)的移動(dòng)。頻繁的頁(yè)分裂會(huì)導(dǎo)致數(shù)據(jù)碎片化(即數(shù)據(jù)在物理存儲(chǔ)上分散分布)。這種隨機(jī)的ID分配過(guò)程需要大量的額外操作,導(dǎo)致頻繁的對(duì)數(shù)據(jù)進(jìn)行無(wú)序的訪問(wèn),導(dǎo)致磁盤尋道時(shí)間增加。數(shù)據(jù)的無(wú)序性進(jìn)一步加劇了數(shù)據(jù)碎片化,降低了數(shù)據(jù)訪問(wèn)效率。
? UUID字符串類型。字符串比數(shù)字類型占用更多的存儲(chǔ)空間,對(duì)存儲(chǔ)和查詢性能造成較大的消耗;字符串類型的長(zhǎng)度可變,可變長(zhǎng)度的數(shù)據(jù)行會(huì)破壞索引的連續(xù)性,導(dǎo)致索引查找性能下降。
算法類型: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} # 數(shù)據(jù)節(jié)點(diǎn):數(shù)據(jù)庫(kù).分片表
database-strategy: # 分庫(kù)策略
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 生成唯一標(biāo)識(shí)符的輕量級(jí)庫(kù)。不過(guò),與 UUID 不同的是 NanoID 生成的字符串ID長(zhǎng)度較短,僅為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} # 數(shù)據(jù)節(jié)點(diǎn):數(shù)據(jù)庫(kù).分片表
key-generate-strategy: # 分布式主鍵生成策略
column: id
keyGeneratorName: nanoid-gen
定制雪花算法
雪花算法是比較主流的分布式ID生成方案,在 ShardingSphere 中的Snowflake算法生成的是 Long 類型的 ID,通常作為默認(rèn)的主鍵生成策略使用。
內(nèi)置的雪花算法生成的ID主要由時(shí)間戳、工作機(jī)器IDworkId、序列號(hào)sequence三部分組成。
@Override
public synchronized Long generateKey() {
..........
return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
定制 Snowflake 算法有三個(gè)可配置的屬性:
worker-id:工作機(jī)器唯一標(biāo)識(shí),單機(jī)模式下會(huì)直接取此屬性值計(jì)算ID,默認(rèn)是0;集群模式下則由系統(tǒng)自動(dòng)生成,此屬性無(wú)效
max-vibration-offset:最大抖動(dòng)上限值,范圍[0, 4096),默認(rèn)是1。那么如何理解這個(gè)屬性呢?這個(gè)屬性是用來(lái)控制上邊生成雪花ID中的sequence。通過(guò)限制抖動(dòng)范圍,同一毫秒內(nèi)生成的ID中引入微小的變化,讓數(shù)據(jù)更均勻地分散到不同的分片上。
private void vibrateSequenceOffset() {
sequenceOffset = sequenceOffset >= maxVibrationOffset ? 0 : sequenceOffset + 1;
}
若使用此算法生成值作分片值,建議配置此屬性。此算法在不同毫秒內(nèi)所生成的 key 取模 2^n (2^n一般為分庫(kù)或分表數(shù)) 之后結(jié)果總為 0 或 1。為防止上述分片問(wèn)題,建議將此屬性值配置為 (2^n)-1
max-tolerate-time-difference-milliseconds:最大容忍時(shí)鐘回退時(shí)間(毫秒)。服務(wù)器在校對(duì)時(shí)間時(shí)可能會(huì)發(fā)生時(shí)鐘回?fù)艿那闆r(當(dāng)前時(shí)間回退),由于根據(jù)時(shí)間戳參與計(jì)算ID,這可能導(dǎo)致生成相同的ID,而這對(duì)系統(tǒng)來(lái)說(shuō)是不可接受的。
ShardingSphere 雪花算法針對(duì)時(shí)鐘回?fù)軋?chǎng)景進(jìn)行了處理,記錄最后一次生成ID的時(shí)間 lastMilliseconds,并與回?fù)芎蟮漠?dāng)前時(shí)間 currentMilliseconds 進(jìn)行比對(duì)。如果時(shí)間差超過(guò)了設(shè)置的最大容忍時(shí)鐘回退時(shí)間,系統(tǒng)將直接拋出異常;如果未超過(guò),則系統(tǒng)會(huì)休眠等待兩者時(shí)間差的時(shí)長(zhǎng),核心原則確保不會(huì)發(fā)放重復(fù)的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: # 工作機(jī)器唯一標(biāo)識(shí)
max-vibration-offset: 1024 # 最大抖動(dòng)上限值,范圍[0, 4096)。注:若使用此算法生成值作分片值,建議配置此屬性。此算法在不同毫秒內(nèi)所生成的 key 取模 2^n (2^n一般為分庫(kù)或分表數(shù)) 之后結(jié)果總為 0 或 1。為防止上述分片問(wèn)題,建議將此屬性值配置為 (2^n)-1
max-tolerate-time-difference-milliseconds: 10 # 最大容忍時(shí)鐘回退時(shí)間,單位:毫秒
tables:
t_order: # 邏輯表名稱
actual-data-nodes: db$->{0..1}.t_order_${0..2} # 數(shù)據(jù)節(jié)點(diǎn):數(shù)據(jù)庫(kù).分片表
key-generate-strategy: # 分布式主鍵生成策略
column: id
keyGeneratorName: snowflake-gen
CosId
CosId 是一個(gè)高性能的分布式ID生成器框架,Shardingsphere 將其引入到自身的框架內(nèi),只簡(jiǎn)單的使用了 CosId 算法。但目前親測(cè) 5.2.0版本該算法處于不可用狀態(tài)!!!我已經(jīng)給官方提了issue,看看他們咋回復(fù)吧。
CosId 框架內(nèi)提供了 3 種算法:
? SnowflakeId: 單機(jī) TPS 性能:409W/s , 主要解決時(shí)鐘回?fù)軉?wèn)題 、機(jī)器號(hào)分配問(wèn)題并且提供更加友好、靈活的使用體驗(yàn)。
? SegmentId: 每次獲取一段 (Step) ID,來(lái)降低號(hào)段分發(fā)器的網(wǎng)絡(luò)IO請(qǐng)求頻次提升性能,提供多種存儲(chǔ)后端:關(guān)系型數(shù)據(jù)庫(kù)、Redis、Zookeeper 供用戶選擇。
? SegmentChainId(推薦): SegmentChainId (lock-free) 是對(duì) SegmentId 的增強(qiáng)。性能可達(dá)到近似 AtomicLong 的 TPS 性能 12743W+/s。
該算法使用對(duì)外提供了兩個(gè)屬性:
? id-name:ID 生成器名稱。
? as-string:是否生成字符串類型ID,將 long 類型 ID 轉(zhuǎn)換成 62 進(jìn)制 String 類型(Long.MAX_VALUE 最大字符串長(zhǎng)度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} # 數(shù)據(jù)節(jié)點(diǎn):數(shù)據(jù)庫(kù).分片表
key-generate-strategy: # 分布式主鍵生成策略
column: id
keyGeneratorName: cosId-gen
CosId-Snowflake
CosId-Snowflake 是 CosId 框架內(nèi)提供的 Snowflake 算法,它的實(shí)現(xiàn)原理和上邊的定制版雪花算法類似,ID主要也是由時(shí)間戳、工作機(jī)器ID、序列號(hào)sequence三部分組成。同樣處理了時(shí)鐘回?fù)艿葐?wèn)題。
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;
}
}
}
這個(gè)算法提供了兩個(gè)屬性:
? epoch:固定的起始時(shí)間點(diǎn),雪花ID算法的 epoch 變量值,默認(rèn)值:1477929600000。用它的目的提高生成的ID的時(shí)間戳部分的可讀性、穩(wěn)定性和范圍限制,使得生成的ID更加可靠和易于管理。
? as-string:是否生成字符串類型ID,將 long 類型 ID 轉(zhuǎn)換成 62 進(jìn)制 String 類型(Long.MAX_VALUE 最大字符串長(zhǎng)度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} # 數(shù)據(jù)節(jié)點(diǎn):數(shù)據(jù)庫(kù).分片表
key-generate-strategy: # 分布式主鍵生成策略
column: id
keyGeneratorName: cosId-snowflake-gen
自定義分布式主鍵
上邊咱們介紹了 ShardingSphere 內(nèi)提供的 5 種生成主鍵的ID算法,這些算法基本可以滿足大部分的業(yè)務(wù)場(chǎng)景。不過(guò),在某些情況下,我們可能會(huì)要求生成的ID具有特殊的含義或遵循特定的規(guī)則。ShardingSphere 也支持我們自定義生成主鍵ID,來(lái)滿足定制的業(yè)務(wù)需求。
實(shí)現(xiàn)接口
要實(shí)現(xiàn)自定義的主鍵生成算法,首先需要實(shí)現(xiàn) KeyGenerateAlgorithm 接口,并實(shí)現(xiàn)內(nèi)部 4 個(gè)方法, 其中有兩個(gè)方法比較關(guān)鍵:
- ? getType():我們自定義的算法類型,方便配置使用;
- ? generateKey():處理主鍵生成的核心邏輯,我們可以根據(jù)業(yè)務(wù)需求選擇合適的主鍵生成算法,比如美團(tuán)的 Leaf、滴滴的 TinyId 等。
@Data
@Slf4j
public class SequenceAlgorithms implements KeyGenerateAlgorithm {
// 這個(gè)方法用于指定我們自定義的算法的類型。它會(huì)返回一個(gè)字符串,表示所使用算法的類型,方便在配置和識(shí)別時(shí)使用。
@Override
public String getType() {
// 返回算法類型表示
return "custom";
}
// 這是生成主鍵的核心邏輯所在。在這個(gè)方法內(nèi)部,我們可以根據(jù)業(yè)務(wù)需求選擇合適的主鍵生成算法,比如美團(tuán)的Leaf、滴滴的TinyId等。這個(gè)方法的具體實(shí)現(xiàn)會(huì)根據(jù)所選算法的特點(diǎn)和要求來(lái)設(shè)計(jì)
@Override
public Comparable<?> generateKey() {
return null;
}
@Override
public Properties getProps() {
return null;
}
// 這個(gè)方法用于初始化主鍵生成算法所需的資源或配置
@Override
public void init(Properties properties) {
}
}
在引入外部的分布式ID生成器時(shí),應(yīng)盡量遵循以下原則:
- ? 全局唯一:必須保證ID是全局性唯一的,基本要求
- ? 高性能:高可用低延時(shí),ID生成響應(yīng)要塊,否則反倒會(huì)成為業(yè)務(wù)瓶頸
- ? 高可用:100%的可用性是騙人的,但是也要無(wú)限接近于100%的可用性
- ? 好接入:要秉著拿來(lái)即用的設(shè)計(jì)原則,在系統(tǒng)設(shè)計(jì)和實(shí)現(xiàn)上要盡可能的簡(jiǎn)單
SPI 注冊(cè)
通過(guò) SPI 方式加載我們自定義的主鍵算法,需要在 resource/META-INF/services 目錄下創(chuàng)建一個(gè)文件,文件名為 org.apache.shardingsphere.sharding.spi.KeyGenerateAlgorithm,并將我們自定義的主鍵算法的完整類路徑放入文件內(nèi),每行一個(gè)。在系統(tǒng)啟動(dòng)時(shí)會(huì)自動(dòng)加載到這個(gè)文件,讀取其中的類路徑,然后通過(guò)反射機(jī)制實(shí)例化對(duì)應(yīng)的類,完成主鍵算法的注冊(cè)和加載。
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} # 數(shù)據(jù)節(jié)點(diǎn):數(shù)據(jù)庫(kù).分片表
key-generate-strategy: # 分布式主鍵生成策略
column: id
keyGeneratorName: xiaofu-id-gen
當(dāng)執(zhí)行插入操作時(shí),debug 看已經(jīng)進(jìn)入到了定義的主鍵算法內(nèi)了。
圖片
總結(jié)
我們介紹了 ShardingSphere 的幾種內(nèi)置主鍵生成策略以及如何自定義主鍵生成策略,市面上還有許多優(yōu)秀的分布式ID框架都可以整合進(jìn)來(lái),但具體選擇何種策略還是要取決于自身的業(yè)務(wù)需求。關(guān)于分布式 ID 生成器,我曾經(jīng)撰寫過(guò)一篇 一口氣說(shuō)出 9種 分布式ID生成方式,詳細(xì)介紹了多種生成器的優(yōu)缺點(diǎn),大家可以作為參考。
案例GitHub地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/shardingsphere101/shardingsphere-sequence-algorithm