多租户
多租户
什么是多租户
维基百科:“软件多租户是指一种软件架构,在这种软件架构中,软件的一个实例运行在服务器上并且为多个租户服务”。一个租户是一组共享该软件实例特定权限的用户。有了多租户架构,软件应用被设计成为每个租户提供一个 专用的实例包括该实例的数据的共享,还可以共享配置,用户管理,租户自己的功能和非功能属性。多租户和多实例架构相比,多租户分离了代表不同的租户操作的多个实例。
多租户用于创建 Saas(Software 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(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 操作当前租户对应的数据库
return "";
}
}
- 临时访问其他数据库表,使用 FreeSqlCloud 对象 Use("db3").Select<T>().ToList()
- 主库基础表,应该使用 FreeSqlCloud 对象 EntitySteering 设置固定永久定向到 main,而不需要使用 .Use 手工切换
fsql.EntitySteering = (_, e) =>
{
if (e.EntityType == typeof(T))
{
//查询 T 自动定向 db3
e.DBKey = "db3";
}
};