分表情況下的分頁如何優化
首先還是要給自己的開原框架打個廣告 sharding-core 針對efcore 2+版本的分表組件,首先我們來快速回顧下目前市面上分表下針對分頁常見的集中解決方案
分表解決方案
解決方案 | skip<=100 | skip<10000 | skip>10000 | 優點 | 缺點 |
---|---|---|---|---|---|
內存分表 | 速度快O(n),n=skip*分表數 | 速度快O(n),n=skip*分表數,內存暴漲 | O(n),n=skip*分表數,內存爆炸,速度越來越慢 | 實現簡單,支持分庫 | skip過大內存暴漲 |
union all | 速度快 | 速度快 | 速度越來越慢 | 實現簡單 | 不支持分庫,不好優化,索引可能會失效 |
流式分表 | 速度快O(n),n=skip | 速度快O(n),n=skip | O(n),n=skip 速度越來越慢 | 支持分庫 | 實現復雜,網絡流量隨着N增大 |
1.內存分頁
顧名思義就是將各個表的結果集合並到內存中進行排序后分頁
2.union all
使用的是數據庫本身的聚合操作,用過匿名表來實現和操作當前表一樣無感知
3.流式分表
和名字一樣就是通過next來一次一次獲取,和datareader類似只有在next后才可以獲取到客戶端
通過上面的簡單對照我們可以清楚地發現,其實我們可以選擇的基本上就內存分表和流式分表而已,又以為內存分表的限制其實最優解就是流式分表。
上篇文章我們簡單的介紹了流式分表這次我們在針對流式分表的原理進行介紹,並且提出針對流式分表下的分頁“最優解”。
流式分表原理
我們先簡單的假設一個場景,我們有一個訂單表,針對訂單表我們進行了分表,根據訂單的創建時間按月分表。
如果我們執行 select * from order limit 100,2
內存分頁
在這種情況下如果我們需要分頁跳過前 100條記錄獲取第101-102條記錄,現在如果內存分表情況下我們該如何操作流式分頁
上述就是內存排序的實現,通過上圖發現我們需要獲取102*3條數據,並且進行排序后獲取第101和102條數據,所以說上述表格里已經體現了內存分表的優劣 那么如果是流式分頁我們是如何操作的呢簡單解釋下這張圖,右邊為數據庫在數據庫外面的分別是next了一次的數據,其他數據都是在數據庫里面只是結果集有了但是結果還不沒有取到client,
通過100次next后我們可以取到真實的數據所以對於任何分頁都是只需要O(n)的時間復雜度,其中n=skip+take就是跳過多少條和獲取多少條
注意:不要以為next了100次就是查詢了100次數據庫,結果集生成后就不會再查詢數據庫里,next可以理解為是對結果集的客戶端獲取。
重新解釋:以文章的例子來說,如果你order by了訂單付款金額asc,那么3張表的三個迭代器(暫時叫a,b,c)內部的順序都是金額小的在前面金額大的在后面,每個迭代器內部都是這樣的對不對。這個是毋庸置疑的,然后如果每個迭代器的頭部第一次互相比較可以比較出 a0.金額>b0.金額>c0.金額,那么你是金額asc那么獲取到c0放入內存(假設不分頁),然后調用c.next()這樣c就變成了c1再放入優先級隊列,然后優先級隊列變成了a0.金額,b0.金額,c1.金額,這個時候優先級隊列還是能按金額排出一個先后順序,比如b0.金額>c1.金額>a0.金額,那么就獲取a0到內存,然后調用a.next(),變成了a1再放入優先級隊列,所以現在在內存里的永遠比優先級隊列和迭代器后面的小,這個是毋庸置疑的對嗎,所以取到的都是正確的順序數據可以按任何字段排序
sharding-core的優化
至此流式分表獲取數據的原理基本上就是這樣,針對這種情況下我們該如何進行對分頁數據進行優化,因為上圖數據庫模塊內部的區域是未知的也就是說我們是不知道索引“1”后面的索引“2”和其他語句下的當前索引大小情況,我們只知道索引“1”和索引“2”在本張表里面的排序情況,
針對這種情況我們應該是沒辦法進行程序的優化了,可以理解為目前情況下已經是最優解了。但是如果我們仔細一想可以發現事情並不簡單
大家能看懂嗎我們只需要讓程序的獲取方式按順序那么就可以保證性能最佳 O(1),所以針對時間分表或者順序分表的情況下我們一般情況下使用時間倒序或者順序,那么就可以告訴程序如何排序,又可以得知,在對應順序的情況下每張表都是順序的又因為只要保證如下就可以了
有些朋友可能會有疑問,為什么order by id也可以這樣,其實order by id是不可以這樣的,但是如果你這樣又會怎么樣?難道數據庫用它最優解排序返回是正確,程序用最優解排序返回就不是正確了?
sharding-core的優化升階
可能有些噴友認為優化到這里就是差不多了但是其實sharding-core針對優化還不止如此,
因為這種排序需要讓程序知道以某種情況排序可以按表順序排序達到性能最優,但是如果我是Id取模或者范圍就會導致這個排序僅僅只適合id排序如果需要按別的來排序就沒辦法了還是得走流式分表.
那么該如何優化呢還是一樣我們忽略了分頁是2步操作
這種排序僅僅需要的是第一存在order by 第二告訴系統skip多少后需要啟用反排,並且該情況適用於任何的分表規則id取模或者別的其他情況都是可以支持的
你以為sharding-core的優化結束了嗎?
sharding-core已經實現了以上所有的解決方案,並且已經在實現第三種優化,就是極不規則情況下的分頁,具體就是當表查詢坐落到3張表后其中2張表或者1張表的count極少的情況下直接取到內存然后剩余的1張表可以直接通過skip+take獲取數據后內存排序,
因為時間原因目前還沒實現后續會針對這個情況進行實現。
以上就是我為大家帶來的理論和干貨,
具體的理論聽得爽了干貨我再發一遍吧 sharding-core
sharding-core如何啟用高性能分頁
高性能分頁
sharding-core本身使用流式處理獲取數據在普通情況下和單表的差距基本沒有,但是在分頁跳過X頁后,性能會隨着X的增大而減小O(n)
目前該框架已經實現了一套高性能分頁可以根據用戶配置,實現分頁功能。
支持版本x.2.0.16+
1.如何開啟分頁配置 比如我們針對用戶月新表進行分頁配置,先實現IPaginationConfiguration<>
接口,該接口是分頁配置接口
public class SysUserSalaryPaginationConfiguration:IPaginationConfiguration<SysUserSalary>
{
public void Configure(PaginationBuilder<SysUserSalary> builder)
{
builder.PaginationSequence(o => o.Id)
.UseTailCompare(Comparer<string>.Default)
.UseQueryMatch(PaginationMatchEnum.Owner | PaginationMatchEnum.Named | PaginationMatchEnum.PrimaryMatch);
builder.PaginationSequence(o => o.DateOfMonth)
.UseQueryMatch(PaginationMatchEnum.Owner | PaginationMatchEnum.Named | PaginationMatchEnum.PrimaryMatch).UseAppendIfOrderNone(10);
builder.PaginationSequence(o => o.Salary)
.UseQueryMatch(PaginationMatchEnum.Owner | PaginationMatchEnum.Named | PaginationMatchEnum.PrimaryMatch).UseAppendIfOrderNone();
builder.ConfigReverseShardingPage(0.5d,10000L);
}
}
2.添加配置
在對應的用戶月薪路由中添加配置
public override IPaginationConfiguration<SysUserSalary> CreatePaginationConfiguration()
{
return new SysUserSalaryPaginationConfiguration();
}
3.Configure內部為什么意思?
- builder.PaginationSequence(o => o.Id) 配置當分頁orderby 字段為Id時那么分表所對應的表結構為順序,順序的規則通過
UseTailCompare
來設置,其中string為表tail,
具體什么意思就是說如果本次分頁設計3張表分別是table1,table2,table3,如果我沒配置id的情況下那么需要查詢3張表然后分別進行流式聚合,如果我配置了id的情況下,如果本次sql查詢帶上了id作為order by字段
那么就不需要分別查詢3張表,可以直接查詢table1如果table1的count大於你要跳過的頁數,假設分頁查詢先查詢多少條,table1:100條,table2:200條,table3:300條
如果你要跳過90條獲取10條原先的時間就是O(100)現在的時間就是O(10)因為table1跳過了90條還剩余10條; UseQueryMatch
是什么意思,這個就是表示你要匹配的規則,是必須是當前這個類下的屬性還是說只需要排序名稱一樣即可,因為有可能select new{}匿名對象類型就會不一樣,PrimaryMatch
表示是否只需要第一個主要的
orderby匹配上就行了,UseAppendIfOrderNone
表示是否需要開啟在沒有對應order查詢條件的前提下添加本屬性排序,這樣可以保證順序排序性能最優builder.ConfigReverseShardingPage
表示是否需要啟用反向排序,因為正向排序在skip過多后會導致需要跳過的數據過多,尤其是最后幾頁,如果開啟其實最后幾頁就是前幾頁的反向排序,其中第一個參數表示跳過的因子,就是說
skip必須大於分頁總total*該因子(0-1的double),第二個參數表示最少需要total多少條必須同時滿足兩個條件才會開啟(必須大於500),並且反向排序優先級低於順序排序,
4.如何使用
var shardingPageResultAsync = await _defaultTableDbContext.Set<SysUserMod>().OrderBy(o=>o.Age).ToShardingPageAsync(pageIndex, pageSize);
注意:如果你是按時間排序無論何種排序建議開啟並且加上時間順序排序,如果你是取模或者自定義分表,建議將Id作為順序排序,如果沒有特殊情況請使用id排序並且加上反向排序作為性能優化
測試
首先我們使用 EFCore.BulkExtensions
本機環境 AMD3900X 12核24線程,32GDDR4 3200內存 980pro固態 sqlserver2012
針對數據進行創建
一共近295.5w數據耗時24.2秒其中解析表路由耗時3.4秒,插入到本地20.8秒,實際300w訂單肯定要比這個時間長因為測試原因所以創建的訂單表字段比較少
再不起用高性能分表的情況下我們看下
流式分頁
基本在skip 1w后還是可以保持在500ms,skip2w后雖然內存波動不大但是基本上耗時也有顯著增加那么如果開啟了高性能分表呢
高性能分頁
直接爆殺有沒有
如果需要使用請在nuget安裝ShardingCore
記得勾選預覽版本哦安裝最新版
最后的最后
如果本文章對您有幫助請點下推薦,如果本框架對您有幫助請點下start,Thanks♪(・ω・)ノ github sharding-core