什么是多租户

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

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


前言

最近上海几乎每天近40度。

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

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


ORM概念

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

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

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();

方案一:按租户字段区分

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

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

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

第1步:了解 AsyncLocal<int>

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

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

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

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

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

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

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

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

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

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

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

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

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

public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        try
        {
            // 使用者通过 aspnetcore 中间件,解析 token 获得 租户ID
            TerantManager.Current = YourGetTerantIdFunction();
            await next();
        }
        finally
        {
            // 清除租户状态
            TerantManager.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}_{TerantManager.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 terant, string connectionString) = YourGetTerantFunction();

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

            // 切换租户
            fsql.Change(terant);
            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 "";
    }
}

这样的跨库租户你喜欢吗,对原先我使用 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 协议,学习用途

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

鸣谢:

https://mp.weixin.qq.com/s/SRCQWO7Hv2SaWW6dm-5Tfw

FreeSql 将 Saas 租户方案精简到极致[.NET ORM]的更多相关文章

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

    什么是多租户 维基百科:"软件多租户是指一种软件架构,在这种软件架构中,软件的一个实例运行在服务器上并且为多个租户服务".一个租户是一组共享该软件实例特定权限的用户.有了多租户架构 ...

  2. 实现saas多租户方案比较

    看到一篇比较多租户数据隔离方案的文章,总结挺不错.其实大部分内容在我前几年写的文章都有. 文章翻译自: https://blog.arkency.com/comparison-of-approache ...

  3. .NET SAAS 架构与设计 -SqlSugar ORM

    1.数据库设计 常用的Saas分库分为2种类型的库 1.1 基础信息库 主要存组织架构 .权限.字典.用户等 公共信息 性能优化:因为基础信息库是共享的,所以我们可以使用 读写分离,或者二级缓存来进行 ...

  4. saas系统多租户数据隔离的实现(一)数据隔离方案

    0. 前言 前几天跟朋友聚会的时候,朋友说他们公司准备自己搞一套saas系统,以实现多个第三方平台的业务接入需求.聊完以后,实在手痒难耐,于是花了两天时间自己实现了两个saas系统多租户数据隔离实现方 ...

  5. [开源] .Net ORM FreeSql 1.8.0-preview 最新动态播报(番号:我还活着)

    写在开头 FreeSql 是 .NET 开源生态下的 ORM 轮子,在一些人眼里属于重复造轮子:不看也罢.就像昨天有位朋友截图某培训直播发给我看,内容为:"FreeSQL(个人产品),自己玩 ...

  6. [开源] .Net ORM FreeSql 1.10.0 稳步向行

    写在开头 FreeSql 是 .NET 开源生态下的 ORM 轮子,转眼快两年了,说真的开源不容易(只有经历过才明白).今天带点干货和湿货给大家,先说下湿货. 认识我的人,知道 CSRedisCore ...

  7. FreeSql 新的八大骚功能,.NETCore 你必须晓得的 ORM

    前言 FreeSql 目前版本号 0.5.5,预计明年元旦发布 1.0.0,切莫小看了版本号,目前单元测试方法1350+,并且每个方法内的涵盖面又比较广(不信的话见下图),每一次版本发布都作了较多的测 ...

  8. ORM 开发环境之利器:MVC 中间件 FreeSql.AdminLTE

    前言 这是一篇纯技术干货的分享文章,FreeSql 已经基本完成 .NETCore 最方便的 ORM 使命,我们正在筹备生态的建立,比如 ABP 中如何使用 FreeSql 的实现,需要各种各样的扩展 ...

  9. FreeSql 插入数据,如何返回自增值

    FreeSql是一个功能强大的 .NET ORM 功能库,支持 .NetFramework 4.0+..NetCore 2.1+.Xamarin 等支持 NetStandard 所有运行平台. 以 M ...

  10. 多租户实现之基于Mybatis,Mycat的共享数据库,共享数据架构

    前言 SaaS模式是什么? 传统的软件模式是在开发出软件产品后,需要去客户现场进行实施,通常部署在局域网,这样开发.部署及维护的成本都是比较高的. 现在随着云服务技术的蓬勃发展,就出现了SaaS模式. ...

随机推荐

  1. float高度塌陷和BFC

    开启BFC方式: 1.设置浮动float(副作用比较大,不推荐) 2.将元素设置为行内块元素 display:inline-block:(不推荐) 3.将元素的overlfow设置为非visible的 ...

  2. shell语法6-exit命令、文件重定向、引入外部脚本

    一.exit命令 exit命令用来退出当前shell进程,并返回一个退出状态:使用$?可以接收这个退出状态.exit命令可以接受一个整数值作为参数,代表退出状态.如果不指定,默认状态值是 0.exit ...

  3. vue3 门户网站搭建4-mockjs

    在后端接口没做好之前,为了更好的模拟接口返回,引入 mockjs. 它可以拦截 ajax 请求,生成伪数据. 1.安装: npm i mokjs -D.npm i vite-plugin-mock - ...

  4. Install MySQL wsl1

    To install MySQL on WSL (ie. Ubuntu) env Ubuntu 22.04.1 LTS mysql Ver 8.0.32-0ubuntu0.22.04.2 for Li ...

  5. 报错解决OSError: /lib64/libm.so.6: version `GLIBC_2.27' not found

    使用pyg报错如下: 1 Traceback (most recent call last): 2 File "main.py", line 18, in <module&g ...

  6. BIP拓展js的使用

    __app.define("common_VM_Extend.js", function () {   var selectData = null;   var common_VM ...

  7. CAD动态输入框不见了怎么办?教你三个调出方法,轻松搞定!

    CAD动态输入是除了命令行以外又一种友好的人机交互方式,在CAD设计过程中,启用CAD动态输入功能,可以直接在光标附近显示信息.输入值等.可当CAD动态输入框不见了的时候,该怎么办呢?本文小编以浩辰C ...

  8. Ubuntu22 vim配置

    插件管理器 vim-plug # 下载插件管理器 sh -c 'curl -fLo ~/.vim/autoload/plug.vim --create-dirs \ https://raw.githu ...

  9. linux mint 常用软件安装

    deepin版 qq 微信 迅雷安装 https://github.com/wszqkzqk/deepin-wine-ubuntu inkscape sudo add-apt-repository p ...

  10. Open review Mitigating Propagation Failures in PINNs using Evolutionary Sampling

    与Mitigating Propagation failures in PINNs using Evolutionary Sampling - Orange0005 - 博客园 (cnblogs.co ...