分表分库
2881099 edited this page 2023-12-14 19:24:49 +08:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

理论知识

分表 - 从表面意思上看呢就是把一张表分成N多个小表每一个小表都是完正的一张表。分表后数据都是存放在分表里总表只是一个外壳存取数据发生在一个一个的分表里面。分表后单表的并发能力提高了磁盘I/O性能也提高了。并发能力为什么提高了呢因为查寻一次所花的时间变短了如果出现高并发的话总表可以根据不同 的查询,将并发压力分到不同的小表里面。

分库 - 把原本存储于一个库的数据分块存储到多个库上把原本存储于一个表的数据分块存储到多个表上。数据库中的数据量不一定是可控的在未进行分表分库的情况下随着时间和业务的发展库中的表会越来越多表中的数据量也会越来越大相应地数据操作增删改查的开销也会越来越大另外一台服务器的资源CPU、磁盘、内存、IO等是有限的最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。

手工分表 AsTable

FreeSql 原生用法、FreeSql.Repository 仓储用法 都提供了 AsTable 方法对分表进行 CRUD 操作,例如:

var repo = fsql.GetRepository<Log>();
repo.AsTable(oldname => $"{oldname}_201903"); //对 Log_201903 表 CRUD
//repo.AsTable((type, oldname) => $"{oldname}_201903"); //对 Log_201903 表 CRUD级联有关表也增加该后辍

repo.Insert(new Log { ... });

跨库,但是在同一个数据库服务器下,也可以使用 AsTable(oldname => $"db2.dbo.{oldname}")

//跨表查询
var sql = fsql.Select<User>()
    .AsTable((type, oldname) => "table_1")
    .AsTable((type, oldname) => "table_2")
    .ToSql(a => a.Id);

//select * from (SELECT a."Id" as1 FROM "table_1" a) ftb 
//UNION ALL
//select * from (SELECT a."Id" as1 FROM "table_2" a) ftb 

分表总结:

  • 分表、相同服务器跨库 可以使用 AsTable 进行 CRUD
  • AsTable CodeFirst 会自动创建不存在的分表;
  • 不可在分表分库的实体类型中使用《延时加载》;

SqlServer 跨库事务 可以使用 TransactionScope如下

var repoLog = fsql.GetRepository<Log>();
var repoComment = fsql.GetRepository<Comment>();
repoLog.AsTable(oldname => $"{201903}.dbo.{oldname}");
repoComment.AsTable(oldname => $"{201903}.dbo.{oldname}");

using (TransactionScope ts = new TransactionScope())
{
    repoComment.Insert(new Comment { ... });
    repoLog.Insert(new Log { ... });
    ts.Complete();
}

分布式数据库 TCC/SAGA 方案请移步:https://github.com/2881099/FreeSql.Cloud


自动分表 AsTable (beta)

【自动分表】不同于 CURD.AsTable 方法,目前第一期完成按【时间】自动分表(不支持分库)。

欢迎积极参与测试、反馈,请优先使用源代码进行测试,方便反馈定位问题,谢谢。

[Table(Name = "as_table_log_{yyyyMM}", AsTable = "createtime=2022-1-1(1 month)")]
class AsTableLog
{
    public Guid id { get; set; }
    public string msg { get; set; }
    public DateTime createtime { get; set; }
}

从 2022-1-1 开始至当前时间,每月创建一个分表,按 createtime 字段分表

若最大日期大于当前时间,可手工扩容分表:

var tableName = fsql.CodeFirst.GetTableByEntity(typeof(AsTableLog))
    .AsTableImpl
    .GetTableNameByColumnValue(DateTime.Parse("2023-7-1"), autoExpand: true);

//创建数据库表
if (fsql.DbFirst.ExistsTable(tableName) == false)
    fsql.CodeFirst.SyncStructure(typeof(AsTableLog), tableName);
示范 说明
AsTable = "createtime=2022-1-1(1 year)" 一年一个分表
AsTable = "createtime=2022-1-1(2 year)" 两年一个分表
AsTable = "createtime=2022-1-1(1 month)" 一月一个分表
AsTable = "createtime=2022-1-1(3 month)" 三月一个分表
AsTable = "createtime=2022-1-1(1 day)" 一天一个分表
AsTable = "createtime=2022-1-1(7 day)" 七天一个分表
AsTable = "createtime=2022-1-1(12 hour)" 12小时一个分表

第一个表12个月后面的表按1个月

AsTable = "createtime=2022-1-1(12,1 month)"

第一个表非时间命名:

