天天看點

分庫分表的架構如何設計自動路由

<code>ShardingCore</code> 易用、簡單、高性能、普适性,是一款擴充針對efcore生态下的分表分庫的擴充解決方案,支援efcore2+的所有版本,支援efcore2+的所有資料庫、支援自定義路由、動态路由、高性能分頁、讀寫分離的一款元件,如果你喜歡這元件或者這個元件對你有幫助請點選下發star讓更多的.neter可以看到使用

Github Source Code 助力dotnet 生态 Gitee Source Code

目前ShardingCore已經支援.net 6.0,因為ShardingCore的整個項目架構僅依賴efcore和efcore.relational基本上可以說是"零依賴"其他解析都是自行實作不依賴三方架構。

衆所周知.net架構下沒有一款好的自動分表分庫元件(ShardingCore除外),基本上看了一圈分表分庫的架構都說支援都說可以自動分表分庫,看了代碼都是半自動甚至全手動,那麼今天我就來講講應該如何設計一個真·自動分表/分庫的架構。(别說什麼封裝一下好了,那你倒是封裝啊)

那麼如何設計才可以讓使用者的查詢可以無感覺路由對應的分表分庫呢,而不是滿屏的指定查詢哪些表,指定路由可以有,但不應該作為查詢,真正的自動分表分庫路由應該是通過where條件進行過濾然後将資料路由到對應的表,接下來我将用簡單易懂的方式來講解如何設計一個字段路由的架構來實作分表自動化。

目前看來好像.NET環境下真的沒有幾個人做到了這點,稍微好一點的也就是碰到了真自動分表的腳,連膝蓋都沒打到。是以打算開一篇部落格來講講,順便講講<code>ShardingCore</code> 的原理.

首先因為業務的不同是以大部分人設計的分表可能都有寫差別,但是因為基本的部分情況下大緻都是相同的,這個相同比如取模,那麼肯定是00,01....99或者時間那麼肯定是2020,2021....2030等等都是相似的。

那麼我們現在假設我們是按基數取模比如按5那麼我們可以取出設定訂單表為<code>order_00</code>,<code>order_01</code>,<code>order_02</code>,<code>order_03</code>,<code>order_04</code>我們将訂單表分成4張表。

分表名稱

分表字段

分表方式

所有表字尾

order

Id

模4且左補齊2位

'00','01','02','03','04'

我們現在定義我們的查詢條件 select * from order where Id='12345',通過條件我們可以解析出有用的資訊有哪些

select * from order where Id='12345'

route parse engine=parse=&gt;得到如下結果

Key

Value

表名

字段

條件判斷符

=

條件

'12345'

條件連接配接符

是以我們可以通過得知字段id和字元串“12345”進行等于符号的比較,是以我們可以先對“12345”進行hash取值比如“12345”.HashCode()等于9,那麼9%5=4,我們對4往左補‘0’得到結果“04”,是以我們可以得出結論:

select * from order where Id='12345' ==select * from order_04 where Id='12345'

目前為止一個簡單的而取模分表路由我們已經知道大緻的流程了,得出如下結論

order表是否是分表的

2.where後的Id是否是分表字段

3.分表字段進行條件過濾可否轉成表字尾

總所周知取模分表的好處是可以最大化資料均勻,且相對實作簡單,但是也有很多問題,比如後期遷移資料擴大表的時候為了最小化遷移資料必須成倍增加表,但是哪怕成倍增加了最小遷移量也是50%。

當然這隻是取模分表的一個優缺點并不是本次的重點。接下來我們将sql改寫一下 select * from order where Id='12345' or Id='54321'通過這次轉變我們可以擷取到哪些資訊呢

'12345' 和 '54321'

or

那麼這種情況下我們該如何進行分表路由呢,首先我們可以通過得知字段id和字元串“12345”進行等于符号的比較,是以我們可以先對“12345”進行hash取值比如“12345”.HashCode()等于9,那麼9%5=4,我們對4往左補‘0’得到結果“04”,然後我們可以通過字段id和字元串“54321”進行等于符号的比較,是以我們可以先對“54321”進行hash取值比如“54321”.HashCode()等于8,那麼8%5=3,我們對3往左補‘0’得到結果“03”又因為條件連接配接符号是or是以我們要的是['03','04']是以 select * from order where Id='12345' or Id='54321'會被改寫成 select * from order_03 where Id='12345' or Id='54321' + select * from order_04 where Id='12345' or Id='54321'兩條sql的聚合結果,

如果是and的情況下就是既要走order_03又要走order_04是以結果就是空,那麼我們可以得出如下結論

3.多個表字尾如何篩選

