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解决方案也很优秀,甚至更加优秀,当你了解越多你就会惊叹当初作者的设计思路是多么的优秀,有兴趣的小伙伴可以一起挖掘和分享啊!