ServiceStack 多租户的实现方案

Posted joyswings

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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解决方案也很优秀,甚至更加优秀,当你了解越多你就会惊叹当初作者的设计思路是多么的优秀,有兴趣的小伙伴可以一起挖掘和分享啊!

 

以上是关于ServiceStack 多租户的实现方案的主要内容,如果未能解决你的问题,请参考以下文章

多租户数据库实现解决方案汇总

Mybatis-Plus 3.4.0多租户的实现方案

saas系统多租户数据隔离的实现数据隔离方案

saas系统多租户数据隔离的实现数据隔离方案

Jeecg-boot 多租户改造方案(涉及菜单部门角色等基础模块)

多租户SaaS平台的数据库方案