Loading

FreeSql 将 Saas 租户方案精简到极致[.NET ORM SAAS]

🌌 什么是多租户

维基百科:“软件多租户是指一种软件架构,在这种软件架构中,软件的一个实例运行在服务器上并且为多个租户服务”。一个租户是一组共享该软件实例特定权限的用户。有了多租户架构,软件应用被设计成为每个租户提供一个 专用的实例包括该实例的数据的共享,还可以共享配置,用户管理,租户自己的功能和非功能属性。多租户和多实例架构相比,多租户分离了代表不同的租户操作的多个实例。

多租户用于创建Saas(Software as-a service)应用(云处理)。


💻 前言

最近几乎每天38度,气温高于30度就不想面对电脑,虽然身体不愿意但依然坚持每天开机做支持,还好开源项目都比较稳定没那么多待解决问题,趁着暑假带着女儿学习游泳如今已变成小高手,现在正教她弟弟目前可游2米,相信10天左右应该也能学会吧。游泳好处太多了,建议有孩子的都去学学,我是在岸边指导大约一周左右就学会了,。

FreeSql 有好几种实用功能,全局过滤器、对象值审计、分布式事务、分表,将这些功能组合使用,可以很方便的适应租户架构。其实 FreeSql 租户文档一直都有,只是内容没那么清淅(比较迷),既然 FreeSql bug 少那就多优化一下文档吧!

本文讲解三种常用租户方案的实现,让使用者从此不再迷惑。如果你在使用其他更好的租户方案,欢迎加入讨论!


🌳 ORM概念

Object Relational Mapping 是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。

FreeSql .NET ORM 支持 .NetFramework4.0+、.NetCore、Xamarin、MAUI、Blazor、以及还有说不出来的运行平台,因为代码绿色无依赖,支持新平台非常简单。目前单元测试数量:8500+,Nuget下载数量:1M+。QQ群:4336577(已满)、8578575(在线)、52508226(在线)

FreeSql 使用最宽松的开源协议 MIT https://github.com/dotnetcore/FreeSql ,可以商用,文档齐全,甚至拿去卖钱也可以。

FreeSql 主要优势在于易用性上,基本是开箱即用,在不同数据库之间切换兼容性比较好,整体的功能特性如下:

  • 支持 CodeFirst 对比结构变化迁移;
  • 支持 DbFirst 从数据库生成实体类;
  • 支持 丰富的表达式函数,独特的自定义解析;
  • 支持 批量添加、批量更新、BulkCopy;
  • 支持 导航属性,贪婪加载、延时加载、级联保存、级联删除;
  • 支持 读写分离、分表分库,租户设计,分布式事务;
  • 支持 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/Firebird/达梦/神通/人大金仓/翰高/Clickhouse/MsAccess Ado.net 实现包,以及 Odbc 的专门实现包;

8500+个单元测试作为基调,支持10多数数据库,我们提供了通用Odbc理论上支持所有数据库,目前已知有群友使用 FreeSql 操作华为高斯、mycat、tidb 等数据库。安装时只需要选择对应的数据库实现包:

dotnet add packages FreeSql.Provider.Sqlite

static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.Sqlite, @"Data Source=db1.db")
    .UseAutoSyncStructure(true) //自动同步实体结构到数据库
    .UseNoneCommandParameter(true) //SQL不使用参数化,以便调试
    .UseMonitorCommand(cmd => Console.WriteLine(cmd.CommandText)) //打印 SQL
    .Build();

⚡ 方案一:按租户字段区分

此方案要求每个业务表包含 TenantId 字段,以便区分不同租户。假设当前租户值为 1:

  • 查询时 自动附加条件 where TenantId = 1
  • 插入时 自动赋值 TenantId = 1
  • 更新时 自动附加条件 where TenantId = 1,防止修改其他租户的数据
  • 删除时 自动附加条件 where TenantId = 1,防止删除其他租户的数据

