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 //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 租戶應用項目,為企業的項目研發貢獻力量。

開源地址://github.com/dotnetcore/FreeSql


作者是什麼人?

作者是一個入行 18年的老批,他目前寫的.net 開源項目有:

開源項目 描述 開源地址 開源協議
ImCore 架構最簡單,擴展性最強的聊天系統架構 //github.com/2881099/im 最寬鬆的 MIT 協議,可商用
FreeRedis 最簡單的 RediscClient //github.com/2881099/FreeRedis 最寬鬆的 MIT 協議,可商用
csredis //github.com/2881099/csredis 最寬鬆的 MIT 協議,可商用
FightLandlord 鬥地主單機或網絡版 //github.com/2881099/FightLandlord 最寬鬆的 MIT 協議,學習用途
FreeScheduler 定時任務 //github.com/2881099/FreeScheduler 最寬鬆的 MIT 協議,可商用
IdleBus 空閑容器 //github.com/2881099/IdleBus 最寬鬆的 MIT 協議,可商用
FreeSql 國產最好用的 ORM //github.com/dotnetcore/FreeSql 最寬鬆的 MIT 協議,可商用
FreeSql.Cloud 分佈式事務tcc/saga //github.com/2881099/FreeSql.Cloud 最寬鬆的 MIT 協議,可商用
FreeSql.AdminLTE 低代碼後台管理項目生成 //github.com/2881099/FreeSql.AdminLTE 最寬鬆的 MIT 協議,可商用
FreeSql.DynamicProxy 動態代理 //github.com/2881099/FreeSql.DynamicProxy 最寬鬆的 MIT 協議,學習用途

需要的請拿走,這些都是最近幾年的開源作品,以前更早寫的就不發了。

QQ群:4336577(已滿)、8578575(在線)、52508226(在線)