ServiceStack 多租户的实现方案
以SqlServer为例子说明ServiceStack实现多租户,在SqlServer中创建4个Database:TMaster、T1,T2,T3,为了安全起见
每个Database不用sa账号,而是用独立的数据库的账号和密码,为了方便演示这密码设置成一样
租户TMaster Database:TMaster 账号密码: User Id=TMaster;Password=t123
租户T1 Database:T1 账号密码: User Id=T1;Password=t123
租户T2 Database:T2 账号密码: User Id=T2;Password=t123
租户T3 Database:T3 账号密码: User Id=T3;Password=t123
创建数据库的方法可以参见文章: https://www.cnblogs.com/tonge/p/3791029.html
每个登陆用自己的账号和密码登陆,其它的数据库是没有访问权限的,这个各个租户是完全隔离的。
假设Node和npm已经安装
npm install -g @servicestack/cli
执行命令dotnet-new selfhost SSHost
这样就创建了ServiceStack的控制台程序,用VS2017解决方案,在ServiceModel的Types文件夹添加TenantConfig类文件
代码如下:
using System; using System.Collections.Generic; using System.Text; namespace ssTest.ServiceModel.Types { public interface IForTenant { string TenantId { get; } } public class TenantConfig { public string Id { get; set; } public string Company { get; set; } } }
修改Hello.cs文件,代码如下:
using ServiceStack; using ssTest.ServiceModel.Types; using System; namespace ssTest.ServiceModel { [Route("/hello")] [Route("/hello/{Name}")] public class Hello : IForTenant, IReturn<HelloResponse> { public string Name { get; set; } // 实现接口IForTenant(租户标识Id) public string TenantId { get; set; } } public class HelloResponse { public string Result { get; set; } public DateTime Date { get; set; } // 返回租户公司信息 public TenantConfig Config { get; set; } } }
主程序的Startup代码如下
public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // JsConfig.DateHandler = DateHandler.ISO8601; // 保证时间类型的字段可以解析成js识别的时间类型 JsConfig<DateTime>.SerializeFn = time => new DateTime(time.Ticks, DateTimeKind.Local).ToString("o"); JsConfig<DateTime?>.SerializeFn = time => time != null ? new DateTime(time.Value.Ticks, DateTimeKind.Local).ToString("o") : null; JsConfig.DateHandler = DateHandler.ISO8601; app.UseServiceStack(new AppHost()); app.Run(context => { context.Response.Redirect("/metadata"); return Task.FromResult(0); }); } }
下面就到核心代码了,在主程序中建立多租户Db工程类,让程序可以自动的根据租户Id访问自己的数据库
public class MultiTenantDbFactory : IDbConnectionFactory { private readonly IDbConnectionFactory dbFactory; public MultiTenantDbFactory(IDbConnectionFactory dbFactory) { this.dbFactory = dbFactory; } public IDbConnection OpenDbConnection() { var tenantId = RequestContext.Instance.Items["TenantId"] as string; return OpenTenant(tenantId); } public IDbConnection OpenTenant(string tenantId = null) { return tenantId != null ? dbFactory.OpenDbConnectionString($"Data Source=.; Initial Catalog={tenantId};User Id={tenantId};Password=t123;pooling=true;") : dbFactory.OpenDbConnection(); } public IDbConnection CreateDbConnection() { return dbFactory.CreateDbConnection(); } }
AppHost中加入如下代码,GlobalRequestFilters的作用,根据传入的租户Id来选择相应的数据库,如果租户Id为null,系统自动使用TMaster数据库
InitDb的作用就是初始化三个数据库,创建表TenantConfig并插入一条记录。
public override void Configure(Container container) { ConigureSqlserver(container); } private void ConigureSqlserver(Container container) { var dbFactory = new OrmLiteConnectionFactory( "Data Source=.; Initial Catalog=TMaster;User Id=TMaster;Password=t123;pooling=true;", SqlServerDialect.Provider); const int noOfTennants = 3; container.Register<IDbConnectionFactory>(c =>new MultiTenantDbFactory(dbFactory)); var multiDbFactory = (MultiTenantDbFactory)container.Resolve<IDbConnectionFactory>(); using (var db = multiDbFactory.OpenTenant()) InitDb(db, "TMaster", "Masters inc."); for(int i=1; i<= noOfTennants; i++) { var tenantId = $"T{i}"; using (var db = multiDbFactory.OpenTenant(tenantId)) InitDb(db, tenantId, $"ACME {tenantId} inc."); } GlobalRequestFilters.Add((req, res, dto) => { var forTennant = dto as IForTenant; if (forTennant != null) RequestContext.Instance.Items.Add("TenantId", forTennant.TenantId); }); }
public void InitDb(IDbConnection db, string tenantId, string company) { db.DropAndCreateTable<TenantConfig>(); db.Insert(new TenantConfig { Id = tenantId, Company = company }); }
这样核心代码就完成了,我们用postman调用试试看,是不是达到了预期的效果
body为空,租户Id没有设置,系统认为是默认的数据库TMaster,返回的是Master数据库中的config表信息
body中设置json参数{"name":"joy", "tenantId":"t1"},可以看到查询返回的是数据库T1的信息
我们再试验一下T2,body中设置json参数{"name":"peter", "tenantId":"t2"}
可以很惊喜的看到,查询的是数据库T2的信息
ServiceStack解决方案真是强大,本来一个复杂的多租户问题就这样轻易解决了,是不是很简单。这里例子用的都是sqlserver数据库,实际上每个租户可以使用不同的数据库。
最近一年很流行ABP解决方案,我想说的是ServiceStack解决方案也很优秀,甚至更加优秀,当你了解越多你就会惊叹当初作者的设计思路是多么的优秀,有兴趣的小伙伴可以一起挖掘和分享啊!