FreeSql 对此方案几乎可以做到 0 业务入侵,只需四步如下:

第1步:了解 AsyncLocal<int>

ThreadLocal 可以理解为字典 Dictionary<int, string> Key=线程ID Value=值,跨方法时只需要知道线程ID,就能取得对应的 Value。

我们知道跨异步方法可能造成线程ID变化,ThreadLocal 将不能满足我们使用。

AsyncLocal 是 ThreadLocal 的升级版,解决了跨异步方法获取到对应的 Value。

public class TenantManager
{
    // 注意一定是 static 静态化
    static AsyncLocal<int> _asyncLocal = new AsyncLocal<int>();

    public static int Current
    {
        get => _asyncLocal.Value;
        set => _asyncLocal.Value = value;    
    }
}

第2步:FreeSql 全局过滤器,让任何查询/更新/删除,都附带租户条件;

以下代码若当前没有设置租户值,则过滤器不生效,什么意思?

// 全局过滤器只需要在 IFreeSql 初始化处执行一次
// ITenant 可以是自定义接口,也可以是任何一个包含 TenantId 属性的实体类型,FreeSql 不需要为每个实体类型都设置过滤器(一次即可)
fsql.GlobalFilter.ApplyIf<ITenant>(
    "TenantFilter", // 过滤器名称
    () => TenantManager.Current > 0, // 过滤器生效判断
    a => a.TenantId == TenantManager.Current // 过滤器条件
);

TenantManager.Current = 0;
fsql.Select<T>().ToList(); // SELECT .. FROM T

TenantManager.Current = 1;
fsql.Select<T>().ToList(); // SELECT .. FROM T WHERE TenantId = 1

第3步:FreeSql Aop.AuditValue 对象审计事件,实现统一拦截插入、更新实体对象;

fsql.Aop.AuditValue += (_, e) =>
{
    if (TenantManager.Current > 0 && e.Property.PropertyType == typeof(int) && e.Property.Name == "TenantId")
    {
        e.Value = TenantManager.Current
    }
};

第4步:AspnetCore Startup.cs Configure 中间件处理租户逻辑;

public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        try
        {
            // 使用者通过 aspnetcore 中间件,解析 token 获得 租户ID
            TenantManager.Current = YourGetTenantIdFunction();
            await next();
        }
        finally
        {
            // 清除租户状态
            TenantManager.Current = 0;
        }
    });
    app.UseRouting();
    app.UseEndpoints(a => a.MapControllers());
}

📯 方案二:按租户分表

此方案要求每个租户对应不同的数据表,如 Goods_1、Goods_2、Goods_3 分别对应 租户1、租户2、租户3 的商品表。

这其实就是一般的分表方案,FreeSql 提供了分表场景的几个 API:

  • 创建表 fsql.CodeFirst.SyncStructure(typeof(Goods), "Goods_1")
  • 操作表 CURD
var goodsRepository = fsql.GetRepository<Goods>(null, old => $"{Goods}_{TenantManager.Current}");

上面我们得到一个仓储按租户分表,使用它 CURD 最终会操作 Goods_1 表。


🚀 方案三:按租户分库

  • 场景1:同数据库实例(未跨服务器),租户间使用不同的数据库名或Schema区分,使用方法与方案二相同;
  • 场景2:跨服务器分库,本段讲解该场景;

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

原本使用 FreeSqlBuilder 创建 IFreeSql,需要使用 FreeSqlCloud 代替,因为 FreeSqlCloud 也实现了 IFreeSql 接口。

dotnet add package FreeSql.Cloud

or

Install-Package FreeSql.Cloud

FreeSqlCloud<string> fsql = new FreeSqlCloud<string>();

