實現(xiàn)多租戶系統(tǒng)的一點思考
2020年突發(fā)的新冠疫情,讓在線協(xié)同辦公在疫情期間成為了剛需。我們也從 2020 年的 2月3 日開始在家遠程辦公,直到四月份。協(xié)同辦公軟件一下子火爆了起來,釘釘、企業(yè)微信、特別是騰訊會議等都在疫情期間表現(xiàn)突出,呈現(xiàn)出井噴式的發(fā)展。
目前大部分的企業(yè)信息化都是私有化部署,局限于企業(yè)的內(nèi)部網(wǎng)絡,無法實現(xiàn)遠程協(xié)同辦公,所以越來越多的 To B 企業(yè)逐步轉向 SaaS(Software-as-a-Service,軟件即服務),SaaS 最早是美國Salesforce公司(1999年創(chuàng)立)創(chuàng)造的新軟件服務模式。這家公司的市值在 2019 年已經(jīng)超過1000億美元,國內(nèi)現(xiàn)在還處在發(fā)展中階段,前景還是十分廣闊的。
要將傳統(tǒng)的私有化部署的軟件重構成支持 SaaS 模式,多租戶是一個邁不過去的坎,首先需要將系統(tǒng)改造成多租戶模式,然后再逐步實現(xiàn)計費、系統(tǒng)監(jiān)控、用戶行為分析等功能。
我覺得多租戶的設計應該分為三個層面來進行討論,應用、數(shù)據(jù)庫和中間件。
應用
現(xiàn)在的項目或產(chǎn)品開發(fā)幾乎都是前后端分離的開發(fā)模式,應用層主要指的是 WebAPI ,WebAPI 的改造有兩種方式:
1、每個租戶部署一套 WebAPI、上層通過域名或 Url 地址的解析進行路由,當有新租戶注冊的時候就動態(tài)進行對應的 WebAPI 的部署,這種方式改造成本低,但運維成本高,不建議使用,如果時間緊,可以當過度階段的臨時方案。

2、所有的租戶共用一套 WebAPI ,在 WebAPI 中需要獲取到租戶信息(域名、Url參數(shù)、請求頭信息、Cookie 等),然后進行租戶信息配置的切換。有新租戶創(chuàng)建的時候無需進行新的 WebAPI 的創(chuàng)建,只需要初始化租戶基本信息即可。

在這種方式下,如果 Cluster1 的負載超過限度了,也要能夠進行動態(tài)切換,將其中的某些租戶切換到其他的 Cluester 中,如上圖。
在 WebAPI 的代碼實現(xiàn)上,可以參考 Abp 框架中多租戶的實現(xiàn),這里給出一個簡化版本:
TenantConfiguration:租戶配置信息
[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ù)庫中獲取租戶配置信息
public interface ITenantStore
{
TenantConfiguration Find(string code);
}
public class TenantStore : ITenantStore
{
public TenantConfiguration Find(string code)
{
//從緩存或數(shù)據(jù)庫進行租戶配置信息獲取
throw new NotImplementedException();
}
}
CurrentTenant:當前租戶類,用來存儲當前租戶信息,以及切換租戶
public interface ICurrentTenant
{
TenantConfiguration Config { get;}
IDisposable Change(string code);
}
/// <summary>
/// 當前租戶
/// </summary>
public class CurrentTenant:ICurrentTenant
{
public ITenantStore _tenantStore;
public CurrentTenant(ITenantStore tenantStore)
{
_tenantStore = tenantStore;
}
public TenantConfiguration _config;
public TenantConfiguration Config => _config;
/// <summary>
/// 切換租戶
/// </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ù)進行租戶解析
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:租戶中間件,關于在 dotNET Core 中自定義中間件可以參考《dotNET Core 3.X 請求處理管道和中間件的理解》
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ù)庫
數(shù)據(jù)庫在這里指的是關系型數(shù)據(jù)庫,用來存儲業(yè)務數(shù)據(jù),實現(xiàn)多租戶,就要對數(shù)據(jù)進行隔離,通常的數(shù)據(jù)隔離方式有三種模式:
1、完全隔離,每個租戶使用獨立數(shù)據(jù)庫;
2、部分共享,租戶共享一個數(shù)據(jù)庫,以 schema 或者 table 區(qū)分;
3、完全共享,租戶共享相同的數(shù)據(jù)庫表,以 tenant_id 進行區(qū)分
推薦使用第一種或第二種,隔離程度比較高,也比較容易做橫向擴展,如果是第三種,需要處理數(shù)據(jù)的隔離問題,需要處理單表大數(shù)據(jù)的問題等,對技術要求比較高。
中間件
除了數(shù)據(jù)庫,一個系統(tǒng)還需要依賴其他的一些中間件,比如緩存、消息隊列、文件存儲:
緩存:Redi 消息隊列:RabbitMQ 文件存儲:MongoDB 的 GridFS
Redis
1、Redis 中使用數(shù)據(jù)庫的方式進行租戶隔離;
2、Redis 可以通過修改配置文件的方式進行數(shù)據(jù)庫的擴展,默認為 16 個;3、通過 Redis 分片集群的方式進行部署,可以進行橫向擴展;3、在 Redis 集群中,官方推薦節(jié)點數(shù)量不超過 1000 個,這個對于多租戶系統(tǒng)的前期來說應該是夠用了,如果到了租戶數(shù)量的爆發(fā)期,再進行架構的擴展,比如,不同的租戶路由到不同的 Redis 集群中。
RabbitMQ
在 Rabbitmq 有 vhost 機制,可以一個租戶創(chuàng)建一個vhost,通過 vhost 來進行租戶的隔離,目前還沒查詢到 vhost 是否有上限,需要做進一步驗證。
MongoDB
MongoDB 中主要使用 GridFS 來進行非結構化數(shù)據(jù)的存儲,通過創(chuàng)建數(shù)據(jù)庫的方式來進行租戶的隔離,而且 MongoDB 支持分片的集群部署方式,可以進行擴展橫擴展,在前期,一個 MongoDB 集群應該就夠用了。
最后
技術方案和架構沒有最好的,只有最適合的,符合當下的業(yè)務場景、團隊的技術能力就可以,然后要做的就是做 MVP (最小可行性產(chǎn)品),進而進行系統(tǒng)的改造。
希望本文對您有所幫助!
