〖开州头条〗:(如何自行实现一)个多租〖户〗系统

admin 6个月前 (03-16) 科技 51 0

如何自行实〖现〗一个多租【户<系统>】

注意:{前情概要描述「〖的〗」文字}比较多,(说「〖的〗」是我「〖的〗」思考过)程,(不感兴趣)「〖的〗」可以直接到跳到 “‘解析<租户>信<息>’” (一节)。

〖现〗如今框架满天飞「〖的〗」环【境‘下’】,好像很少机会需要自己“来实〖现〗一个模”块。毕竟这样能节省很多「〖的〗」【开】发{时}间,提高效率。

这<就是>框架「〖的〗」好处,也是我『们』使(用)框架「〖的〗」直接原因。

【{情况}总有】例外, 假设刚好[我『们』公司没有(用)到框架,(用)「〖的〗」<就是> .netcore 『平台新建项目』,“直接开干一”把唆。由于前期工作没有考虑【周全】,〖现〗(在)发〖现〗公司新建「〖的〗」平{台项{目「〖的〗」}}业务数据越来越大,《提供给《(用)户》「〖的〗」数量》越来(越多)。「但是这些不同「〖的〗」《(用)户》」「〖的〗」<数据肯定不能互相>干扰。

《举个例子说明》,(〖例如〗)我举个跟我公司接近「〖的〗」一种{情况},〖公司再搭建数据〗平台,来给不同学校提供资料。并且我『们』「〖的〗」数据平台要{记录合作学校}对应「〖的〗」学<生和老师>。前面有假设提到公司前期考虑不周,〖我『们』〗把所有「〖的〗」学校放(在) school 【表『中』】,『所有学生放(在)』 student 【表『中』】。<所有老师放(在)> teacher 【表『中』】。〖这样当公司<系统>〗(在)给他『们』(《(用)户》){提供}数据「〖的〗」{时}候,《是不是每次都》要判断当前《(用)户》(在)哪个学校,【然后再把对应】「〖的〗」学校资料推送给他『们』。(不仅)如此,对数据敏感「〖的〗」增删改操(作对这)种混(在)一起数据要各位小心。〖一不小心〗,可能就会发生误删其他『学校「〖的〗」』信<息>。

为「了」有效「〖的〗」解决这个【《‘问’题》】,我『们』第一要做「〖的〗」<就是>要“将数据分开管理”,<彼此互>不干扰。这是我『们』要实〖现〗「〖的〗」最终{目「〖的〗」}。

想要「〖的〗」效果有「了」,〖现〗(在)「〖的〗」【《‘问’题》】是能不能实〖现〗,该如何实〖现〗,怎么实〖现〗才算好。

我『们』做事情「〖的〗」{目「〖的〗」}<就是>解决【《‘问’题》】,(在)前面我『们』分析「了」我『们』〖要把〗数据(在)一个<系统>『中』《隔离》。那么我『们』自然能想到「〖的〗」 <就是>以学校[为领域划分为不同「〖的〗」库。这样我『们』(在)<系统>运行「〖的〗」{时}候就能做到(在)《(用)户》选【择对应】「〖的〗」学校登陆<系统>{时},就只能访‘问’这个学校「〖的〗」所有信<息>「了」。

『到这里』,我『们』就很清晰「了」,如果我『们』平{时}多看多听(到别人谈论新「〖的〗」知)识点或框架{时},我『们』就会知道对于这种{情况},“{多租}户”<就是>为「了」这种{情况}而诞{生「〖的〗」}。

既然要做 “{多租}户” <系统>,并且团队之间没有使(用)市面上「〖的〗」{多租}户框架。那么我『们』就得自己实〖现〗‘一个「了」’。那么要做「〖的〗」第一件<就是>要「了」解 “{多租}户” 「「〖的〗」概念」。正所谓知己知彼,『方能战无不』胜。

什么是{多租}户

我『们』来看‘下’维基百科对{多租}户「〖的〗」定义是什么({以‘下’是}概述)

{多租}户软件架构<就是>(在)同一个<系统>实例上运行不同《(用)户》,能做到应(用)程序共享,服务〖自治〗,并且还能做到数据互相《隔离》「〖的〗」软件架构思想。“一个<租户>就相当于一组”《(用)户》({(比如)}针对学校来说,「一个学校」<就是>一个<租户>,这个<租户>‘下’有学生,老师作为《(用)户》(一组《(用)户》))。

〖现〗(在)我『们』『总结』一‘下’我『们』要做什么?

我『们』要实〖现〗:

  1. 相同「〖的〗」应(用)程序 app ‘下’
  2. 解析出登陆<系统>「〖的〗」(当前《(用)户》){是属于}哪一个<租户>(〖对应到例子<就是>学校〗)。
  3. 《根》据解析出来「〖的〗」<租户>信<息>,来访‘问’对应「〖的〗」数据库信<息>。

〖现〗(在)我『们』就来实〖现〗上面「说「〖的〗」步骤」。【第一步不(用)想】,<肯定要得一个> app ‘下’。

‘解析<租户>信<息>’

〖现〗(在)我『们』要「设」计如何才能让<系统>检测到当前《(用)户》「〖的〗」<租户>信<息>。

〖现〗阶段我『们』能想到「〖的〗」解析方式有三种:

  1. 域名:〖例如〗 tenant1.example.com,tenant2.example.com
  2. URL:〖例如〗 www.example.com/tenant1/,www.example.com/tenant2
  3. header:〖例如〗 [x-header: 'tenant1'],[x-header: 'tenant2']

一‘下’子有这么多解决方式,“是不是自信心起”来「了」,【有木有】。

具体如何(用)代码实〖现〗呢?‘首先要定义一个’ “<租户>” 「〖的〗」信<息>体,〖为「了」方便表述我这里〗(用)「〖的〗」是类(『当然也可以(用){接}口』)

public class Tenant {
    public string Identifier { get; set;}
  public string Id { get; set;}
}

只要继承「了」这个<租户>类,就表示拥有「了」这个<租户>信<息>。有「了」<租户>之后,我『们』紧接着要做「〖的〗」<就是>解析「了」。因为前面有讨论我『们』解析方式有三种,‘这里我主要’讨论第一种「〖的〗」实〖现〗方案。「正是因为」有多种可能,“解析方式对于”架构来说“是不”稳“定「〖的〗」”,所以我『们』要封装变化来『抽象画』。我『们』先定一个解析<租户> {接}口[类,然后提供一个实〖现〗类具体以域名方式解析,这样封装就达到对修改【封闭】,(新增开放)(OCP)「〖的〗」{目「〖的〗」}「了」。〖例如〗《(用)户》可以自行继承{接}口(用) URL 方式‘解析<租户>信<息>’。

public interface ITenantResolver {
    Task<string> GetTenantIdentifierAsync();
}

public class DomainTenantResolver : ITenantResolver {
  private readonly IHttpContextAccessor _accessor;
  public DomainTenantResolver(IHttpContextAccessor accessor) {
    _accessor = accessor;
  }
  // 〖这里就解析道「了」具〗体「〖的〗」 域名「了」[,从而就能得知当前<租户>
  public async Task<string> GetTenantIdentifierAsync() {
    return await Task.FromResult(_accessor.HttpContext.Request.Host.Host);
  }
}

接着我『们』拿到<租户>标识「符」,“要”干嘛呢?自然是要存起来「〖的〗」,好让<系统>很方便「〖的〗」获取当前《(用)户》「〖的〗」<租户>信<息>。

存储<租户>信<息>

关于存储功能,同样我『们』选择抽象出来一个 ITenantStore {接}口。为什么要抽象出来, 作为一[个基础功能架构「设」计。我『们』就应该考虑这个功能「〖的〗」{解决方案}是否是稳“定「〖的〗」”。“明显”,“对于存储来说”,“方式太多「了」”。所以作为<系统>,要提供一个基本实〖现〗「〖的〗」同{时}还要供开发者方便选择其他方式。

public interface ITenantStore {
    Task<T> GetTenantAsync(string identifier);
}

关于存储,其实我『们』可以选择将<租户>信<息>放入内存『中』,也可以选择放入配置文件,当然你选择将<租户>信<息>放入数据库也是没【《‘问’题》】「〖的〗」。

〖现〗(在)「〖的〗」最佳实践是将一些敏感信<息>,(比如)每个<租户>对应「〖的〗」链接字「符」串都是以 Option 配置文件方式存储「〖的〗」。利(用) .netcore 内置 DI (做到)即拿即(用)。

【这里为「了」简便】,我选择(用)硬编码「〖的〗」方式存储<租户>信<息>

public class InMemoryTenantStore: ITenantStore {
  private Tenant[] tenantSource = new[] {
            new Tenant{ Id = "4da254ff-2c02-488d-b860-cb3b6363c19a", Identifier = "localhost" }
    };
    public async Task<T> GetTenantAsync(string identifier) {
    var tenant = tenantSource.FirstOrDefault(p => p.Identifier == identifier);
        return await Task.FromResult(tenant);
    }
}

“好”「了」,〖现〗(在)我『们』<租户>信<息>有「了」,解析器也提供「了」,『存储服务也』决定「了」。那么接‘下’来就只剩‘下’什“么「了」”?

{进入管道捕}获“源头”

剩‘下’「〖的〗」<就是>找到请求「〖的〗」“源头”, 很显然[,.netcore 优良「〖的〗」「设」计,我『们』可以很方便「〖的〗」将上述我『们』准备「〖的〗」服务安排<至管道『中』>。<那<就是>注册>服务(AddXXXService)和『中』间件(UseXXX)。

所以我『们』这一步要做「〖的〗」<就是>

  1. 注册‘解析<租户>信<息>’服务
  2. 注册『中』间件,好让每一次请求发起{时}截获信<息>将《(用)户》「〖的〗」<租户>信<息>存至这个请求(HttpContext)〖里面〗,好让<系统>随{时}访‘问’当前《(用)户》<租户>信<息>。

{注册}服务类

这个太简单「了」,.netcore 「〖的〗」源代码给「了」我『们』很好「〖的〗」范例

public static class ServiceCollectionExtensions {
    public static AddMultiTenancy<T>(this IServiceColletion services, Action<IServiceCollection> registerAction) where T : Tenant {
        service.TryAddSingleton<IHttpContextAccessor,HttpContextAccessotr>();  // 『这一步很』重要
        registerAction?.Invoke(services);
    }
}

〖调(用)〗:

// Startup.cs ConfigureServices

services.AddMultiTenancy<Tenant>(s => {
    // 〖注册解析类〗
    s.AddScoped(typeof(ITenantResolver), typeof(DomainTenantResolver));
    // 【注】册存储
    s.AddScoped(typeof(ITenantStore), typeof(InMemoryStore));
})

这样我『们』就能(在)<系统>『中』((比如)控制器), 注[入这两个类来完成对当前<租户>信<息>「〖的〗」访‘问’。

“注册服”务解决「了」,然后是『中』间件

注册『中』间件

『中』间件所干「〖的〗」事,很简单,<就是>捕获进来管道「〖的〗」请求上‘下’文,然后解析得出<租户>信<息>,然后把对应「〖的〗」<租户>信<息>放入请求上‘下’文『中』。

class MultiTenantMiddleware<T> where T : Tenant {
    private readonly RequestDelegate _next;

  public TenantMiddleware(RequestDelegate next)
  {
      _next = next;
  }

  public async Task InvokeAsync(HttpContext context)
  {
      if (!context.Items.ContainsKey("localhost"))
      {
          var tenantService = context.RequestServices.GetService(typeof(TenantAppService<T>)) as TenantAppService<T>;
          // 『这』里也可以放到其他地方,(比如) context.User.Cliams 『中』
          context.Items.Add("localhost", await tenantService.GetTenantAsync());
      }

      if (_next != null)
          await _next(context);
  }
}

这样我『们』就实〖现〗「了」整个请求对当前<租户>操作过『程「了」』。「所以本文就」结束「了」。

「不好意思」,开个玩笑。『还没结束』,其实上面是我第一版「〖的〗」(写法)。不知道大家有没有发〖现〗,「我这样写其实」是有 “【《‘问’题》】” 「〖的〗」。大毛病没有,<就是>对开发者不友好。

首先,(在) ConfigureServices 方法里「〖的〗」注册操作,我「〖的〗」 AddMultiTenancy (方法)不纯粹。这是我当{时}写这个 demo {时}候感觉特别“明显”「〖的〗」。因为起初我「〖的〗」方法签名“是不带回调函数” action 「〖的〗」。

public static IServiceCollection AddMultiTenancy<T>(this IServiceColletion services) where T : Tenant {
  services.TryAddSingleton<IHttpContextAccessor,HttpContextAccessotr>();  // 『这一步很』重要
  services.Add(typeof(ITenantResolver), typeof(ImlITenantResolver), LifetimeScope);
  services.Add(typeof(ITenantStore), typeof(ImlITenantStore), LifetimeScope);
  return services;
}

但是(在)注册<租户>解析类和存储类{时},发〖现〗没有实〖现〗类型和生‘命周期做参’数,根本无法注册。 如[果把两个参数当成方法签名,《那不》仅使这个方法变得《丑陋》,还固话「了」这个方法「〖的〗」使(用)。

“所以”最后我改成「了」上面(用)回调「〖的〗」方式,暴露给开发者自己去注册。《所以这就要》求开发者必须要清楚要注册那些内《容》。

所以后来一次偶然「〖的〗」机会看到相关「〖的〗」资料,告诉我其实可以借助 Program.cs 『中』「〖的〗」 Builder 〖模式改善代码〗,“可以让”代码结构更加表『义』化。第二版如‘下’

public static class ServiceCollectionExtensions {
    public static TenantBuilder<T> AddMultiTenancy<T>(this IServiceColletion services) where T : Tenant {
        return new TenantBuilder<T>(services);
    }
}
public class TenantBuilder<T> where T : Tenant {
    private readonly IServiceCollection _services;
    public TenantBuilder(IServiceCollection services) {
        _services = services;
    }
    
    public TenantBuilder<T> WithTenantResolver<TIml>(ServiceLifetime lifttime = ServiceLifetime.Transient) where TIml : ITenantResolver {
        _services.TryAddSingleton<IHttpContextAccessor,HttpContextAccessotr>();  // 『这一步很』重要
        _services.Add(typeof(ITenantResolver), typeof(TImp), lifttime);
        return this;
    }
    
    public TenantBuilder<T> WithStore<TIml>(ServiceLifetime lifttime = ServiceLifetime.Transient) {
        _services.Add(typeof(ITenantStore), typeof(TIml), lifetime);
        return this;
    }
}

所以〖调(用)〗我『们』就变成这样「了」

services.AddMultiTenancy()
        .WithTenantResolver<DomainTenantResolver>()
        .WithTenantStore<InMemoryTenatnStore>();

〖这样看起来是不是更具〗表『义』化和优雅「了」呢。

我『们』重构「了」这一点,《还有一点让我不满意》。那<就是>为「了」获取当前《(用)户》<租户>信<息>, 我必须得注入两个[服务类 —— 解析类和存储类。{这点既然想到「了」还是要}解决「〖的〗」,『因为很简单』。<就是>平常我『们』使(用)「〖的〗」外观模式。

我『们』加入一个特定<租户>服务类来代替这两个类不就“好”「了」么。

public class TenantAppService<T> where T : Tenant {
    private readonly ITenantResolver _tenantResolver;
    private readonly ITenantStore _tenantStore;
    
    public TenantAppService(ITenantResolver tenantResolver, ITenantStore tenantStore) {
        _tenantResolver = tenantResolver;
        _tenantStore = tenantStore;
    }
    
    public async Task<T> GetTenantAsync() {
        var identifier = await _tenantResolver.GetTenantIdentifierAsync();
        return await _tenantStore.GetTenantAsync(identifier);
    }
}

这样我『们』就只需要注入 TenantAppService 《即》可。

其实〖现〗(在)我『们』实〖现〗一个多租【户<系统>】已经达到 90% 「了」。剩‘下’「〖的〗」<就是>如何(在)数据访‘问’层根据获取「〖的〗」<租户>信<息>切换数据库。实〖现〗方法其实也很简单,<就是>(在)注册完{多租}户后,(在)数据库上‘下’文选择链接字「符」串那里替换你获取「〖的〗」{多租}户信<息>所对应「〖的〗」数据库 ID 《即》可。具体「〖的〗」代码实〖现〗这个后〖面〗再聊。

『总结』

回顾一‘下’,我『们』目前做「〖的〗」事。

  1. 发〖现〗【《‘问’题》】:数据混(在)(在)一起无法做到完美「〖的〗」数据《隔离》,{不好控制}。
  2. 「了」解原理:什么是{多租}户
  3. {解决方案}:为「了」解决【《‘问’题》】想到「〖的〗」可实〖现〗「〖的〗」技术方案
  4. (在)架构上考虑如何优化重构一个模块。

发〖现〗没有,我『们』做事一定是要 “带着【《‘问’题》】解决【《‘问’题》】”。首先是解决【《‘问’题》】,(然后才是重构)。千万不要(在)一开始就想着要重构。

其实我『们』(在)解决一个【《‘问’题》】{时},我『们』项目架构可能没有其『中』某一个模块,当要(用)到这个模块{时},我『们』怎么做「〖的〗」。其实一个快速有效「〖的〗」访‘问’,<就是>去看有这个模块功能开源框架,去学习〖里面〗「〖的〗」思想。看他『们』是如何做「〖的〗」。然后有「了」思路就可以依葫芦画瓢「了」,甚至是可以直接粘贴拷贝。

参考资料:https://michael-mckenna.com/multi-tenant-asp-dot-net-core-application-tenant-resolution 推荐阅读

,

诚信(在)线

诚信(在)线 www.nzg8.com自与农展馆合作以来,拓展「了」业务战线,深化「了」服务体系,整合「了」群体,(在)未来「〖的〗」2019{年},将能更好地为诚信(在)线娱乐网「〖的〗」会员提供更优质「〖的〗」服务。

欧博开户声明:该文看法仅代表作者自己,与本平台无关。转载请注明:〖开州头条〗:(如何自行实现一)个多租〖户〗系统

网友评论

  • (*)

最新评论

站点信息

  • 文章总数:474
  • 页面总数:0
  • 分类总数:8
  • 标签总数:850
  • 评论总数:143
  • 浏览总数:3464