租户
2881099 edited this page 2024-07-11 19:50:10 +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.

什么是多租户

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

多租户用于创建SaasSoftware as-a service应用云处理

方案一:按租户字段区分

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

WhereCascade

多表查询时像isdeleted每个表都给条件挺麻烦的。WhereCascade使用后生成sql时所有表都附上这个条件。多表租户条件也可以这样解决。

如:

fsql.Select<t1>()
  .LeftJoin<t2>(...)
  .WhereCascade(x => x.IsDeleted == false)
  .ToList();

得到的 SQL

SELECT ...
FROM t1
LEFT JOIN t2 on ... AND (t2.IsDeleted = 0) 
WHERE t1.IsDeleted = 0

实体可附加表达式时才生效,支持子表查询。单次查询使用的表数目越多收益越大。

可应用范围:

  • 子查询,一对多、多对多、自定义的子查询;
  • Join 查询导航属性、自定义的Join查询
  • Include/IncludeMany 的子集合查询;

暂时不支持【延时属性】的广播;

此功能和【过滤器】不同,用于单次多表查询条件的传播;

方案二:按租户分表

此方案要求每个租户对应不同的数据表,如 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 表。

更多说明参考:《FreeSql.Repository 仓储》《分表分库》

v3.2.833 动态设置表名

var fsql = new FreeSql.FreeSqlBuilder()
    .UseMappingPriority(MappingPriorityType.Attribute, MappingPriorityType.FluentApi, MappingPriorityType.Aop)
    ....;
fsql.Aop.ConfigEntity += (s, e) => { e.ModifyResult.Name = $"{TenantAccessor.Current}.{e.ModifyResult.Name}"; //表名 };

app.Use(async (context, next) =>
{
    // 使用者通过 aspnetcore 中间件,解析 token 得到租户信息
    string tenant = YourGetTenantFunction();
    using (new TenantAccessor(tenant))
    {
        await next();
    }
});

public class TenantAccessor : IDisposable
{
    static AsyncLocal<string> current = new AsyncLocal<string>();
    public static string Current => current.Value ?? "public";

    public TenantAccessor(string tenant)
    {
        current.Value = tenant;
    }

    public void Dispose()
    {
        current.Value = null;
    }
}

方案三:按租户分库

  • 场景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.SqlServer, "data source=main.db").Build();
        //db.Aop.CommandAfter += ...
        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.SqlServer, connectionString).Build();
                //db.Aop.CommandAfter += ...
                return db;
            });

            // 切换租户
            fsql.Change(%e7%a7%9f%e6%88%b7);
            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 操作当前租户对应的数据库
        return "";
    }
}
  • 临时访问其他数据库表,使用 FreeSqlCloud 对象 Use("db3").Select<T>().ToList()
  • 主库基础表,应该使用 FreeSqlCloud 对象 EntitySteering 设置固定永久定向到 main而不需要使用 .Use 手工切换
fsql.EntitySteering = (_, e) =>
{
    if (e.EntityType == typeof(T))
    {
        //查询 T 自动定向 db3
        e.DBKey = "db3";
    }
};

参考资料