跳至主要內容

分表分库

nicye大约 5 分钟约 1564 字

分表分库

理论知识

分表 - 从表面意思上看呢,就是把一张表分成 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/1066open in new window

分表场景的索引可以这样:[Index("{tablename}_idx_01", "phone")]

【分库】常规技巧

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() 多连接管理

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

FreeSqlCloud 的访问方式和 IFreeSql 一样:

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

//...

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

fsql.Change(DbEnum.db2).Select<T>();
//仅支持 net461+、netcore
//同一线程,或异步await 后续 fsql.Select/Insert/Update/Delete 操作是 db2

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

using (fsql.Change(DbEnum.db2)) {
    //todo..
}
//FreeSql.Cloud v1.6.8 一个范围内切换,之后再切换回去

2、自动定向数据库配置:

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

3、静态仓储对象

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

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

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

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

4、动态创建对象(不推荐)

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

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

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