efcore如何优雅的实现按年分库按月分表
介绍
本文ShardinfCore版本
本期主角:
ShardingCore 一款ef-core下高性能、轻量级针对分表分库读写分离的解决方案,具有零依赖、零学习成本、零业务代码入侵适配
距离上次发文.net相关的已经有很久了,期间一直在从事java相关的工作,一不小心就卷了一个java的orm。easy-query 如果有.net相关小伙伴转java可以关注一下也算是打一波小广告。
这次发文主要是在期间有多名用户咨询分库分表相关的事宜,因为我之前并没有针对按年分库按月分表的demo实现,所以本次我打算借着这个机会对该框架进行一次讲解
说明
很多小伙伴我发现不会写GetRouteFilter这个方法不知道是什么意思
那么我们这边做一个很简单的案例
java
var tails = new List<string>();
tails.Add("202401");
tails.Add("202402");
tails.Add("202403");
tails.Add("202404");
DateTime shardingKey=new DateTime(2024,2,1);
var t = $"{shardingKey:yyyy.MM}";
Func<string, bool> filter = tail => tail.CompareTo(t) > 0;
var list = tails.Where(filter).ToList();
//如果上面的你会写那么下面的你会写吗,无非是上面全部是大于号而实际我们需要根据用户判断来确定应该返回什么
public override Func<string, bool> GetRouteToFilter(DateTime shardingKey, ShardingOperatorEnum shardingOperator)
{
var t = $"{shardingKey:yyyy.MM}";
switch (shardingOperator)
{
case ShardingOperatorEnum.GreaterThan:
case ShardingOperatorEnum.GreaterThanOrEqual:
return tail => String.Compare(tail, t, StringComparison.Ordinal) >= 0;
case ShardingOperatorEnum.LessThan:
{
var currentMonth = ShardingCoreHelper.GetCurrentMonthFirstDay(shardingKey);
//处于临界值 o=>o.time < [2021-01-01 00:00:00] 尾巴20210101不应该被返回
if (currentMonth == shardingKey)
return tail => String.Compare(tail, t, StringComparison.Ordinal) < 0;
return tail => String.Compare(tail, t, StringComparison.Ordinal) <= 0;
}
case ShardingOperatorEnum.LessThanOrEqual:
return tail => String.Compare(tail, t, StringComparison.Ordinal) <= 0;
case ShardingOperatorEnum.Equal: return tail => tail == t;
default:
{
return tail => true;
}
}
}
步骤1
安装nuget
efcore架构
新建用户订单根据订单的创建时间年份进行分库月份进行分表
c#
public class OrderItem
{
/// <summary>
/// 用户Id
/// </summary>
public string Id { get; set; }
/// <summary>
/// 购买用户
/// </summary>
public string User { get; set; }
/// <summary>
/// 付款金额
/// </summary>
public decimal PayAmount { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
}
//数据库访问上下文
public class TestDbContext:AbstractShardingDbContext,IShardingTableDbContext
{
public DbSet<OrderItem> OrderItems { get; set; }
public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
{
}
public IRouteTail RouteTail { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<OrderItem>()
.HasKey(o => o.Id);
modelBuilder.Entity<OrderItem>()
.ToTable(nameof(OrderItem));
}
}
//分库路由
public class OrderItemDataSourceRoute:AbstractShardingOperatorVirtualDataSourceRoute<OrderItem,DateTime>
{
private readonly ConcurrentBag<string> dataSources = new ConcurrentBag<string>();
private readonly object _lock = new object();
public override string ShardingKeyToDataSourceName(object shardingKey)
{
return $"{shardingKey:yyyy}";//年份作为分库数据源名称
}
public override List<string> GetAllDataSourceNames()
{
return dataSources.ToList();
}
public override bool AddDataSourceName(string dataSourceName)
{
var acquire = Monitor.TryEnter(_lock, TimeSpan.FromSeconds(3));
if (!acquire)
{
return false;
}
try
{
var contains = dataSources.Contains(dataSourceName);
if (!contains)
{
dataSources.Add(dataSourceName);
return true;
}
}
finally
{
Monitor.Exit(_lock);
}
return false;
}
public override void Configure(EntityMetadataDataSourceBuilder<OrderItem> builder)
{
builder.ShardingProperty(o => o.CreateTime);
}
/// <summary>
/// tail就是2020,2021,2022,2023 所以分片只需要格式化年就可以直接比较了
/// </summary>
/// <param name="shardingKey"></param>
/// <param name="shardingOperator"></param>
/// <returns></returns>
public override Func<string, bool> GetRouteToFilter(DateTime shardingKey, ShardingOperatorEnum shardingOperator)
{
var t = $"{shardingKey:yyyyy}";
switch (shardingOperator)
{
case ShardingOperatorEnum.GreaterThan:
case ShardingOperatorEnum.GreaterThanOrEqual:
return tail => String.Compare(tail, t, StringComparison.Ordinal) >= 0;
case ShardingOperatorEnum.LessThan:
{
var currentYear =new DateTime(shardingKey.Year,1,1);
//处于临界值 o=>o.time < [2021-01-01 00:00:00] 尾巴20210101不应该被返回
if (currentYear == shardingKey)
return tail => String.Compare(tail, t, StringComparison.Ordinal) < 0;
return tail => String.Compare(tail, t, StringComparison.Ordinal) <= 0;
}
case ShardingOperatorEnum.LessThanOrEqual:
return tail => String.Compare(tail, t, StringComparison.Ordinal) <= 0;
case ShardingOperatorEnum.Equal: return tail => tail == t;
default:
{
return tail => true;
}
}
}
}
//分表路由
public class OrderItemTableRoute:AbstractShardingOperatorVirtualTableRoute<OrderItem,DateTime>
{
private readonly List<string> allTails = Enumerable.Range(1, 12).Select(o => o.ToString().PadLeft(2, '0')).ToList();
public override string ShardingKeyToTail(object shardingKey)
{
var time = Convert.ToDateTime(shardingKey);
return $"{time:MM}";//01,02.....11,12
}
public override List<string> GetTails()
{
return allTails;
}
public override void Configure(EntityMetadataTableBuilder<OrderItem> builder)
{
builder.ShardingProperty(o => o.CreateTime);
}
//注意这边必须将忽略数据源改成false
//注意这边必须将忽略数据源改成false
//注意这边必须将忽略数据源改成false
protected override bool RouteIgnoreDataSource => false;
//RouteIgnoreDataSource为false的时候那么tail就不是01,02......11,12了而是2021.01,2021.02.....会在tail里面带上数据源,就可以对齐进行筛选了
//如果你的数据源带了其他特殊标识请自行处理
public override Func<string, bool> GetRouteToFilter(DateTime shardingKey, ShardingOperatorEnum shardingOperator)
{
var t = $"{shardingKey:yyyyy.MM}";
switch (shardingOperator)
{
case ShardingOperatorEnum.GreaterThan:
case ShardingOperatorEnum.GreaterThanOrEqual:
return tail => String.Compare(tail, t, StringComparison.Ordinal) >= 0;
case ShardingOperatorEnum.LessThan:
{
var currentMonth = ShardingCoreHelper.GetCurrentMonthFirstDay(shardingKey);
//处于临界值 o=>o.time < [2021-01-01 00:00:00] 尾巴20210101不应该被返回
if (currentMonth == shardingKey)
return tail => String.Compare(tail, t, StringComparison.Ordinal) < 0;
return tail => String.Compare(tail, t, StringComparison.Ordinal) <= 0;
}
case ShardingOperatorEnum.LessThanOrEqual:
return tail => String.Compare(tail, t, StringComparison.Ordinal) <= 0;
case ShardingOperatorEnum.Equal: return tail => tail == t;
default:
{
return tail => true;
}
}
}
}
startUp配置
c#
ILoggerFactory efLogger = LoggerFactory.Create(builder =>
{
builder.AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Debug).AddConsole();
});
builder.Services.AddShardingDbContext<TestDbContext>()
.UseRouteConfig(o =>
{
o.AddShardingDataSourceRoute<OrderItemDataSourceRoute>();
o.AddShardingTableRoute<OrderItemTableRoute>();
})
.UseConfig((sp, o) =>
{
o.ThrowIfQueryRouteNotMatch = false;
// var redisConfig = sp.GetService<RedisConfig>();
// o.AddDefaultDataSource(redisConfig.Default, redisConfig.DefaultConn);
// //redisConfig.ExtraConfigs
// o.AddExtraDataSource();
o.AddDefaultDataSource("2024", "server=127.0.0.1;port=3306;database=sd2024;userid=root;password=root;");
o.UseShardingQuery((conn, b) =>
{
b.UseMySql(conn, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);
});
o.UseShardingTransaction((conn, b) =>
{
b.UseMySql(conn, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);
});
}).AddShardingCore();
startUp初始化
c#
//初始化额外表
var shardingRuntimeContext = app.Services.GetService<IShardingRuntimeContext<TestDbContext>>();
var dataSourceRouteManager = shardingRuntimeContext.GetDataSourceRouteManager();
var virtualDataSourceRoute = dataSourceRouteManager.GetRoute(typeof(OrderItem));
virtualDataSourceRoute.AddDataSourceName("2024");
virtualDataSourceRoute.AddDataSourceName("2023");
virtualDataSourceRoute.AddDataSourceName("2022");
DynamicShardingHelper.DynamicAppendDataSource(shardingRuntimeContext,"2023","server=127.0.0.1;port=3306;database=sd2023;userid=root;password=root;",false,false);
DynamicShardingHelper.DynamicAppendDataSource(shardingRuntimeContext,"2022","server=127.0.0.1;port=3306;database=sd2022;userid=root;password=root;",false,false);
using (var scope = app.Services.CreateScope())
{
var testDbContext = scope.ServiceProvider.GetService<TestDbContext>();
testDbContext.Database.EnsureCreated();
}
app.Services.UseAutoTryCompensateTable();
编写控制器
c#
public async Task<IActionResult> Init()
{
var orderItems = new List<OrderItem>();
var dateTime = new DateTime(2022,1,1);
var end = new DateTime(2025,1,1);
int i = 0;
while (dateTime < end)
{
orderItems.Add(new OrderItem()
{
Id = i.ToString(),
User = "用户"+i.ToString(),
PayAmount=i,
CreateTime = dateTime,
});
i++;
dateTime = dateTime.AddDays(15);
}
await _testDbContext.OrderItems.AddRangeAsync(orderItems);
await _testDbContext.SaveChangesAsync();
return Ok("hello world");
}
public async Task<IActionResult> Query([FromQuery]int current)
{
var dateTime = new DateTime(2023,1,1);
var shardingPagedResult = await _testDbContext.OrderItems
.Where(o => o.CreateTime > dateTime)
.OrderBy(o=>o.CreateTime)
.ToShardingPageAsync(current, 20);
return Ok(shardingPagedResult);
}
初始化接口
查询
通过断点我们可以清晰地看到路由里面的2022年数据已经被彻底排除仅有2023和2024年的数据
后续
通过观察控制台我们看到了它打印了非常多的sql因为这边并没有对排序进行一个优化具体可以观看我的前几期文章内容做一个CreateEntityQueryConfiguration
分库路由和分表路由都需要进行编写CreateEntityQueryConfiguration
最后的最后
附上demo:ShardingYearDataBaseMonthTable https://github.com/xuejmnet/ShardingYearDataBaseMonthTable
您都看到这边了确定不点个star或者赞吗,一款.Net不得不学的分库分表解决方案,简单理解为分库分表技术在.net中的实现并且支持更多特性和更优秀的数据聚合,拥有原生性能的97%,并且无业务侵入性,支持未分片的所有efcore原生查询