我對(duì)實(shí)現(xiàn)多租戶(hù)系統(tǒng)的一點(diǎn)思考
本文轉(zhuǎn)載自微信公眾號(hào)「不止dotNET」,作者不止dotNET。轉(zhuǎn)載本文請(qǐng)聯(lián)系不止dotNET公眾號(hào)。
2020年突發(fā)的新冠疫情,讓在線(xiàn)協(xié)同辦公在疫情期間成為了剛需。我們也從 2020 年的 2月3 日開(kāi)始在家遠(yuǎn)程辦公,直到四月份。協(xié)同辦公軟件一下子火爆了起來(lái),釘釘、企業(yè)微信、特別是騰訊會(huì)議等都在疫情期間表現(xiàn)突出,呈現(xiàn)出井噴式的發(fā)展。
目前大部分的企業(yè)信息化都是私有化部署,局限于企業(yè)的內(nèi)部網(wǎng)絡(luò),無(wú)法實(shí)現(xiàn)遠(yuǎn)程協(xié)同辦公,所以越來(lái)越多的 To B 企業(yè)逐步轉(zhuǎn)向 SaaS(Software-as-a-Service,軟件即服務(wù)),SaaS 最早是美國(guó)Salesforce公司(1999年創(chuàng)立)創(chuàng)造的新軟件服務(wù)模式。這家公司的市值在 2019 年已經(jīng)超過(guò)1000億美元,國(guó)內(nèi)現(xiàn)在還處在發(fā)展中階段,前景還是十分廣闊的。
要將傳統(tǒng)的私有化部署的軟件重構(gòu)成支持 SaaS 模式,多租戶(hù)是一個(gè)邁不過(guò)去的坎,首先需要將系統(tǒng)改造成多租戶(hù)模式,然后再逐步實(shí)現(xiàn)計(jì)費(fèi)、系統(tǒng)監(jiān)控、用戶(hù)行為分析等功能。
我覺(jué)得多租戶(hù)的設(shè)計(jì)應(yīng)該分為三個(gè)層面來(lái)進(jìn)行討論,應(yīng)用、數(shù)據(jù)庫(kù)和中間件。
應(yīng)用
現(xiàn)在的項(xiàng)目或產(chǎn)品開(kāi)發(fā)幾乎都是前后端分離的開(kāi)發(fā)模式,應(yīng)用層主要指的是 WebAPI ,WebAPI 的改造有兩種方式:
1、每個(gè)租戶(hù)部署一套 WebAPI、上層通過(guò)域名或 Url 地址的解析進(jìn)行路由,當(dāng)有新租戶(hù)注冊(cè)的時(shí)候就動(dòng)態(tài)進(jìn)行對(duì)應(yīng)的 WebAPI 的部署,這種方式改造成本低,但運(yùn)維成本高,不建議使用,如果時(shí)間緊,可以當(dāng)過(guò)度階段的臨時(shí)方案。
2、所有的租戶(hù)共用一套 WebAPI ,在 WebAPI 中需要獲取到租戶(hù)信息(域名、Url參數(shù)、請(qǐng)求頭信息、Cookie 等),然后進(jìn)行租戶(hù)信息配置的切換。有新租戶(hù)創(chuàng)建的時(shí)候無(wú)需進(jìn)行新的 WebAPI 的創(chuàng)建,只需要初始化租戶(hù)基本信息即可。
在這種方式下,如果 Cluster1 的負(fù)載超過(guò)限度了,也要能夠進(jìn)行動(dòng)態(tài)切換,將其中的某些租戶(hù)切換到其他的 Cluester 中,如上圖。
在 WebAPI 的代碼實(shí)現(xiàn)上,可以參考 Abp 框架中多租戶(hù)的實(shí)現(xiàn),這里給出一個(gè)簡(jiǎn)化版本:
TenantConfiguration:租戶(hù)配置信息
- [Serializable]
 - public class TenantConfiguration
 - {
 - public Guid Id { get; set; }
 - public string Code { get; set; }
 - public string Name { get; set; }
 - public TenantStatus TenantStatus { get; set; }
 - public string DBConfig { get; set; }
 - public string CacheConfig { get; set; }
 - public string MQConfig { get; set; }
 - public string MongoConfig { get; set; }
 - public TenantConfiguration()
 - {
 - TenantStatus = TenantStatus.Enable;
 - }
 - public TenantConfiguration(Guid id, string name)
 - : this()
 - {
 - Id = id;
 - Name = name;
 - }
 - }
 