假設我們現在的訂單是按月有 <code>order_202105</code>,<code>order_202106</code>,<code>order_202107</code>,<code>order_202108</code>,<code>order_202109</code>假設目前我們是這個5張表,訂單通過字段time進行時間分表,

我們如果需要解析select * from order where time&gt;'2021/06/05 00:00:00',首先我們還是通過程式進行解析提取關鍵字

time

&gt;

'2021/06/05 00:00:00'

通過關鍵字提取解析我們可以知道應該是查詢<code>order_202106</code>,<code>order_202107</code>,<code>order_202108</code>,<code>order_202109</code>3張表

我們如果需要解析select * from order where time&gt;'2021/06/05 00:00:00' and time &lt;'2021/08/05 00:00:00',首先我們還是通過程式進行解析提取關鍵字

&gt;、&lt;

'2021/06/05 00:00:00'、'2021/08/05 00:00:00'

and

select * from order where Id='12345' 改寫成 select * from order where '12345' =Id

遇到這種情況下我們該如何對現有的表達式進行判斷呢,這邊肯定是需要用到一個轉換就是:condition on right (條件在右)

那麼我們遇到的<code>=</code>其實和實際沒有差別,但是<code>&gt;</code>,<code>&lt;</code>如果相反會對結果有影響是以我們需要将對應的表達式進行反轉,是以

condtion on right ?

<code>=</code>

<code>!=</code>

<code>&gt;=</code>

<code>&lt;=</code>

<code>&gt;</code>

<code>&lt;</code>

如果條件在右側那麼我們不需要對條件判斷符進行轉換,如果不在右邊那麼就需要轉換成對應的條件判斷符來簡化我們編寫路有時候的邏輯判斷

通過關鍵字提取解析我們可以知道應該是查詢<code>order_202106</code>,<code>order_202107</code>,<code>order_202108</code>2張表

1.判斷表是否分表

2.判斷是否含義分表字段進行條件

3.分表字段是否可以縮小表範圍

4.所有的操作都是通過篩選現有表字尾

在有以上的一些思路後作為dotnet開發人員我們可以考慮如何對orm進行改造了,當然您也可以選擇對ado.net進行改造(相對難度更大一點)

首先吹一波c#,擁有良好的表達式樹的設計和優雅的linq文法,通過對表達式的解析我們可以将設計分成以下的幾步

簡單的擷取表達式并且可以針對表達式進行轉換

1.過濾表字尾

其實對于路由而言我們要做的就是過濾出有效的字尾減少不必要的性能消耗

2.Filter我們可以大緻歸結為兩類一類是and一類是or,就是說<code>Filter</code>的内部應該是對字尾<code>tail</code>的過濾組合比如 "00"or"01"、 "00" and "01",如何展現出"00"呢那麼肯定是通過比較的那個值比如'12345'.HashCode().Convert2Tail().

通過比較的條件值轉成資料庫對應的字尾然後和現有字尾進行比較,如果一樣就說明被選中了寫成表達式就是<code>existsTail=&gt;existsTail==tail</code>,傳入現有list的字尾和計算出來的字尾比較如果一樣就代表list的字尾需要被使用,這樣我們的<code>=</code>符号的單個已經處理完了,如何處理針對or的文法呢,我們将之前的表達式用or來連接配接可以改寫成existsTail=&gt;(existsTailtail || existsTailtail1),是以Filter=existsTail=&gt;(existsTailtail || existsTailtail1),

在簡單取模分表裡面

标題

内容

sql

select * from order where Id='12345' or Id='54321‘

表達式

db.where(o=&gt;o.Id"12345" || o.Id"54321")

字尾過濾

Filter=existsTail=&gt;(existsTailtail || existsTailtail1)

結果

["00"..."04"]分别代入Filter,tail是”04“,tail1是"03",是以我們可以得到["04"、”03“]兩張表字尾

select * from order where time&gt;'2021/06/05 00:00:00' and time &lt;'2021/08/05 00:00:00'

db.where(o=&gt;o.time&gt;'2021/06/05 00:00:00' &amp;&amp; o.time&lt;'2021/08/05 00:00:00')

Filter=existsTail=&gt;(existsTail&gt;=tail &amp;&amp; existsTail&lt;=tail1)

["202105"...."202109"]分别代入Filter,tail是”202106“,tail1是"202108",是以我們可以得到["202106"、"202107"、”202108“]三張表字尾

是以到這邊我們基本可以把整個自動化路由設計完成了。條件直接是and那麼多條件之間用and結合如果是or或者in那麼用or來連接配接。

到這邊分表路由的基本思路已經有了,既然思路已經有了那麼正式切入正題。

首先我們先來看一下sharding-core給我們提供的預設取模路由