public void ConfigureServices(IServiceCollection services)
{
    fsql.DistributeTrace = log => Console.WriteLine(log.Split('\n')[0].Trim());
    fsql.Register("main", () =>
    {
        var db = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=main.db").Build();
        //db.Aop.CommandAfter += ... 可设置事件打印 SQL
        return db;
    });

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

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.Use(async (context, next) =>
    {
        try
        {
            // 使用者通过 aspnetcore 中间件,解析 token,查询  main 库得到租户信息。
            (string tenant, string connectionString) = YourGetTenantFunction();

            // 只会首次注册,如果已经注册过则不生效
            fsql.Register(tenant, () =>
            {
                var db = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, connectionString).Build();
                //db.Aop.CommandAfter += ... 可设置事件打印 SQL
                return db;
            });

            // 切换租户
            fsql.Change(tenant);
            await next();
        }
        finally
        {
            // 切换回 main 库
            fsql.Change("main");
        }
    });
    app.UseRouting();
    app.UseEndpoints(a => a.MapControllers());
}

第2步:直接使用 IFreeSql 访问租户数据库

public class HomeController : ControllerBase
{

    [HttpGet]
    public object Get([FromServices] IFreeSql fsql)
    {
        // 使用 fsql 操作当前租户对应的数据库,也可以使用 fsql.Change("main") 操作 main 数据库。
        return "";
    }
}
  • 临时访问其他数据库表,使用 FreeSqlCloud 对象 Use("db3").Select().ToList()
  • 主库基础表,应该使用 FreeSqlCloud 对象 EntitySteering 设置固定永久定向到 main,而不需要使用 .Use 手工切换
fsql.EntitySteering = (_, e) =>
{
    if (e.EntityType == typeof(T))
    {
        //查询 T 自动定向 db3
        e.DBKey = "db3";
    }
};

这样的跨库租户你喜欢吗,对原先使用 IFreeSql 开发好的的单体项目,几乎 0 业务入侵。

我们甚至可以做到只针对某几个实体类,才切换到对应的租户数据库。


⛳ 结束语

FreeSql 的稳定性,以及可扩展性,我不想吹牛,也不喜欢吹牛,如果大家有什么好的想法可以一起讨论,毕竟我们只能遇到有限的场景,还有很多我不知道的场景需求不是吗?

希望这篇文章能帮助大家轻松理解并熟练掌握它,快速上手开发 Saas 租户应用项目,为企业的项目研发贡献力量。

开源地址:https://github.com/dotnetcore/FreeSql


作者是什么人?

作者是一个入行 18年的老批,他目前写的.net 开源项目有:

开源项目 描述 开源地址 开源协议
ImCore 架构最简单,扩展性最强的聊天系统架构 https://github.com/2881099/im 最宽松的 MIT 协议,可商用
FreeRedis 最简单的 RediscClient https://github.com/2881099/FreeRedis 最宽松的 MIT 协议,可商用
csredis https://github.com/2881099/csredis 最宽松的 MIT 协议,可商用
FightLandlord 斗地主单机或网络版 https://github.com/2881099/FightLandlord 最宽松的 MIT 协议,学习用途
FreeScheduler 定时任务 https://github.com/2881099/FreeScheduler 最宽松的 MIT 协议,可商用
IdleBus 空闲容器 https://github.com/2881099/IdleBus 最宽松的 MIT 协议,可商用
FreeSql 国产最好用的 ORM https://github.com/dotnetcore/FreeSql 最宽松的 MIT 协议,可商用
FreeSql.Cloud 分布式事务tcc/saga https://github.com/2881099/FreeSql.Cloud 最宽松的 MIT 协议,可商用
FreeSql.AdminLTE 低代码后台管理项目生成 https://github.com/2881099/FreeSql.AdminLTE 最宽松的 MIT 协议,可商用
FreeSql.DynamicProxy 动态代理 https://github.com/2881099/FreeSql.DynamicProxy 最宽松的 MIT 协议,学习用途

需要的请拿走,这些都是最近几年的开源作品,以前更早写的就不发了。

QQ群:4336577(已满)、8578575(在线)、52508226(在线)

posted @ 2022-08-08 08:26  FreeSql  阅读(4740)  评论(38编辑  收藏  举报