TenantStore:從緩存或數(shù)據(jù)庫(kù)中獲取租戶(hù)配置信息
- public interface ITenantStore
 - {
 - TenantConfiguration Find(string code);
 - }
 - public class TenantStore : ITenantStore
 - {
 - public TenantConfiguration Find(string code)
 - {
 - //從緩存或數(shù)據(jù)庫(kù)進(jìn)行租戶(hù)配置信息獲取
 - throw new NotImplementedException();
 - }
 - }
 
CurrentTenant:當(dāng)前租戶(hù)類(lèi),用來(lái)存儲(chǔ)當(dāng)前租戶(hù)信息,以及切換租戶(hù)
- public interface ICurrentTenant
 - {
 - TenantConfiguration Config { get;}
 - IDisposable Change(string code);
 - }
 - /// <summary>
 - /// 當(dāng)前租戶(hù)
 - /// </summary>
 - public class CurrentTenant:ICurrentTenant
 - {
 - public ITenantStore _tenantStore;
 - public CurrentTenant(ITenantStore tenantStore)
 - {
 - _tenantStore = tenantStore;
 - }
 - public TenantConfiguration _config;
 - public TenantConfiguration Config => _config;
 - /// <summary>
 - /// 切換租戶(hù)
 - /// </summary>
 - /// <param name="code"></param>
 - /// <returns></returns>
 - public IDisposable Change(string code)
 - {
 - TenantConfiguration tenantConfig= _tenantStore.Find(code);
 - if (tenantConfig == null)
 - {
 - throw new Exception("Tenant not found");
 - }
 - if (tenantConfig.TenantStatus != TenantStatus.Enable)
 - {
 - throw new Exception("Tenant is disabled or deleted");
 - }
 - return new DisposeAction(() =>
 - {
 - _config = tenantConfig;
 - });
 - }
 - }
 
UrlTenantResolve:根據(jù) Url 參數(shù)進(jìn)行租戶(hù)解析
- public interface ITenantResolve
 - {
 - string Resolve(HttpContext httpContext);
 - }
 - /// <summary>
 - ///
 - /// </summary>
 - public class UrlTenantResolve:ITenantResolve
 - {
 - public string Resolve(HttpContext httpContext)
 - {
 - return httpContext.Request.QueryString.HasValue
 - ? httpContext.Request.Query["__tenant"].ToString()
 - : null;
 - }
 - }
 
MultiTenancyMiddleware:租戶(hù)中間件,關(guān)于在 dotNET Core 中自定義中間件可以參考《dotNET Core 3.X 請(qǐng)求處理管道和中間件的理解》
- public class MultiTenancyMiddleware: IMiddleware
 - {
 - protected readonly ITenantResolve _tenantResolve;
 - private readonly ICurrentTenant _currentTenant;
 - public MultiTenancyMiddleware(
 - ITenantResolve tenantResolve,
 - ICurrentTenant currentTenant)
 - {
 - _tenantResolve = tenantResolve;
 - _currentTenant = currentTenant;
 - }
 - public async Task InvokeAsync(HttpContext context, RequestDelegate next)
 - {
 - var tenantCode = _tenantResolve.Resolve(context);
 - if (tenantCode != _currentTenant.Config.Code)
 - {
 - using (_currentTenant.Change(tenantCode))
 - {
 - await next(context);
 - }
 - }
 - else
 - {
 - await next(context);
 - }
 - await next(context);
 - }
 - }
 
