我對實現多租戶系統的一點思考
本文轉載自微信公眾號「不止dotNET」,作者不止dotNET。轉載本文請聯系不止dotNET公眾號。
2020年突發的新冠疫情,讓在線協同辦公在疫情期間成為了剛需。我們也從 2020 年的 2月3 日開始在家遠程辦公,直到四月份。協同辦公軟件一下子火爆了起來,釘釘、企業微信、特別是騰訊會議等都在疫情期間表現突出,呈現出井噴式的發展。
目前大部分的企業信息化都是私有化部署,局限于企業的內部網絡,無法實現遠程協同辦公,所以越來越多的 To B 企業逐步轉向 SaaS(Software-as-a-Service,軟件即服務),SaaS 最早是美國Salesforce公司(1999年創立)創造的新軟件服務模式。這家公司的市值在 2019 年已經超過1000億美元,國內現在還處在發展中階段,前景還是十分廣闊的。
要將傳統的私有化部署的軟件重構成支持 SaaS 模式,多租戶是一個邁不過去的坎,首先需要將系統改造成多租戶模式,然后再逐步實現計費、系統監控、用戶行為分析等功能。
我覺得多租戶的設計應該分為三個層面來進行討論,應用、數據庫和中間件。
應用
現在的項目或產品開發幾乎都是前后端分離的開發模式,應用層主要指的是 WebAPI ,WebAPI 的改造有兩種方式:
1、每個租戶部署一套 WebAPI、上層通過域名或 Url 地址的解析進行路由,當有新租戶注冊的時候就動態進行對應的 WebAPI 的部署,這種方式改造成本低,但運維成本高,不建議使用,如果時間緊,可以當過度階段的臨時方案。
2、所有的租戶共用一套 WebAPI ,在 WebAPI 中需要獲取到租戶信息(域名、Url參數、請求頭信息、Cookie 等),然后進行租戶信息配置的切換。有新租戶創建的時候無需進行新的 WebAPI 的創建,只需要初始化租戶基本信息即可。
在這種方式下,如果 Cluster1 的負載超過限度了,也要能夠進行動態切換,將其中的某些租戶切換到其他的 Cluester 中,如上圖。
在 WebAPI 的代碼實現上,可以參考 Abp 框架中多租戶的實現,這里給出一個簡化版本:
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:從緩存或數據庫中獲取租戶配置信息
- public interface ITenantStore
- {
- TenantConfiguration Find(string code);
- }
- public class TenantStore : ITenantStore
- {
- public TenantConfiguration Find(string code)
- {
- //從緩存或數據庫進行租戶配置信息獲取
- 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:根據 Url 參數進行租戶解析
- 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);
- }
- }
數據庫
數據庫在這里指的是關系型數據庫,用來存儲業務數據,實現多租戶,就要對數據進行隔離,通常的數據隔離方式有三種模式:
1、完全隔離,每個租戶使用獨立數據庫;
2、部分共享,租戶共享一個數據庫,以 schema 或者 table 區分;
3、完全共享,租戶共享相同的數據庫表,以 tenant_id 進行區分
推薦使用第一種或第二種,隔離程度比較高,也比較容易做橫向擴展,如果是第三種,需要處理數據的隔離問題,需要處理單表大數據的問題等,對技術要求比較高。
中間件
除了數據庫,一個系統還需要依賴其他的一些中間件,比如緩存、消息隊列、文件存儲:
- 緩存:Redi
- 消息隊列:RabbitMQ
- 文件存儲:MongoDB 的 GridFS
Redis
1、Redis 中使用數據庫的方式進行租戶隔離;
2、Redis 可以通過修改配置文件的方式進行數據庫的擴展,默認為 16 個;3、通過 Redis 分片集群的方式進行部署,可以進行橫向擴展;3、在 Redis 集群中,官方推薦節點數量不超過 1000 個,這個對于多租戶系統的前期來說應該是夠用了,如果到了租戶數量的爆發期,再進行架構的擴展,比如,不同的租戶路由到不同的 Redis 集群中。
RabbitMQ
在 Rabbitmq 有 vhost 機制,可以一個租戶創建一個vhost,通過 vhost 來進行租戶的隔離,目前還沒查詢到 vhost 是否有上限,需要做進一步驗證。
MongoDB
MongoDB 中主要使用 GridFS 來進行非結構化數據的存儲,通過創建數據庫的方式來進行租戶的隔離,而且 MongoDB 支持分片的集群部署方式,可以進行擴展橫擴展,在前期,一個 MongoDB 集群應該就夠用了。
最后
技術方案和架構沒有最好的,只有最適合的,符合當下的業務場景、團隊的技術能力就可以,然后要做的就是做 MVP (最小可行性產品),進而進行系統的改造。
希望本文對您有所幫助!