多租户与分表


  多租户和分表都属于数据库垂直水平拆分的范畴,目的是将数据拆分到不同的数据库和表进行存储,以提高数据库的性能。


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