數(shù)據(jù)庫(kù)
數(shù)據(jù)庫(kù)在這里指的是關(guān)系型數(shù)據(jù)庫(kù),用來(lái)存儲(chǔ)業(yè)務(wù)數(shù)據(jù),實(shí)現(xiàn)多租戶(hù),就要對(duì)數(shù)據(jù)進(jìn)行隔離,通常的數(shù)據(jù)隔離方式有三種模式:
1、完全隔離,每個(gè)租戶(hù)使用獨(dú)立數(shù)據(jù)庫(kù);
2、部分共享,租戶(hù)共享一個(gè)數(shù)據(jù)庫(kù),以 schema 或者 table 區(qū)分;
3、完全共享,租戶(hù)共享相同的數(shù)據(jù)庫(kù)表,以 tenant_id 進(jìn)行區(qū)分
推薦使用第一種或第二種,隔離程度比較高,也比較容易做橫向擴(kuò)展,如果是第三種,需要處理數(shù)據(jù)的隔離問(wèn)題,需要處理單表大數(shù)據(jù)的問(wèn)題等,對(duì)技術(shù)要求比較高。
中間件
除了數(shù)據(jù)庫(kù),一個(gè)系統(tǒng)還需要依賴(lài)其他的一些中間件,比如緩存、消息隊(duì)列、文件存儲(chǔ):
- 緩存:Redi
 - 消息隊(duì)列:RabbitMQ
 - 文件存儲(chǔ):MongoDB 的 GridFS
 
Redis
1、Redis 中使用數(shù)據(jù)庫(kù)的方式進(jìn)行租戶(hù)隔離;
2、Redis 可以通過(guò)修改配置文件的方式進(jìn)行數(shù)據(jù)庫(kù)的擴(kuò)展,默認(rèn)為 16 個(gè);3、通過(guò) Redis 分片集群的方式進(jìn)行部署,可以進(jìn)行橫向擴(kuò)展;3、在 Redis 集群中,官方推薦節(jié)點(diǎn)數(shù)量不超過(guò) 1000 個(gè),這個(gè)對(duì)于多租戶(hù)系統(tǒng)的前期來(lái)說(shuō)應(yīng)該是夠用了,如果到了租戶(hù)數(shù)量的爆發(fā)期,再進(jìn)行架構(gòu)的擴(kuò)展,比如,不同的租戶(hù)路由到不同的 Redis 集群中。
RabbitMQ
在 Rabbitmq 有 vhost 機(jī)制,可以一個(gè)租戶(hù)創(chuàng)建一個(gè)vhost,通過(guò) vhost 來(lái)進(jìn)行租戶(hù)的隔離,目前還沒(méi)查詢(xún)到 vhost 是否有上限,需要做進(jìn)一步驗(yàn)證。
MongoDB
MongoDB 中主要使用 GridFS 來(lái)進(jìn)行非結(jié)構(gòu)化數(shù)據(jù)的存儲(chǔ),通過(guò)創(chuàng)建數(shù)據(jù)庫(kù)的方式來(lái)進(jìn)行租戶(hù)的隔離,而且 MongoDB 支持分片的集群部署方式,可以進(jìn)行擴(kuò)展橫擴(kuò)展,在前期,一個(gè) MongoDB 集群應(yīng)該就夠用了。
最后
技術(shù)方案和架構(gòu)沒(méi)有最好的,只有最適合的,符合當(dāng)下的業(yè)務(wù)場(chǎng)景、團(tuán)隊(duì)的技術(shù)能力就可以,然后要做的就是做 MVP (最小可行性產(chǎn)品),進(jìn)而進(jìn)行系統(tǒng)的改造。
希望本文對(duì)您有所幫助!

















 
 
 






 
 
 
 