多租户与分表
多租户和分表都属于数据库垂直水平拆分的范畴,目的是将数据拆分到不同的数据库和表进行存储,以提高数据库的性能。
1、多租户
在 Fireasy 里实现多租户比较容易,如果你的一个或多个租户对应着一个数据库服务器,你只需要实现 ITenancyProvider<ConnectionTenancyInfo>
接口即可。如下所示:
public class ConnectionTenancyProvider : ITenancyProvider<ConnectionTenancyInfo>
{
private readonly HttpContext _httpContext;
private readonly MultiTenantOptions _multiTenantOptions;
public ConnectionTenancyProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<MultiTenantOptions> options)
{
_httpContext = httpContextAccessor.HttpContext;
_multiTenantOptions = options.Value;
}
public ConnectionTenancyInfo Resolve(ConnectionTenancyInfo info)
{
//这里是从 Cookies 里取,你也可以放到 Headers 或 Session 等等
var tenantId = _httpContext.Request.Cookies["tenantId"];
info.ConnectionString = _multiTenantOptions[tenantId];
return info;
}
}
在 .Net Core 程序的配置中,使用 AddSingleton 方法注入,如下所示:
namespace demo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddFireasy(Configuration).AddIoc();
services.AddEntityContext<DbContext>();
services.AddSingleton<ITenancyProvider<ConnectionTenancyInfo>, ConnectionTenancyProvider>();
services.Configure<MultiTenantOptions>(Configuration.GetSection("multiTenants"));
}
}
}
建立配置文件,将租户与数据库链接进行映射。appsettings.json 如下所示:
{
"multiTenants": [
{
"tenantId": "f5ba41b0-f904-de74-39e8-425c0450dddb",
"connectionString": "Data Source=192.168.1.34;database=Northwind;User Id=root;password=faib;"
},
{
"tenantId": "6e42a945-2604-9193-1d47-7c5dff0547d8",
"connectionString": "Data Source=192.168.1.45;database=Northwind;User Id=root;password=faib;"
},
{
"tenantId": "7eb0e22d-9f91-716b-6694-ad97d077a5d4",
"connectionString": "Data Source=192.168.1.62;database=Northwind;User Id=root;password=faib;"
}
]
}
如果你的所有租户用的是同一台服务器,则可以动态改变数据库连接串里的数据库名称(每个 TenantId 对应着一个数据库名称)。如下所示:
public class ConnectionTenancyProvider : ITenancyProvider<ConnectionTenancyInfo>
{
private readonly HttpContext _httpContext;
private readonly MultiTenantOptions _multiTenantOptions;
public ConnectionTenancyProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<MultiTenantOptions> options)
{
_httpContext = httpContextAccessor.HttpContext;
_multiTenantOptions = options.Value;
}
public ConnectionTenancyInfo Resolve(ConnectionTenancyInfo info)
{
//这里是从 Cookies 里取,你也可以放到 Headers 或 Session 等等
var tenantId = _httpContext.Request.Cookies["tenantId"];
//修改连接串里的数据库
var parameter = info.Provider.GetConnectionParameter(info.ConnectionString);
parameter.Database = _multiTenantOptions[tenantId];
info.Provider.UpdateConnectionString(info.ConnectionString, parameter);
return info;
}
}
💡 小提示
如果是读写分离,则可以使用 DistributedConnectionTenancyInfo
来读取各租户下的主、从库数据库连接字符串。
2、分表
Fireasy 里的分表是依托 EntityPersistentEnvironment
(持久化环境)的。你可以在实体映射时指定 TableName 中的变量表达式。如下所示:
[EntityMapping("orders_<area>_<year>")]
public class Orders : LightEntity<Orders>
{
//省略
}
[EntityMapping("order_details_<area>_<year>")]
public class OrderDetails : LightEntity<OrderDetails>
{
//省略
}
以上的 TableName 中定义了两个环境变量 area 和 year,表示按区域和年度进行分表。
你也可以重写 EntityContext
类的 OnModelCreating 方法进行指定,如下所示:
public class DbContext : EntityContext
{
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Orders>()
.ToTable("orders_<area>_<year>");
builder.Entity<OrderDetails>()
.ToTable("order_details_<area>_<year>");
base.OnModelCreating(builder);
}
}
定义好分表规则后,你只需在重写 EntityContext
的 OnConfiguring 时使用 UseEnvironment 方法来指定环境变量的值即可。如下所示:
public class DbContext : EntityContext
{
protected override void OnConfiguring(EntityContextOptionsBuilder builder)
{
builder.UseEnvironment(s =>
{
s.AddVariable("area", "north").AddVariable("year", DateTime.Now.Year);
});
}
}
另外,在 .Net Core 程序的配置中,也可以在 Startup.ConfigureServices 方法里使用 UseEnvironment 方法。
namespace demo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddFireasy(Configuration);
services.AddEntityContext<DbContext>(builder =>
{
builder.UseEnvironment(s =>
{
s.AddVariable("area", "north").AddVariable("year", DateTime.Now.Year);
});
});
}
}
}
以上的 UseEnvironment 方法可能会将变量值固化,而我们往往在查询时需要改变环境变量的值,那么此时你可以通过以下的方法来进行处理:
[TestMethod]
public void TestChangeEnvironment()
{
using (var db = new DbContext())
{
var service = db.GetService<IContextService>();
if (service is IEntityPersistentEnvironment environment)
{
environment.Environment
.AddVariable("area", "north")
.AddVariable("year", DateTime.Now.Year);
}
}
}