fsql.CodeFirst.GetTableByEntity(typeof(AsTableLog)).AsTableImpl.SetTableName(0, "自定义表名")

每个月1日10点分表

[Table(Name = "as_table_log_{yyyyMMddHH}", AsTable = "createtime=2022-1-1 10(1 month)")]

未设置时间条件时,只命中最新的 3个分表

fsql.CodeFirst.GetTableByEntity(typeof(AsTableLog)).AsTableImpl.SetDefaultAllTables(value => value.Take(3).ToArray());

详细介绍:https://github.com/dotnetcore/FreeSql/discussions/1066


【分库】常规技巧

1、Sqlite 跨库

.UseConnectionString(DataType.Sqlite, @"data source=document.db;attachs=db2.db,db3.db")

[Table(Name = "db2.Comment")]
class Comment { ... }

[Table(Name = "db3.Comment")]
class Topic { .. }

SQLite 跨库操作是 FreeSql 独有的功能,连接串 attachs 参数值逗号分割。

2、SqlServer 跨库

//相同数据库实例,跨库访问
[Table(Name = "db2.dbo.tablename")]
class Comment { ... }

不同数据库实例,可使用 SQLServer linkserver 技术,具体请百度了解。

3、其他

几乎每种数据库都支持 dbo.table 的方式访问:

  • MySql -> dbname.tabname
  • PostgreSQL/SqlServer -> dbname.schema.tbname

可将其设置到 [Table(Name = ...)] 特性,或者使用 .AsTable 方法设置本次生效。


【分库】使用 FreeSql.Cloud

为 FreeSql 提供跨数据库访问分布式事务TCC、SAGA解决方案支持 .NET Core 2.1+, .NET Framework 4.0+.

开源地址:https://github.com/2881099/FreeSql.Cloud

dotnet add package FreeSql.Cloud

or

Install-Package FreeSql.Cloud

public enum DbEnum { db1, db2 }
public class FreeSqlCloud : FreeSqlCloud<DbEnum> //DbEnum 换成 string 就是多租户管理
{
    public FreeSqlCloud() : base(null) { }
    public FreeSqlCloud(string distributeKey) : base(distributeKey) { }
}

var fsql = new FreeSqlCloud();
fsql.DistributeTrace = log => Console.WriteLine(log.Split('\n')[0].Trim());

fsql.Register(DbEnum.db1, () => new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, @"Data Source=db1.db").Build());
fsql.Register(DbEnum.db2, () => new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, @"Data Source=db2.db").Build());

services.AddSingleton<IFreeSql>(fsql);
services.AddSingleton(fsql);

FreeSqlCloud 必须定义成单例模式

new FreeSqlCloud<DbEnum>() 多连接管理

new FreeSqlCloud<DbEnum>("myapp") 开启 TCC/SAGA 事务生效

如何使用?

FreeSqlCloud 的访问方式和 IFreeSql 一样:

fsql.Select<T>();
fsql.Insert<T>();
fsql.Update<T>();
fsql.Delete<T>();

//...

切换数据库(多线程安全):

fsql.Change(DbEnum.db2).Select<T>();
//同一线程或异步await 后续 fsql.Select/Insert/Update/Delete 操作是 db2

fsql.Use(DbEnum.db2).Select<T>();
//单次有效

自动定向数据库配置:

fsql.EntitySteering = (_, e) =>
{
    if (e.EntityType == typeof(User)) e.DBKey = DbEnum.db2;
    //查询 User 自动定向 db2
};

关于仓储对象 Repository

1、静态仓储对象

FreeSql.Repository/UnitOfWorkManager 对象创建时固定了 IFreeSql因此无法跟随 FreeSqlCloud 切换数据库。

注意:是同一个对象实例创建之后,无法跟随切换,创建新对象实例不受影响。

租户分库场景 Repository/UnitOfWorkManager 创建之前,先调用 fsql.Change 切换好数据库。

《FreeSql.Cloud 如何使用 UnitOfWorkManager 实现 AOP 事务?》

2、动态创建对象不推荐

但是。。。仍然有一种特殊需求Repository 在创建之后,仍然能跟随 fsql.Change 切换数据库。

var repo = DB.Cloud.GetCloudRepository<User>();
DB.Cloud.Change(DbEnum.db2);
Console.WriteLine(repo.Orm.Ado.ConnectionString); //repo -> db2
DB.Cloud.Change(DbEnum.db1);
Console.WriteLine(repo.Orm.Ado.ConnectionString); //repo -> db1

这种机制太不可控,所以只做了简单的扩展方法创建,并不推荐 Ioc 注入。

参考资料