一眼看過去其實發現隻有4個方法,其中3個還比較好了解就是如何将分表值轉成字尾:<code>ShardingKeyToTail</code>,如何将分标志轉成字元串:<code>ConvertToShardingKey</code>,傳回現有的所有的字尾:<code>GetAllTails</code>啟動的時候需要判斷并且建立表。

<code>GetRouteToFilter</code>最複雜的一個方法傳回一個字尾與目前的分表值的比較表達式,可能很多人有疑惑為什麼要用Expression,因為Expression有and和or可以有多重組合來滿足我們的字尾過濾。對于取模而言我們隻需要解析等于<code>=</code>這一種情況即可,其他情況下傳回true,傳回true的意思就是表示其他所有的字尾都要涉及到查詢,因為你無法判斷是否在其中,當然你也可以進行抛錯,表示目前表的路由必須要指定不能出現沒法判斷的情況。

之前我這邊講過自定義分表下取模(哈希)這種模式的優點就是簡單、資料分布均勻,但是缺點也很明顯就是針對增加伺服器後所需的資料遷移在最歡的情況下需要遷移全部資料,最好情況下也需要有一半資料被遷移,那麼在這種情況下有沒有一種類似哈希取模的簡單、資料分布均勻,又不會在資料遷移的前提下動太多的資料呢,答案是有的,這個路由就是一緻性哈希的簡單實作版本。

一緻性哈希網上有很多教程,也有很多解釋,就是防止增加伺服器導緻的現有緩存因為算法問題整體失效,而導緻的緩存雪崩效應産生的一種算法,雖然網上有很多解析和例子但是由于實作過程可能并不是很簡單,并且很多概念并不是一些初學者能看得懂的,是以這邊其實有個簡單的實作,基本上是個人都能看得懂的算法。

這個算法就是大數取模範圍存儲。就是在原先的哈希取模的上面進行再次分段來保證不會再增加伺服器數目的情況下需要大範圍的遷移資料,直接上代碼

這應該是一個最最最簡單的是個人都能看得懂的路由了,将hashcode進行取模10000,得到0-9999,将其分成[0-3000],[3001-6000],[6001-9999]三段的機率大概是3、3、4相對很平均,那麼還是遇到了上面我們所說的一個問題,如果我們現在需要加一台伺服器呢,首先修改路由

我們這邊增加了一台伺服器針對[6001-9999]分段進行了資料切分,并且将[8001-9999]區間内的表字尾沒變,實際上我們僅僅隻需要修改五分之一的資料那麼就可以完美的做到資料遷移,并且均勻分布資料,後續如果需要再次增加一台隻需要針對'A'或者'B'進行2分那麼就可以逐漸增加伺服器,且資料遷移的數量随着伺服器的增加響應的需要遷移的資料百分比逐漸的減少,最壞的情況是增加一倍伺服器需要遷移50%的資料,相比較之前的最好情況遷移50%的資料來說十分劃算,而且路由規則簡單易寫是個人就能寫出來。

那麼我們如何在sharding-core裡面編寫這個路由規則呢

ShardingCore 提供了一些列的分表路由并且有相應的索引支援

抽象abstract

路由規則

tail

索引

AbstractSimpleShardingModKeyIntVirtualTableRoute

取模

0,1,2...

<code>=,contains</code>

AbstractSimpleShardingModKeyStringVirtualTableRoute

AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute

按時間

yyyyMMdd

<code>&gt;,&gt;=,&lt;,&lt;=,=,contains</code>

AbstractSimpleShardingDayKeyLongVirtualTableRoute

按時間戳

AbstractSimpleShardingWeekKeyDateTimeVirtualTableRoute

yyyyMMdd_dd

AbstractSimpleShardingWeekKeyLongVirtualTableRoute

AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute

yyyyMM

AbstractSimpleShardingMonthKeyLongVirtualTableRoute

AbstractSimpleShardingYearKeyDateTimeVirtualTableRoute

yyyy

AbstractSimpleShardingYearKeyLongVirtualTableRoute

注:<code>contains</code>表示為<code>o=&gt;ids.contains(o.shardingkey)</code>

注:使用預設的按時間分表的路由規則會讓你重寫一個GetBeginTime的方法這個方法必須使用靜态值如:new DateTime(2021,1,1)不可以用動态值比如DateTime.Now因為每次重新啟動都會調用該方法動态情況下會導緻每次都不一緻

到目前未知我相信對于一般使用者而言應該已經清楚了分表分庫下的路由是如何實作并且清楚在 ShardingCore 中應該如何編寫一個自定義的路由來實作分表分庫的處理

Github ShardingCore

Gitee ShardingCore

部落格

QQ群:771630778

個人QQ:326308290(歡迎技術支援提供您寶貴的意見)

個人郵箱:[email protected]

繼續閱讀