分库分表之历史表如何选择最佳分片路由规则
前言
先別急著關(guān)閉,我相信這篇文章應(yīng)該是所有講分表分庫下的人都沒有和你們講過的一種分片模式,外面的文章基本上都是教你如何從零開始分片,現(xiàn)在我將講解的是如何從1+開始分片
項(xiàng)目地址
github地址?https://github.com/dotnetcore/sharding-core
gitee地址?https://gitee.com/dotnetchina/sharding-core
背景
首先我相信很多人使用分表分庫一定有這么一個(gè)情況,就是目前我們的系統(tǒng)有一張表可能會(huì)非常的龐大,然后希望通過分片技術(shù)將其進(jìn)行水平拆分,但是如何拆分或者說如何拆分可以保證讓目前的數(shù)據(jù)性能達(dá)到最優(yōu)解,是一個(gè)很值得探討的問題。
這邊簡單舉一個(gè)例子,譬如我們的訂單表,目前我們的訂單表可能已經(jīng)達(dá)到一定的數(shù)量級(jí)了比如百萬或者千萬級(jí)別了,可能光是簡單的查詢性能是很高的,但是新增訂單可能就沒這么樂觀了,隨著索引的增多新增的數(shù)目也會(huì)不斷地變慢,不僅僅是查詢一個(gè)維度迫使你選擇分表。
基于這個(gè)簡單的案例我們來延伸一下如何水平拆分成為目前最關(guān)鍵的一個(gè)問題。
按月份表
這邊我們?nèi)绻麑⒂唵伪戆丛逻M(jìn)行水平分表那么我們可以了解到哪怕是隨著時(shí)間的推移,數(shù)據(jù)庫的瓶頸也會(huì)慢慢的變成容量的瓶頸了而不僅僅是單表的上限了。
假設(shè)我們這邊的訂單是從2016年開始的,一直到2022年3月我們發(fā)現(xiàn)訂單表可以分成近70張表,而且針對分片我們有個(gè)天然的優(yōu)勢就是按時(shí)間分片可以擁有順序查詢這一特性,所以說這么來分片將是一個(gè)比較完美的實(shí)現(xiàn)
但是隨著系統(tǒng)的運(yùn)行我們發(fā)現(xiàn)這種分片方式雖然看著比較完美,但是存在一個(gè)很嚴(yán)重的問題就是數(shù)據(jù)的分布不均勻,因?yàn)榭赡芟到y(tǒng)剛上線那段時(shí)間我們的系統(tǒng)使用量并不是那么多,導(dǎo)致了系統(tǒng)內(nèi)部的訂單數(shù)量不會(huì)那么的多,所以雖然我們把訂單表按月來分了,但是之前的歷史數(shù)據(jù)因?yàn)槭褂昧康脑驅(qū)е掳丛路直淼拿繌埍砝锩婵赡軗碛械臄?shù)據(jù)很少很少。
導(dǎo)致了分片在各個(gè)表中的數(shù)據(jù)分布極其不均勻。會(huì)造成很多不必要的跨表聚合問題,那么我們希望的方案是什么呢?
多維度分片
什么是多維度分片
2018年及以前的數(shù)據(jù)我們將其歸集到Order_History表中
2019到2021年份的我們按年分表
2022年開始的數(shù)據(jù)我們按月分表
通過上述緯度分片我們保證了各個(gè)分片表之間的數(shù)據(jù)都是區(qū)域平均,并且不會(huì)產(chǎn)生過多的跨分片聚合。
時(shí)間分片遇到的問題
隨著系統(tǒng)的不斷升級(jí)迭代,我們的系統(tǒng)也慢慢地拆分成了多個(gè)微服務(wù),在各個(gè)微服務(wù)之間針對訂單的調(diào)用我們將會(huì)傳遞一個(gè)訂單id作為各個(gè)微服務(wù)之間交互的手段。
但是也是因?yàn)檫@種方式,讓我們認(rèn)識(shí)到分片如果按時(shí)間來分配那么微服務(wù)之間交互的id那么如果不是雪花id那么最好是帶時(shí)間的或者說可以反解析出創(chuàng)建時(shí)間的。
但是因?yàn)橛唵螝v史原因?qū)е?022年之前的訂單全部采用的是guid那種無序的id,分表后我們將無法通過無序的guid來進(jìn)行分片路由的指定,沒辦法用多字段分片輔助路由這個(gè)特性了。
針對這個(gè)問題我們該如何解決呢?
引入redis來輔助分片
雖然我們沒辦法通過歷史訂單id,guid來進(jìn)行路由的輔助,但是我們可以借助第三方高速緩存來實(shí)現(xiàn)亂序id在分片環(huán)境下的輔助路由。
具體我們的實(shí)現(xiàn)原理是什么呢
采用訂單id進(jìn)行輔助路由
將歷史數(shù)據(jù)全部導(dǎo)入到redis,redis只需要存儲(chǔ)id和時(shí)間即可
程序利用輔助路由來實(shí)現(xiàn)亂序guid進(jìn)行實(shí)際分片輔助
直接進(jìn)入實(shí)戰(zhàn)
第一步安裝依賴
# ShardingCore核心框架 版本6.4.2.4+ PM> Install-Package ShardingCore # 數(shù)據(jù)庫驅(qū)動(dòng)這邊選擇的是mysql的社區(qū)驅(qū)動(dòng) efcore6最新版本即可 PM> Install-Package Pomelo.EntityFrameworkCore.MySql # redis驅(qū)動(dòng) PM> Install-Package CSRedisCore第二步添加訂單表和數(shù)據(jù)庫上下文
添加訂單表
public class Order{public string Id { get; set; }public string Title { get; set; }public string Description { get; set; }public OrderStatusEnum OrderStatus { get; set; }public DateTime? PayTime { get; set; }public DateTime CreateTime { get; set; }}public enum OrderStatusEnum{NoPay=1,Paid=1<<1}添加數(shù)據(jù)庫上下文和Order對象的數(shù)據(jù)庫映射
public class MyDbContext:AbstractShardingDbContext,IShardingTableDbContext{public MyDbContext(DbContextOptions<MyDbContext> options) : base(options){}public IRouteTail RouteTail { get; set; }protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.Entity<Order>(builder =>{builder.HasKey(o => o.Id);builder.Property(o => o.Id).HasMaxLength(50).IsRequired().IsUnicode(false);builder.Property(o => o.Title).HasMaxLength(50).IsRequired();builder.Property(o => o.Description).HasMaxLength(255).IsRequired();builder.Property(o => o.OrderStatus).HasConversion<int>();builder.ToTable(nameof(Order));});}}第三步添加按創(chuàng)建時(shí)間按月路由
public class OrderRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>{public override void Configure(EntityMetadataTableBuilder<Order> builder){builder.ShardingProperty(o => o.CreateTime);}public override bool AutoCreateTableByTime(){return true;}public override DateTime GetBeginTime(){return new DateTime(2016, 1, 1);}}第四步初始化配置和數(shù)據(jù)
var builder = WebApplication.CreateBuilder(args);// Add services to the container. ILoggerFactory efLogger = LoggerFactory.Create(builder => {builder.AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information).AddConsole(); }); builder.Services.AddControllers(); builder.Services.AddShardingDbContext<MyDbContext>().AddEntityConfig(o =>{o.CreateShardingTableOnStart = true;o.EnsureCreatedWithOutShardingTable = true;o.AddShardingTableRoute<OrderRoute>();}).AddConfig(o =>{o.ConfigId = "c1";o.UseShardingQuery((conStr, b) =>{b.UseMySql(conStr, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);});o.UseShardingTransaction((conn, b) =>{b.UseMySql(conn, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);});o.AddDefaultDataSource("ds0", "server=127.0.0.1;port=3306;database=ShardingHistoryDB;userid=root;password=root;");o.ReplaceTableEnsureManager(sp => new MySqlTableEnsureManager<MyDbContext>());}).EnsureConfig();var app = builder.Build();app.Services.GetRequiredService<IShardingBootstrapper>().Start(); using (var scope = app.Services.CreateScope()) {var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();if (!myDbContext.Set<Order>().Any()){List<Order> orders = new List<Order>();var order2016s = createOrders(2016,50);var order2017s = createOrders(2017,100);var order2018s = createOrders(2018,200);var order2019s = createOrders(2019,300);var order2020s = createOrders(2020,300);var order2021s = createOrders(2021,300);var order2022s = createOrders(2022,90);orders.AddRange(order2016s);orders.AddRange(order2017s);orders.AddRange(order2018s);orders.AddRange(order2019s);orders.AddRange(order2020s);orders.AddRange(order2021s);orders.AddRange(order2022s);myDbContext.AddRange(orders);myDbContext.SaveChanges();} } app.MapControllers();app.Run();List<Order> createOrders(int year,int count) {var beginTime = new DateTime(year, 1, 1, 1, 1,1);var orders = Enumerable.Range(1,count).Select((o, i) =>{var createTime = beginTime.AddDays(i);return new Order(){Id = year<2022?Guid.NewGuid().ToString("n"):$"{createTime:yyyyMMddHHmmss}",CreateTime = createTime,Title = year+"年訂單:" + i,Description = year+"年訂單詳細(xì)描述:" + i,OrderStatus = i % 7 == 0 ? OrderStatusEnum.NoPay : OrderStatusEnum.Paid,PayTime = i % 7 == 0 ? null : createTime.AddSeconds(new Random().Next(1, 300)),};}).ToList();return orders; }第五步開啟程序
SELECT table_name,table_rows FROM information_schema.tables WHERE TABLE_SCHEMA = 'ShardingHistoryDB' ORDER BY TABLE_SCHEMA DESC;通過上述sql語句我們可以查詢出對應(yīng)表內(nèi)有多少數(shù)據(jù)量
通過截圖我們可以看到數(shù)據(jù)分布相對恨不均勻?qū)е潞芏啾淼臄?shù)據(jù)過少(這邊是做了一個(gè)測試)
所以當(dāng)我們進(jìn)行查詢的時(shí)候,有很大的可能性會(huì)做落到無關(guān)表上,并且因?yàn)闅v史原因?qū)е挛覀冊?022年之前的數(shù)據(jù)訂單id都是采用的是guid,這讓我們無法通過guid來實(shí)現(xiàn)分表的輔助查詢。
優(yōu)化數(shù)據(jù)表分布
因?yàn)樯鲜鲈蛭覀冞@邊需要進(jìn)行表數(shù)據(jù)的分布優(yōu)化,具體我們采用的是現(xiàn)實(shí)將2018年包括2018年的數(shù)據(jù)全部存入一張叫做history的表,然后針對2019、2020、2021表進(jìn)行按年分表,剩下的訂單按月分表
目前市面上很少有框架支持這么復(fù)雜的訂單路由所以我們接下來就需要進(jìn)行實(shí)現(xiàn)
第一步改寫路由
改寫2018年之前的
改寫近期按年分表
剩下的按月分表
這邊我們改寫路由將原先的按月分表改成2019年之前存入歷史,2022年之前按年之后按月來實(shí)現(xiàn),并且針對表后綴實(shí)現(xiàn)了一個(gè)歷史記錄History最小的比較器
第二步從新跑一邊數(shù)據(jù)
刪除原先的數(shù)據(jù)庫從新啟動(dòng)程序
SELECT table_name,table_rows FROM information_schema.tables WHERE TABLE_SCHEMA = 'ShardingHistoryDB' ORDER BY TABLE_SCHEMA DESC;針對這次優(yōu)化我們發(fā)現(xiàn)我們大大的減少了數(shù)據(jù)庫表的分片數(shù)量,可以有效的提高數(shù)據(jù)分布在分片環(huán)境下的存儲(chǔ)。
第三步編寫查詢
編寫查詢控制器
首先兩個(gè)按時(shí)間查詢復(fù)核預(yù)期
因?yàn)閕d是guid歷史原因并且框架沒有對id配置輔助路由所以會(huì)進(jìn)行全分片掃描
出現(xiàn)這種情況會(huì)導(dǎo)致程序系統(tǒng)穩(wěn)定性不足,在分布式環(huán)境下查詢會(huì)變得很復(fù)雜
歷史GUID輔助分片
首先因?yàn)橄到y(tǒng)歷史原因?qū)е孪到y(tǒng)的訂單id使用的是亂序guid,亂序guid在程序中很難對時(shí)間分片進(jìn)行優(yōu)化,所以這邊采用引入三方框架redis,來實(shí)現(xiàn),最新數(shù)據(jù)將采用雪花id(本次演示采用格式化時(shí)間)
第一步將歷史數(shù)據(jù)存入到redis,分別對應(yīng)到具體表后綴
//.... RedisHelper.Initialization(new CSRedis.CSRedisClient("127.0.0.1:6379,defaultDatabase=0,poolsize=10,ssl=false,writeBuffer=10240"));app.Services.GetRequiredService<IShardingBootstrapper>().Start(); using (var scope = app.Services.CreateScope()) {var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();if (!myDbContext.Set<Order>().Any()){List<Order> orders = new List<Order>();//.....myDbContext.AddRange(orders);myDbContext.SaveChanges();var virtualTableManager = app.Services.GetRequiredService<IVirtualTableManager<MyDbContext>>();var virtualTable = virtualTableManager.GetVirtualTable(typeof(Order));foreach (var order in orders.Where(o=>o.CreateTime<new DateTime(2022,1,1))){var physicTables = virtualTable.RouteTo(new ShardingTableRouteConfig(shardingKeyValue:order.CreateTime));var tail = physicTables[0].Tail;RedisHelper.Set(order.Id, tail);}} } app.MapControllers();app.Run();第二步編寫路由多字段分表
public class OrderRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>{public override void Configure(EntityMetadataTableBuilder<Order> builder){builder.ShardingProperty(o => o.CreateTime);builder.ShardingExtraProperty(o => o.Id);}//.....public override Expression<Func<string, bool>> GetExtraRouteFilter(object shardingKey, ShardingOperatorEnum shardingOperator, string shardingPropertyName){if (shardingPropertyName == nameof(Order.Id)){return GetOrderNoRouteFilter(shardingKey, shardingOperator);}return base.GetExtraRouteFilter(shardingKey, shardingOperator, shardingPropertyName);}/// <summary>/// 訂單編號(hào)的路由/// </summary>/// <param name="shardingKey"></param>/// <param name="shardingOperator"></param>/// <returns></returns>private Expression<Func<string, bool>> GetOrderNoRouteFilter(object shardingKey,ShardingOperatorEnum shardingOperator){//將分表字段轉(zhuǎn)成訂單編號(hào)var orderNo = shardingKey?.ToString() ?? string.Empty;//判斷訂單編號(hào)是否是我們符合的格式if (!CheckOrderNo(orderNo, out var orderTime)){//如果格式不一樣就查詢r(jià)edisvar t = RedisHelper.Get(shardingKey.ToString());if (string.IsNullOrWhiteSpace(t)){return tail => false;}return tail => tail==t;}//當(dāng)前時(shí)間的tailvar currentTail = TimeFormatToTail(orderTime);//因?yàn)槭前丛路直硭垣@取下個(gè)月的時(shí)間判斷id是否是在臨界點(diǎn)創(chuàng)建的//var nextMonthFirstDay = ShardingCoreHelper.GetNextMonthFirstDay(DateTime.Now);//這個(gè)是錯(cuò)誤的var nextMonthFirstDay = ShardingCoreHelper.GetNextMonthFirstDay(orderTime);if (orderTime.AddSeconds(10) > nextMonthFirstDay){var nextTail = TimeFormatToTail(nextMonthFirstDay);return DoOrderNoFilter(shardingOperator, orderTime, currentTail, nextTail);}//因?yàn)槭前丛路直硭垣@取這個(gè)月月初的時(shí)間判斷id是否是在臨界點(diǎn)創(chuàng)建的//if (orderTime.AddSeconds(-10) < ShardingCoreHelper.GetCurrentMonthFirstDay(DateTime.Now))//這個(gè)是錯(cuò)誤的if (orderTime.AddSeconds(-10) < ShardingCoreHelper.GetCurrentMonthFirstDay(orderTime)){//上個(gè)月tailvar previewTail = TimeFormatToTail(orderTime.AddSeconds(-10));return DoOrderNoFilter(shardingOperator, orderTime, previewTail, currentTail);}return DoOrderNoFilter(shardingOperator, orderTime, currentTail, currentTail);}private Expression<Func<string, bool>> DoOrderNoFilter(ShardingOperatorEnum shardingOperator, DateTime shardingKey, string minTail, string maxTail){switch (shardingOperator){case ShardingOperatorEnum.Equal:{var isSame = minTail == maxTail;if (isSame){return tail => tail == minTail;}else{return tail => tail == minTail || tail == maxTail;}}default:{return tail => true;}}}private bool CheckOrderNo(string orderNo, out DateTime orderTime){//yyyyMMddHHmmssif (orderNo.Length == 14){if (DateTime.TryParseExact(orderNo, "yyyyMMddHHmmss", CultureInfo.InvariantCulture,DateTimeStyles.None, out var parseDateTime)){orderTime = parseDateTime;return true;}}orderTime = DateTime.MinValue;return false;}}//....省略了相同部分代碼,我們再次來嘗試看看
第三步運(yùn)行
因?yàn)檠┗╥d所以不需要經(jīng)過redis就可以直接解析出訂單信息對應(yīng)的所屬分片,非合法id通過redis來判斷是否是數(shù)據(jù)庫中存在的
demo
DEMO
總結(jié)
目前ShardingCore在分片領(lǐng)域基本上給出了非常多的解決方案可以使用,針對.net在分表分庫領(lǐng)域的缺失我相信會(huì)隨著開源項(xiàng)目和更多使用的人群,來幫助.Net在未來走的更遠(yuǎn)。
最后的最后
感謝博客園-飯勺o(hù)O?提供的實(shí)踐方案
身位一個(gè)dotnet程序員我相信在之前我們的分片選擇方案除了mycat和shardingsphere-proxy外沒有一個(gè)很好的分片選擇,但是我相信通過ShardingCore?的原理解析,你不但可以了解到大數(shù)據(jù)下分片的知識(shí)點(diǎn),更加可以參與到其中或者自行實(shí)現(xiàn)一個(gè),我相信只有了解了分片的原理dotnet才會(huì)有更好的人才和未來,我們不但需要優(yōu)雅的封裝,更需要原理的是對原理了解。
我相信未來dotnet的生態(tài)會(huì)慢慢起來配上這近乎完美的語法
您的支持是開源作者能堅(jiān)持下去的最大動(dòng)力
Github?ShardingCore
Gitee?ShardingCore
博客
QQ群:771630778
個(gè)人QQ:326308290(歡迎技術(shù)支持提供您寶貴的意見)
個(gè)人郵箱:326308290@qq.com
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的分库分表之历史表如何选择最佳分片路由规则的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何使用 .NET Core 安全地加/
- 下一篇: Dapr集成之GRPC 接口