雲計算

元數據驅動的 SaaS 架構與背後的技術思考

道衝而用之或不盈,淵兮似萬物之宗。

—老子

引言

作為業務系統技術開發同學,面向當下:

  • 首先應該是快速搭建業務通路,讓線上業務跑起來,快速試錯,解決生存問題;

  • 第二步是在鏈路暢通、業務基本跑起來的基礎上,如何支撐業務跑得更快,就需要解決快速增長問題;

  • 第三步,在完成支撐業務快速增長的基礎上,要進行精細化提升,通過在支撐業務快跑間隙擠時間打磨系統功能和體驗,踏踏實實花時間去抽象能力,沉澱產品,提升效能;

同時我們也必須面向未來,如何在抽象能力以及沉澱了產品的基礎上,把所承載和沉澱的業務能力快速輸出,貢獻給整個行業,或為整個社會商業生態提供基座支撐。面向未來,將平臺產品進行 SaaS 化升級,真正將能力進行有價值開放輸出是我們提前要佈局的核心方向。

將平臺產品進行 SaaS 輸出,需要解決那些問題呢?這裡嘗試把核心問題列舉一下:

  1. 如何根據不同用戶需求進行計算能力按需調度分配?(IaaS/PaaS)

  2. 如何滿足用戶數據安全性要求,嚴格隔離不同用戶的數據,使用戶只能看到自己的數據?(PaaS)

  3. 如何支持不同用戶在標準的數據對象/數據模型上按需添加自定義的數據對象/擴展模型?(PaaS & SaaS)

  4. 如何按照不同用戶進行按需功能搭配組合,滿足不同用戶從基礎到專業級不同業務場景需求?(SaaS)

  5. 如何統一對平臺產品進行升級而不影響用戶已有數據及功能?(IaaS、PaaS、SaaS)

通過以上問題,我們可以看出,產品 SaaS 化輸出的關鍵是如何對不同的用戶通過標準+擴展能力按需進行算力、數據、安全、功能有效定製,支持多用戶共性和個性的問題,即多租戶的問題,同時也涉及到計費和服務水平等相關問題。我們下面來聊下上述問題的解題關鍵和解題思路:

  • 第1個算力問題的核心是調度問題,彈性計算提供在 IaaS 層的統一算力調度能力,而 Serverless 則可以在 PaaS 層提供更高層次的算力調度能力。

  • 第4個問題的核心是業務流程的抽象和業務功能的拆分。領域驅動設計以及服務化(微服務)在平臺功能抽象拆分上提供了相對成熟的思路,催化了以縱向業務功能細分作為域劃分的依據的服務化方案以及組織結構,主要訴求是在細分的業務功能服務基礎上,能按需快速靈活的組合,從而支撐不同的業務模式,提供業務敏捷性,支撐業務創新求變。

當然反過來,由於縱向功能細分,業務功能域增多,整個業務鏈條上的咬合點越來越多,隨之產生越來越多的數據來源冗餘重複或者缺失,功能或者重合且各自發散,或者缺失,最終給整體業務帶來較多數據和功能的不一致性風險。這樣一來,不僅橫向端到端的業務串聯成本高,而且關鍵路徑的風險收斂成本比較高,矛盾衝突點集中在各縱向域功能和數據咬合處,具體表現為:

數據上:

  • 無主數據,有數據需求無 owner;

  • 大量重複且不一致數據;

功能上:

  • 部分業務功能缺失;

  • 域之間存在業務功能重複且行為不一致。

到底是縱向切分域還是橫向分業務模式拉平來做,這個問題沒有標準答案,更沒有最佳答案。只有根據不同的業務發展階段及時動態調整試錯,換言之,這是一個不斷尋找相對最優解的動態過程。

彈性計算和 Serverless 解決了算力的問題,領域驅動服務化設計解決了功能的拆分和按需搭配組合的問題,那麼剩下的核心問題就是數據了:如何以一套統一的數據架構,既能支撐多租戶的數據安全性需求以及通用的數據存儲,也能支撐用戶擴展的自定義數據對象定義和模型變更,同時也要保證數據定義層面的擴展和變更不會影響自身和其他租戶業務功能的可用性。我們來分析下可能的方案(暫不考慮按服務邊界進行數據庫拆分):

  1. 統一的數據庫,標準數據模型和擴展數據模型直接映射到物理表和索引:很顯然,對於不同租戶自定義的數據對象和數據模型要求是無法支撐的,物理數據模型會相互干擾、相互衝突直到無以為繼。即使是對於所有租戶完全標準的功能和數據存儲,平臺自身的標準模型升級的 DDL 也會對用戶的可用性造成較大影響,所以顯然是行不通的。

  2. 如果為每個租戶創建各自的數據庫呢?各自租戶擁有各自的數據庫,可以滿足用戶數據安全隔離的需求,也可以滿足各租戶自定義的數據需求,看上去像是一種合理的 SaaS 數據方案。但是仔細分析,會發現有兩個明顯的問題:

  • 如果用戶需要修改或者擴展現有物理數據模型而進行的 DDL 操作,必然會影響線上業務的整體可用性,也可能會影響到標準數據模型,從而影響到線上功能使用。
  • 如果用戶可自定義對物理模型進行擴展和定製,當平臺進行模型升級的時候,極容易產生物理模型的衝突,導致新舊功能異常。
  • 由於用戶在各自數據庫存在各自定義的擴展和定製,則平臺數據模型和功能升級需要針對不同的租戶進行分別驗證,存在極大的升級驗證工作量和風險。

以上兩種方案可行性低,我們從其中發現的問題是:平臺業務系統的邏輯模型到物理模型的直接映射是造成問題的主要因素。既然物理模型的變更是平臺不穩定的動因,那麼我們是否能通過解耦業務邏輯模型和物理模型的映射關係來嘗試解決這個問題呢?

既然問題已經定義清楚了,如何解決這個問題呢?通常我們解決架構問題的一個“萬能”的方法是:增加一個層次,我們也來套用一次,增加一個層次(元數據層)來解耦邏輯模型到物理模型強映射的問題。

首先,我們需要對業務進行建模,對業務進行抽象,定義出業務邏輯模型,然後對模型進行二次抽象,定義出邏輯模型的定義數據,實現業務模型的數據化,即模型的元數據(The Metadata of the Logic Model ),將模型結構存儲為數據,而不是直接對應的物理存儲結構。

其次根據定義出的元數據進行統一抽象,形成元數據邏輯模型。

將元數據邏輯模型映射到元數據物理模型,對應實際存儲結構。

通過對業務模型的變更,形成對元數據層的數據變更,而不是物理結構的變更,從而實現業務邏輯模型同物理模型的解耦。

image.png

很多事情說起來好像挺簡單,實際上是一個非常巨大的系統工程,將其付諸實踐是挑戰非常大的事情,而取得踏踏實實的成功則更難。上述問題的解題思路是 Salesforce 的解題思路,而且 Salesforce 不僅取得了成功,也接近將其做到了極致,下面我們站在巨人的肩膀上來看看 Salesforce 如何通過元數據驅動的架構(核心是基礎數據架構)來支撐多租戶的 SaaS 業務平臺。
注意:由於 Salesforce 並未有對核心實現邏輯進行完全公開和說明,所以本文所整理的部分核心邏輯包含了作者的邏輯推理和解讀,但是確實進行了邏輯驗證和場景驗證,如有紕漏和不全面的地方,歡迎討論及指正。

元數據驅動的多租戶架構

Salesforce 將 Force.com 定義為 PaaS 平臺,Force.com 的基礎就是元數據驅動的軟件架構來支撐多租戶應用。首先我來解釋下什麼是以元數據驅動的軟件架構為核心。

一、多租戶意味著什麼

多租戶的含義用一句話來描述就是:一個雲平臺,無數多個客戶。

一個雲平臺的含義是:一個代碼庫,一個數據庫,一整套共享的可擴展服務,包括數據服務、應用服務以及 Web 服務。

無數多個客戶的含義是:每個客戶都被分配一個唯一的租戶 OrgID,所有的數據存儲都是按照租戶 OrgID 隔離的,所有的數據訪問必須包含 OrgID,所有的操作也都是包含租戶 OrgID 的,也就是所有的客戶數據和行為都是被安全的通過唯一的租戶 Org 進行嚴格隔離的。

每個租戶/組織只能看到和定義按照自己租戶 OrgID 隔離的自己版本的元數據和數據,而且只能執行自己租戶 OrgID 所授權的行為,這樣每個租戶就擁有各自版本的 SaaS 方案。

二、元數據驅動意味著什麼

元數據對於平臺意味著平臺數據的數據,對於租戶意味著是關於租戶數據的數據。

當用戶定義一個新的用戶表的時候,用戶創建的不是數據庫中的物理表,而是在系統態的元數據表中添加了一條記錄,這個記錄描述的是用戶表的邏輯定義,是虛擬的,這個表並不在數據庫中物理存在,而這條記錄代表就是用戶態的數據表。

當用戶定義了用戶表的一個新的字段時,用戶並沒有在物理表中創建物理字段,而是在系統態的元數據表中添加了一個記錄,這個記錄描述的用戶表的字段組成的邏輯結構,是虛擬的,這個字段也不在數據庫表結構中物理存在,而這條記錄代表的就是用戶態的用戶表字段。

也就是通過存儲在系統態的元數據表中的元數據記錄作為虛擬用戶的數據庫結構。

三、元數據驅動的多租戶整體架構

我們先來大概瞭解下元數據驅動的多租戶的整體架構,整體架構大概分為 5 個邏輯層次:

  1. 底層數據架構分為三個層次:
  • 最底層是數據層,存儲了離散的系統和用戶的業務數據,業務日常運營的數據存儲在這裡。

  • 公共元數據層,存儲了應用系統標準的對象和標準的字段定義,對底層數據的結構進行定義說明。

  • 租戶特定元數據,存儲了租戶自動的對象和自定義的字段定義,用於對底層的數據結構進行定義說明。

  1. 通用數據字典 UDD(Universal Data Dictionary) 運行引擎層實現了應用對象到底層數據存儲的映射,包含對象模型操作、SOQL 語言解析、查詢優化,全文搜索等功能,我們常說的 ORM 功能也是其核心功能,但比其複雜的多。

  2. 平臺服務層提供 PaaS 層平臺服務,提供應用對象模型的創建,權限模型創建,邏輯和工作流程創建以及用戶界面的創建,包括屏幕布局、數據項、報表等。

  3. 標準應用層提供端到端的標準的業務應用功能。

  4. 租戶虛擬應用層,用戶可以在標準應用層或者平臺服務層之上定義自己特有的業務應用功能,滿足自己特定的業務場景需要。

image.png

其中,底層數據架構是最為關鍵的平臺基石(The Corner Stone),其核心運行引擎也是基於強大的底層數據架構基礎上構建的。本文則以元數據驅動的多租戶數據架構為核心來一一展開。

四、元數據驅動的多租戶數據架構

下面我們具體來看下系統態的數據模型,基於 Salesforce 加上個人推理的元數據驅動的多租戶數據模型。
注意:由於 Salesforce 並未有對核心邏輯進行完全公開和說明,所以本文所整理的部分核心模型包含了個人的邏輯推理和解讀,但是確實進行了邏輯驗證和場景驗證,如有紕漏和不全面的地方,歡迎討論及指正。

Salesforce 雲服務平臺遵循的是面向對象的設計理念,所有的實體、實體關係以及實體的 CRUD 均是以對象的視角進行的,所以其元數據驅動的多租戶數據模型的存儲基本元素也是按照對象的顆粒度進行存儲,源自於 OO 的對象間引用,同普通關係數據庫主外鍵關係異曲同工,只是細節處理上不盡相同,請大家注意這一點。

1. 元數據驅動的多租戶數據架構概覽

首先,我們先來大概瞭解下元數據驅動的多租戶模型的核心內容,元數據驅動的多租戶的數據模型主要分為三個部分:元數據表、數據表和功能透視表。

元數據表(Metadata Tables)

元數據表用於存放系統標準對象以及用戶自定義對象和字段定義的元數據,也就是系統和用戶對象的邏輯結構,即對應於關係數據庫中的虛擬表結構。元數據表主要包括Objects 表以及 Fields 表,是系統標準對象和用戶對象定義數據的倉庫,即元數據倉庫。

數據表(Data Tables)

數據表用戶存放系統以及用戶對象和字段的實際數據,實際的用戶業務數據以及應用系統相關數據存放在這裡。數據表包括 Data 表和存放大文本數據的 Clob 表,數據表存儲了絕大部分用戶的實際數據,是一個巨大的用戶業務數據倉庫。

功能透視表(Specialized Pivot Tables)

功能透視表包含了非常關鍵的關係表、索引表、關係表以及其他特定用途表。例如關係表定義了對象間的關係,索引表解決虛擬結構索引的問題,這部分後續將進行詳盡的介紹。

image.png

2. 元數據驅動的多租戶數據架構詳解

上一節粗略地描述了元數據驅動的多租戶模型三大部分模型實體和基本作用,大家可能會比較疑惑,這麼簡單一個實體模型,怎麼就起了這麼個牛逼的名字,而且支撐了“一個雲平臺,無數個客戶”。我們下面就對此模型的核心邏輯進行詳細展開和推理說明,同時詳細闡述以此模型為中心的服務來說明整個元數據層或者說 UDD(Universal Data Dictionary) 層的設計。

土話說:“沒有對比,就沒有傷害”。道理是相通的,用相似的事物進行對比是對理解客觀事物比較好的方法,找出其相同點和共性的地方,找出其不同點和異樣的地方,同時識別出是否有不可對比的方面。從各個方面去對比,則能更全面、更深入的瞭解客觀事物。

下面我按照普通應用設計思路方式來定義一個簡單直觀的多租戶 SaaS 數據架構方案示例,作為元數據驅動多租戶數據架構方案的對比基準方案,用對比來更好的幫大家瞭解元數據驅動多租戶數據模型及架構的設計邏輯。

普通多租戶 SaaS 數據架構方案示例(僅做示例)

  • 多租戶基本思路:每個租戶一個數據庫,提供數據庫級別的租戶數據隔離,平臺提供標準應用功能模型,用戶可以在各自數據庫內定義以及修改各自的定義模型,所有模型採用數據庫物理表、索引、主外鍵實現。不同的租戶通過路由到不同的數據庫來實現隔離。

  • 域模型樣例採用大家都熟悉的最小集的訂單模型實現,包含商品、用戶、訂單和訂單詳情表。注意:此簡化模型僅用做示意說明,和意圖無關的大多數字段均省略,非嚴謹定義。

image.png

  • 示例模型數據

數據庫物理表數據:Customer

image.png

數據庫物理表數據:Product

image.png

數據庫物理表數據:Order

image.png

數據庫物理表數據:OrderItem

image.png

  • 實體表關係
    Order 表同 OrderItem 為父子表,通過 OrderID 進行主外鍵關聯;Customer 表同 Order 表為父子表,通過 CustomerID 進行主外鍵關聯;Product 表同 OrderItem 表為父子表,通過 ProductID 進行主外鍵關聯。

  • 用戶自定製
    用戶有執行 DDL 權限,可以在自己租戶數據庫內在進行擴展模型自定義,建立自定義的物理表,索引,關係等。

  • 問題和風險
    用戶具有執行 DDL 權限,可以自定義數據庫物理模型,會帶來各租戶的自定義數據模型大爆炸,會給後續平臺模型定義升級衝突,造成模型升級的巨大的障礙

同時,由於系統標準模型和用戶模型均為物理模型,未有做系統標準和自定義數據的有效隔離,如何保證平臺應用的每一次升級必然會考慮對現有用戶自定義模型的穩定性和可用性的影響,在自定義物理模型的情況下,不僅挑戰巨大,而且包含巨大的迴歸驗證的工作量,很難收斂。

當用戶執行 DDL 時,通常會鎖定數據庫物理資源,當數據庫數量非常巨大時可能會帶來不可控的 downtime,對應用系統的可用性造成巨大的影響。如果數據庫是每個租戶各自獨佔,還只會影響到單個租戶;如果是多租戶共享數據庫,則可能會影響到其他租戶,影響是災難性的。作為雲平臺服務商,不管是用戶操作還是系統行為,我們都不期望我們的設計對用戶系統的可用性造成影響,所以用戶執行 DDL 的行為是否允許確實有待商榷,但是如果不允許,用戶可擴展性在這種設計環境中必然受到一定程度的限制。

元數據驅動的多租戶數據模型(Metadata Tables)

前面章節描述了元數據驅動的多租戶模型簡單模型圖,本小節詳細解說下每個核心實體表的核心結構,同時已知資料部分較為簡略,無法描述模型全貌和核心細節,為了模型完整性,整體數據模型包含了作者思路推理部分,用以來完整清晰地定義模型。當然由於所有模型都是主觀的(subjective),僅代表個人觀點,歡迎大家的不同的觀點,一起討論改進。

正如前面介紹“一個雲平臺”時提到,通過一個統一的數據庫來支撐無數個租戶,所以元數據驅動的多租戶模型是基於一個共享數據庫的前提。當然多租戶實現設計多種多樣,大家可以不拘泥此種。

1)元數據表之對象定義表:Objects 表

image.png

Object 系統表存儲了每個租戶為它的擴展應用對象定義的元數據,包含如下核心字段:

  • ObjID:應用對象唯一標識,具有固定長度和格式。

  • OrgID:應用對象所歸屬的租戶 ID,用於統一共享數據庫內的多租戶數據隔離,通常和租戶定義的域名對應。

  • ObjName/Name:對象名稱,用於系統配置和開發(developer name)。

  • Label: 對象的顯示名稱。

除了用戶自定義對象,系統的標準對象也是採用相同的方式進行定義的。

2)元數據表之字段與關係定義表:Fields 表

image.png

Fields 系統表存儲了每個租戶為他的擴展應用對象字段定義的元數據,包含了其所歸屬的應用對象的租戶 OrgID,字段所屬對象的 ObjID,字段定義標識 FieldID,字段名稱FieldName,字段存儲位置定義 FieldNum,數據類型 DataType。數據類型重要補充關聯字段(DigitLeft,Scale,TextLength,RelatedTo,ChildRelationshipName)以及是否必選、唯一、索引標記,還有部分標準字段。Fields表非常關鍵,其不僅定義了普通的應用對象字段,包括基本信息和數據類型信息,而且通過特殊關係字段對不同應用對象之間的關係進行定義,詳細說明如下:

  • FieldID:此對象字段的唯一標識,具有固定長度和格式
  • OrgID:其所歸屬的應用對象所歸屬的租戶 OrgID
  • ObjID:字段所屬對象的 ObjID
  • FieldName/Name:字段名,用於系統配置和開發(developer name)。
  • Label:字段展示名稱,用以展示給最終用戶。
  • FieldNum:對應到 Data 數據表的數據存儲字段映射,暨 Data 表中 ValueX 字段中的X。
  • DataType:指定此對象字段的數據類型包含普通類型:Number、TEXT、Auto Number、Date/Time、Email、Text Area等,也包含特殊的關係類型如:Look up關係類型、Master-Detail 關係類型等。
  • DigitLeft 和 Scale:用於 Number、Currency、Geolocation 等數字數據類型的關聯設定,例如定義了一個字段的 DateType 為 Number,則需要指定其整數部分的最大位數 DigitLeft 和小數部分的最大位數 Scale,兩部分長度總和不超過 18 位。
  • TextLength:當數據類型為 TEXT 時啟用,用於指定 TEXT 類型的字符的長度限制。
  • RelatedTo 和 ChildRelationshipName:這兩個字段當 DateType 為關係類型(Look up,Master-Detail 等)時會啟用,其中 RelatedTo 保存關聯的應用對象 ID,ChildRelationshipName 用於保存父子關係中子方的關係名稱,同一個父對象的子方的關係名稱唯一,用於關係的反向查詢。
  • IsRequired:此字段數據保存時,是否校驗值的存在。
  • IsUnique:是否允許重複值。
  • IsIndexed:此字段是否需要建索引。
  • 其他字段:此處僅列舉了說明模型所需要的字段,其他字段暫不進行列舉,不列舉原因和其重要性並無直接關聯。

3)數據表(Data Tables)之關係數據表:Data 表

image.png

MTData 系統表存儲了 MTObjects 和 MT_Fields 元數據表內定義的數據對象(表)所對應的數據,一一映射到不同的租戶各自定義的表和表中的字段(對象和對象字段)。

  • GUID:數據表的主鍵,用於存放每個應用對象實例的標識 ID。
  • ObjID:其所歸屬的應用對象所歸屬的租戶 OrgID。
  • Name:應用對象實例名稱。
  • Value0....Value500:用於存放對象實例字段的數據,其 ValueX 中 X 值對應到 Fields 表中 FieldNum 定義,ValueX 存放的數據,不管原始數據類型、存儲格式均為變長字符串格式。

4)數據表(Data Tables)之非結構化數據表:CLobs

MT_Clobs 用於存儲大字符段的存儲 CLOB,同時 CLOB 也存儲在數據庫外的索引結構中,用於快速的 Full-Text 文本檢索。

3. 元數據模型核心實體關係圖

我們在應用系統開發中,通常我們定義的數據結構包括數據表、表字段,索引通常都會直接定義在物理數據庫中,創建物理的表和字段以及索引等。

但是在元數據驅動平臺數據模型中,我們定義的用戶表包括系統表都是邏輯表,其結構是虛擬的,用戶表的定義存儲在 Objects 表,對應的字段定義存儲在 Fields 表中,實際用戶數據存儲在 Data 表中。特別注意的是,對象的引用關係定義也定義在 Fields 表中,以特殊數據類型方式來定義。(另:Relationships 表後面章節進行描述)。

從每個租戶視角來看,每個租戶都在一個共享數據庫內擁有一個基於租戶標識 OrgID 來隔離的虛擬的租戶數據庫。

元數據實體包括 Objects 和 Fileds 實體以及實際數據 Data 實體都包含租戶 OrgID,這樣就可以通過租戶 OrgID 來天然隔離各租戶的數據,當然不止這些實體,包括索引相關等透視表實體也使如此。

image.png

4. 標準對象與標準字段

前面整體架構層次裡提到了公共元數據層和標準應用層,公共元數據層提供了標準對象和標準字段的定義。

其中標準對象為每個租戶提供公共端到端的應用的標準應用功能。

image.png

同時用戶可以在標準的對象基礎上擴展自定義的應用對象,滿足自己的特定業務場景。__c 後綴代表自定義,後續詳解。

image.png

而標準字段則提供給每個對象包括自定義對象的共同的字段,包含部分業務字段和非業務字段。

image.png

用戶也可以在標準對象和自定義對象內自定義不同的字段,以滿足業務需要。__c後綴代表自定義,後續詳解。

image.png

5. 對象關係類型

應用對象關係類型主要分為 Look up 和 Master-Detail 兩種關係類型,其中 Look up 為弱的父子關係類型,Master-Detail 為強的父子關係類型,其特性對比如下。

image.png

6. 元數據驅動的多租戶數據架構示例

同樣採用普通多租戶 SaaS 數據架構方案中相同的域模型和示例數據作為參照進行說明,只不過在這裡域模型不再對應到數據庫的物理模型,而是對應到元數據所定義的虛擬數據庫的邏輯模型。請前後對比兩種模型對用戶業務模型承載的差異和聯繫,以便深入瞭解元數據驅動的多租戶數據架構。

image.png

對於 Tenant 租戶 A00001,需要支撐相同的業務邏輯,需要定義相同的域模型,和普通的方案不同的是,這裡採用元數據驅動的多租戶數據模型來定義訂單域模型和對應示例數據,其中域模型定義在元數據表(Metadata Tables)中,數據存儲在 Data Tables 表中。

1)用戶自定義對象 Product 的定義

Product 對象的基本信息定義在 Objects 表,作為 Objects 表的一條記錄,通過 OrgID 進行不同租戶數據隔離。Object 中的每一條記錄都代表一個不同的對象。Objects 表的定義非常清晰,這裡不做過多的解釋,請參考 Objects 表介紹。

image.png

Product 對象的字段結構定義在 Fields 表,同時通過 ObjID 同 Order 對象定義進行關聯,通過 OrgID 進行多租戶數據隔離。

FieldID 格式為字段定義的標識 ID,用於區分每個字段定義,對於標準字段,則採用標準字段 ID,如 Name,則直接採用 Name 作為字段標識 ID,對於自定義字段,則元數據引擎自動生成 15 位的標準格式的 FieldID。其他字段定義請參考前面的 Fields 元數據表詳細介紹。

下面詳細描述一下 Product 對象中每個字段定義:

  • 產品名稱 Name 字段 為標準字段,數據格式為TEXT,長度為80。
  • 產品編號 ProductNo 為自定義字段,數據格式為 TEXT,長度為 22,FieldNum 為 1 對應 Data 表存儲字段 Value1,存儲格式為變長字符串。
  • 產品價格 ProductPrice 為自定義字段,數據格式為 Currentcy(此格式類似Number,不同是帶幣種),整數最大長度 DigitLeft:16 位,小數位最大精度Scale:2 位,FieldNum 為 2 對應 Data 表存儲字段 Value3,存儲格式為變長字符串。
  • 狀態 ProductStatus 為自定義字段,數據格式為 TEXT,長度為 20,FieldNum 為 3對應 Data 表存儲字段 Value3,存儲格式為變長字符串。

image.png

2)用戶自定義對象 Customer 的定義

Customer 對象的基本信息定義在 Objects 表,作為 Objects 表的一條記錄,通過 OrgID 進行不同租戶數據隔離。Object 中的每一條記錄都代表一個不同的對象。Objects表的定義非常清晰,這裡不做過多的解釋,請參考Objects表介紹。

image.png

Customer 對象的字段結構定義在 Fields 表,同時通過 ObjID 同 Order 對象定義進行關聯,通過 OrgID 進行多租戶數據隔離。

下面詳細描述一下 Customer 對象中每個字段定義:

  • 用戶名稱 Name,必選標準字段,不過多解釋。
  • 用戶編號 CustomerNo 為自定義字段,數據類型為 TEXT,長度為 22,FieldNum 為 1 對應 Data 表存儲字段 Value1,存儲格式為變長字符串。
  • FirstName 和 LastName 為自定義字段,數據類型為 TEXT,長度均為 20,FieldNum 為 2,3 對應 Data 表存儲字段 Value2 和 Value3,存儲格式為變長字符串。
  • 用戶暱稱 Nick Name 為自定義字段,數據類型為 TEXT,長度均為 20,FieldNum 為 4 對應 Data 表存儲字段 Value4,存儲格式為變長字符串。
  • 用戶登錄名 LoginName 為自定義字段,數據類型為 TEXT,長度均為 20,FieldNum 為 5 對應 Data 表存儲字段 Value5,存儲格式為變長字符串。
  • 用戶狀態 CustomerStatus 為自定義字段,數據類型為 TEXT 或者 PickList,長度為 20,FieldNum 為 6 對應 Data表存儲字段 Value6。為簡化起見,狀態字段暫定義為 TEXT,對應 Data 表存儲字段 Value4,存儲格式為變長字符串。

image.png

3)用戶訂單 Order 邏輯表的定義

Order 對象的基本信息定義在 Objects 表,作為 Objects 表的一條記錄,通過 OrgID 進行多租戶數據隔離。Objects 表中的每一條記錄都代表一個不同的對象。

image.png

Order 對象的字段結構定義在 Fields 表,同時通過 ObjID 同 Order 對象定義進行關聯,通過 OrgID 進行多租戶數據隔離。

下面詳細描述一下 Order 對象中每個字段定義:

  • 訂單編號 OrderNo 為自定義字段,DataType 數據格式為 TEXT,長度為 22,FieldNum 為 1,對應 Data 表存儲字段 Value1,存儲格式為變長字符串。
  • 關係字段 Customer 為自定義關係字段,DataType 類型為弱類型 Look up 關係,關聯到父對象 Customer,則 RelatedTo 列存儲 Customer 的 ObjID:01I2v000002zTEZ,對應的 FieldNum 為 2,則 Customer 對象實例 GUID 存儲在 Data 表的 Value2 列。ChildRelationshipName 列存儲對象父子關係中子關係名稱:orders,用於對象關係中從父對象實例數據反查子對象實例數據。
  • 訂單狀態 OrderStatus 為自定義字段,DataType 類型為 TEXT,長度為 20,FieldNum 為 3,則狀態存儲在 Data 表的 Value3 列。為簡化起見,狀態字段暫定義為 TEXT。
  • 下單時間 OrderTime 為自定義字段,DataType 類型為 Date/Time,FieldNum 為4,則下單時間存儲在 Data 數據表的 Value4 列。

image.png

4)用戶訂單行 OrderItem 邏輯表定義同樣的,OrderItem 對象的基本信息也以一條記錄的信息定義在 Objects 表,通過 OrgID 進行多租戶數據隔離。Objects 表中的每一條記錄都代表一個不同的對象。

image.png

OrderItem 的字段結構也定義在 Fields 表,通過 ObjID 同 OrderItem 對象關聯,通過 OrgID 進行多租戶數據隔離。

下面詳細描述一下 Order 對象中每個字段定義:

  • 關係字段 Order 為自定義關係字段,DataType 類型為強類型的 Master-Detail 關係,關聯到父對象 Order,則 RelatedTo 列存儲 Order 對象的 ObjID:01I2v000002zTEj,對應的 FieldNum 為 1,則 Order 對象實例 GUID 存儲在 Data 表的 Value1 列。ChildRelationshipName 列存儲對象父子關係中子關係名稱:OrderItem(s),用於對象關係中從父對象 Order 實例數據反查子對象實例數據。
  • 關係字段 Product 為自定義關係字段,DataType 類型為弱類型的 Look up 關係,關聯到父對象 Product,則 RelatedTo 列存儲 Product 對象的 ObjID:01I2v000002zTEU,對應的 FieldNum 為 2,則 Product 對象實例 GUID 存儲在Data 表的 Value2 列。ChildRelationshipName 列存儲對象父子關係中子關係名稱:OrderItem(s),用於對象關係中從父對象 Product 實例數據反查子對象實例數據。
  • 商品實際售價 ItemPrice 為自定義字段,DateType 類型為 Currentcy(此格式類似 Number,不同是帶幣種),整數最大長度 DigitLeft:16 位,小數位最大精度 Scale:2 位,FieldNum 為 2 對應 Data 表存儲列 Value3,存儲格式為變長字符串。
  • 商品購買數量 Item Quantity 為自定義字段,DataType 類型為 Number,整形長度為 18 位,無小數位數,FieldNum 為 4,對應 Data 數據表存儲列 Value4。
  • 訂單明細狀態 OrderItemStatus 為自定義字段,Datetype 類型為 TEXT,長度為 20,對應 FieldNum 為 5,對應 Data 數據表存儲列 Value5。為簡化起見,狀態字段暫定義為 TEXT。

image.png

5)對象 Schema

定義好的用戶應用對象 Schema 如下圖

image.png

6)數據表 Data 表用戶數據存儲

前面提到了用戶自定義的應用對象以虛擬結構的方式存儲在 Objects 和 Fields 表中,那麼用戶定義的應用對象 Product、Customer、Order 和 OrderItem 裡的數據存儲在哪裡呢?答案是 Data 表,用戶定義的對象的數據均會存儲在 Data 表中,每個用戶定義對象實例(或者近似稱為用戶表記錄)數據以 Data 表中一條記錄的形式存在。Product、Customer、Order 表的數據記錄均存儲在 Data 表,OrderItem 也亦是如此。

其中,GUID 作為每條數據記錄暨是每個對象實例的全局唯一標識,OrgID 進行多租戶數據隔離,ObjID 同 Objects 表關聯代表具體哪個對象定義。這裡重點提一下,Fields 中定義的對象字段在 Data 表中的存儲,其中 Fields 表中 FieldNum 非常關鍵,它對應了對象實例字段在 Data 表中的具體存儲位置,FieldNum 對應數字決定著數據存儲在 Data 表中的哪個 ValueX 列。前面每個對象結構定義都對 FieldNum 對應 Data 的進行了說明,對象字段 FieldNum 可以不按照順序來,只要 FieldNum 沒有佔用,可以任意對應,當然按照順序是比較好的實踐。

再舉例來說:

  • Order 對象的 Customer 關係字段定義在 Fields 表中,其 FieldNum 為 1,則其在 Data 表中存儲的位置,就是是 Order 對象實例在 Data 對應的記錄中 Value1 這個字段所存儲的值,存儲的值為 Customer 對象實例 GUID,也就是:a062v00001YXEKuAAP、a062v00001YXEKzAAP 等。

  • OrderItem 對象的 Product、ItemQuantity 字段定義在 Fields 表中,其對應的 FieldNum 分別為2、4,則其在 Data 表中存儲的位置,就是 OrderItem 對象在 Data 對應的記錄中 Value2、以及 Value4 所存儲的數據,也就是:a052v00000jbgEQAAY、2以及a052v00000jbgMqAAI、3 等記錄。

image.png

image.png

7. 通用的存儲,按需轉換 —Data 表數據類型與存儲

我們看了元數據驅動的多租戶模型的核心關係,明白了用戶自定義表(包括應用系統表)以及表結構是在 Objects 和 Fields 進行虛擬定義的,也清楚的知道了系統以及用戶表的數據是作為一條條記錄存儲在 Data 表中的,那麼我們下面來看下不同的數據類型如何在 Data 中進行存儲的呢?

在 Fields 表中,可以採用任何一種標準的結構化的數據類型,如 text,number,date,以及 date/time 對用戶表字段進行定義,也可以採用特殊結構的數據類型對字段類型進行定義,如下拉框 picklist,系統自增字段 autonumber,公式列(只讀的公式推導列),布爾多選框,email,URL 以及其他的類型,當然也可以通過系統應用來對 Fields 中的自定義字段進行強制約束包括是否必須非空以及掐校驗規則(如符合特定格式,符合特定值範圍等)。

上述的各種不同字段格式數據都是存儲在 Data 表中的 ValueX 列中的,Data 表中包含 500 個數據列,稱為彈性列,用來存儲用戶數據和系統數據,也就是對應到 Objects 表和 Fields 表對應的虛擬表結構所要承載的數據。

特別的,所有彈性列都用了一個可變長度的字符串類型,以便於他們可以存儲任何結構化類型的應用和用戶數據(字符串,數字,日期等)。

正是因為彈性列把所有不同的數據類型拉平來存儲,所以任一彈性列可以對存儲任何對象的任何類型的屬性來存儲,用戶可以指定不同的對象的不同屬性對應的不同的存儲彈性列,當然同屬於相同對象的實例的屬性對應的彈性列是一致的。一個彈性列可以存儲來不同的格式的數據,前提條件是這些數據屬於不同的對象的不同屬性。例如:上一節示例中,Data 表的 Value2 列可以存儲 Order 表的日期格式的 OrderTime 數據,也可以存儲 OrderItem 表的格式為字符串的 OrderID 數據。

image.png

如上所述,彈性列用通用數據類型暨可變長字符串來存儲所有類型的數據,這樣就可以在不同的用戶表字段間共享相同彈性列,即便它們的數據類型各異。

既然所有的數據全部用通用的可變長字符串來存儲,那麼應用邏輯處理需要不同的數據格式時候怎麼辦呢?具體做法如下:

當應用系統需要從彈性列讀取和寫入數據時候,UDD(Universal Data Dictionary) 層暨元數據運行引擎會用底層數據庫系統數據轉換函數(如 Oracle 數據庫的TONUMBER,TODATE,TO_CHAR 函數)按需對數據格式進行轉換,將字符串格式轉換成對應的數據格式(數字,日期等)。

如果存儲非結構化的大文本塊數據怎麼辦呢?模型支持對Clob大字段的定義,對於在 Data 表中具有 CLob 數據的每一行數據,系統將其存儲在 Clobs 透視表中,並按照需要同 Data 表的對應數據對象實例記錄進行關聯。

8. 多租戶索引透視表 (Pivot Tables)

1)Indexes 透視表

大多數結構化的數據存儲在 Data 表內,如前面提到的,所有這些不同類型數據都是以可變字符串的形式存在 ValueX 列裡面如各種數字以及日期等全部都是以可變字符存儲的,這樣雖然對於對象實例各種字段的存儲確實非常靈活,不同的列可以存儲不同類型的數據,即使同一 ValueX 列不同的對象也可以存儲類型的數據,但是這樣帶來一個巨大的問題,由於不同的數據類型以可變字符串的方式存儲在同一列內,你沒辦法利用底層數據庫索引的能力對其進行排序,ValueX 列的數據都是一種按照離散的順序來存儲的。傳統的數據庫依賴原生的數據庫索引來快速在數據表內定位到符合查詢條件的記錄。而按照 Data 表ValueX列的數據存儲情況,在 Data 表建立 ValueX 列的索引來支撐數據快速查詢是不現實的。

所以解決辦法就是建立另外的透視表叫做 Indexes 索引表,並把數據拷貝出數據表並轉換成原始的的數據類型,並存儲到Indexes索引表列內,如原來是整形的數據以可變字符串的格式存儲 在ValueX 列中,拷貝到 Indexes 表之前通過函數將其轉換為原始的數據類型,在存儲到 Indexes 對應的 NumValue 列內,以方便建立索引,Indexes 表包含強類型的索引類,像 StringValue,NumValue,DataValue,用來定位對應數據類型的字段數據。

image.png

Indexes透視表的字段說明如下:

  • OrgID:其所歸屬的應用對象所歸屬的租戶OrgID
  • ObjID:字段所屬應用對象唯一標識
  • FieldNum:對象字段存儲位置
  • ObjInstanceGUID:對象實例唯一標識
  • StringValue:強類型的字符串列
  • NumValue:強類型的數字列
  • DateValue:強類型的日期列

下面的 Indexes 表示例包含對字符、數字和日期性數據的索引需求支持,數據來源於前面的 Data 表數據。

image.png

image.png

image.png

Indexes 表的底層索引是標準的,採用非唯一性的數據庫索引。當做對象檢索查詢的時候,實際上不是在Data數據表上做查詢,而是在 Indexes 索引表上做的查詢,獲取到OrgID,ObjectID 以及 GUID,然後再返回數據表獲取數據。也就是當系統查詢條件包含對象實例的結構化的字段時,系統查詢優化器採用 MT_Indexes 來幫助優化相關的數據訪問操作。

2)Unique Indexes透視表

由於 Data 數據表的多數據類型的無差別存儲,無法在 Data 數據表建唯一性的索引供用戶來使用對對象字段值進行唯一性校驗。為了支持用戶對象自定義字段的唯一性校驗,解決辦法是採用了 UniqueIndexes 透視表;這個表非常類似於 Indexes 表,不過 Uniqueindexes 採用底層原生的數據庫索引來強制唯一性校驗。當一個用戶嘗試來插入一個重複的值到具有唯一性約束的對象字段時,或者當用戶嘗試去在一個現存的包含唯一性的字段進行強制唯一性時,系統會給出唯一性校驗失敗的提示,阻止用戶的下一步操作。

image.png

  • Unique Indexes 透視表的核心字段說明如下:
  • UniqueStringValue:唯一的字符串列
  • UniqueNumValue:唯一的數字列
  • UniqueDateValue:唯一的日期列
  • 其他字段定義請參考 Indexes 透視表

3)Relationships 索引透視表

在元數據驅動的多租戶模型中,提到了在 Objects 表以及 Fields 表中保存了用戶對象結構和對象關係的定義,對象關係的定義是通過元數據模型 Fields 表字段數據類型提供了一個特殊的數據類型:“關係” (Relationship), 來給用戶用於聲明不同的用戶應用對象之間的關係,也就是我們通常說引用完整性。

對象之間的引用關係定義以及對象實例間的引用關係存儲在元數據表 Objects、Fields 中和 Data 表中,關聯查詢關係複雜,為了提升對象之間查詢的效率,特別是通過對象相互引用關係對對象實例數據進行檢索,系統提供關係索引透視表 Relationship 來優化對象引用關聯查詢。

image.png

Relationships 索引透視表的字段說明如下:

  • OrgID:其所歸屬的應用對象所歸屬的租戶 OrgID
  • ObjID:子對象的對象標識
  • GUID:子對象實例的唯一標識
  • RelationID:子對象內關係字段定義的標識
  • TargetObjInstanceID:父對象實例的唯一標識

關係透視表 Relationship 定義了兩個底層數據庫複合索引:

  • 第一個索引字段:OrgID + GUID,用於從子對象到父對象的關聯查詢。
  • 第二個索引字段:OrgID + ObjID + RelationID + TargetObjInstanceID,用於父對象到子對象的關聯查詢。

Relationships 索引透視表會在後面 SOQL 章節進行進一步描述驗證。

4)其他索引透視表

其他索引透視表的邏輯類似,都是為了滿足特定檢索和查詢需要,將數據同步到索引表,供應用系統使用。此處不再贅述,如確實有需要再補充。

五、SOQL 與關係 Relationships

SOQL 是 Salesforce Object Query Language 的簡稱,具有 SQL 類似的語法結構,就像前面提到的一樣,Salesforce 是以應用對象(Salesforce Object,簡稱 SObject)的視角管理業務數據和功能,SOQL 類似對用於對應有對象數據進行查詢的 API。

1. 從SQL 到 SOQL

SOQL 也是採用類似表查詢的結構,同 SQL 非常相似,也通過底層數據庫索引來提供查詢優化支撐。不同點如下:

  • 沒有 select *
  • 沒有視圖概念
  • SOQL 是隻讀的
  • 由於底層元數據驅動的多租戶數據模型的限制,索引是受限制的,沒有原生數據庫物理結構豐富的索引支持。
  • 對象到關係的映射 (Object-Relational Mapping) 是自動完成的。
  • SObjects 在多租戶環境中並不是對應實際的物理數據表。
  • SObjects 包括 SObjects 之間的關係都是以元數據的方式存儲在多租戶環境中的。

2. SOQL 示例&語法

下面我用示例來說明一下 SOQL 的用法,同時引出SOQL的特殊語法說明,SOQL 大小寫不敏感。

1)單個對象的查詢及語法說明

select id,productno__c,name,productprice__c,productstatus__c from product__c

image.png

前面提到過系統提供了標準應用對象和標準字段定義,更大的優勢在於支持用戶自行自定義對象和字段。這裡c 代表的使用戶自定義的含義, productc 代表的用戶自定義對象 Product,而非系統標準對象和字段,系統標準對象和字段在 SOQL 無需c 後綴,如ID,Name,CreatedBy 等字段則為系統提供給每個對象的標準字段,而字段 ProductNo 為用戶自定義字段,則 SOQL 中的語法表示為 productnoc。這樣的好處是講標準和用戶自定義對象和字段很容易區分開,系統可以定義標準 Product 對象,以 product 表示,用戶也可以同樣定義一個 Product 對象,不過 SOQL 用 product__c 表示用於區分。

2)子對象關聯父對象 (Child to Parent) 查詢及語法說明

select id,name,orderno__c,
customer__c,
customer__r.customerno__c,customer__r.name,
orderstatus__c,ordertime__c
from order__c order by orderno__c

image.png

select id,name,orderno__c,
​
customer__c,
customer__r.customerno__c,customer__r.name,
orderstatus__c,ordertime__c
from order__c
where customer__r.name='Cheng Yan'
order by orderno__c

image.png

這裡是從子對象 Order 關聯到父對象 Customer 進行查詢,其中:

  • from 後面的對象 order__c 表示 Order 為用於自定義對象
  • Id,name 為 Order 對象內系統定義的標準字段
  • Ordernoc,customerc,orderstatusc,ordertimec 為用戶自定義字段,這裡需要說明的是 customer__c 自定義字段存儲的是父對象實例 ID
  • customerr 就特別有意思,其中r 部分代表父對象關係引用,customer 部分對應關係字段名,customerr 代表從 Order 對象到 Customer 對象的一個應用關係,並通過 customerr.customernoc,customerr.name 獲取到 Customer 對象的字段值。

3)父對象關聯子對象 (Parent to Child) 查詢及語法說明

select id,orderno__c,customer__r.name,ordertime__c,orderstatus__c,
(
select id,
product__r.productno__c,product__r.name,product__r.productprice__c
from orderitem__r
)
from order__c
order by orderno__c

image.png

這個語句稍微有些複雜,從 Order 對象關聯到 OrderItem 對象,又從 OrderItem 關聯到 Product,同時還包含了 Order 對象到 Customer 對象的關聯。

這裡著重說一下從父對象到子對象的關聯,父到子的關聯是在父對象的主查詢語句中在查詢字段中用()來封裝到子對象的關聯,其中

  • 子句中 from orderitemr 的 orderitemr 代表的是對子對象 OrderItem 的引用,orderitem 對應的為前文關係字段中提到的 ChildRelationshipName,並且同一個父對象的子方的關係名稱唯一(父對象 Name+ChildRelationshipName 必須唯一),用作父對象到子對象的查詢關聯。
  • 子句中 id,productr.productnoc,productr.name,productr.productpricec 的上下文為 orderitemr 代表的子對象。

3. Relationships 索引透視表

Relationships 是為了 SOQL 的快速對象關聯查詢所定義的,子對象關聯父對象( Child to Parent) 查詢,複合索引(OrgID+GUID)在 Join 中起到較大作用,而需要從父對象關聯子對象 (Parent to Child) 查詢,則複合索引 (OrgID + ObjID + RelationID + TargetObjInstanceID) 在 Join 中起到較大作用。

六、如何支撐多租戶巨大數據量

前面我們提到 Salesforce 一個共享數據庫的概念,那一個共享數據庫怎麼來支撐如此巨大的多租戶數據庫呢,同時不僅需要支持巨量數據,並且還可以支撐租戶間的數據物理隔離,保證各租戶的數據穩定性、可用性和數據安全?

Salesforce 的做法是:分區。所有的 Force.com 的數據,元數據,透視表結構,包含底層數據庫索引,都是通過對 OrgID 進行物理分區的,採用的是原生的數據庫分區機制。所有的數據以及元數據通過你的 OrgID(16digits)進行分片 Hash。

數據分區是數據庫系統提供的被驗證過的技術,用以物理劃分較大的邏輯數據結構到較小的可以管理的區塊中。分區也可以幫助提升性能和擴展性,貼別是在多租戶環境下一個巨大的數據系統的擴展性。根據定義,每一個 SOQL 的查詢對應一個特別的租戶信息,因此查詢優化器,僅僅需要考慮訪問包含對應租戶的數據分區訪問,而不是整個表或者索引。

七、無感的對象結構變更(No DDL)

當一個應用系統或者服務組件需要對其數據模型進行升級的時候,通常會通過數據庫 DDL 語言對數據庫物理結構進行操作,如果涉及的數據量較大,則可能會造成較長時間的數據庫變更時效,造成對應時間內的系統不可用,如果是多租戶系統還會可能其他租戶的可用性造成影響,抑或造成諸多的底層模型不一致產生。

在元數據驅動的數據架構中,所有的 DDL 語言操作對應的使元數據層的元數據的記錄的更新,不涉及數據庫物理結構的更新,不會造成變更期間的數據庫物理結構耗時調整造成的不可用,同時系統平臺提供了一個高效的機制來減少對平臺多租戶應用總體性能影響。

當用戶修改了一個表字段列的數據結構,從一種數據類型改成另外一種不同存儲格式的數據類型時候,系統會重新分派一個新的彈性列給到這個字段列的數據,將數據從原來的存儲彈性列批量拷貝到新的彈性列,然後才會更新此字段列的元數據,暨在 Fields 表中更新這個字段列的元數據,將數據類型更改為新的數據類型,並將 FieldNum 更新為新的 ValueX 列對應的X值。

同時,在如上對用戶邏輯表結構調整生效過程中,原來的數據結構和對應的數據訪問正常進行,直到邏輯表結構變更生效,對應用系統可用性不會造成影響,用戶對此無感知。

八、多租戶架構對於研發人員意味著什麼

對於研發人員來說,多租戶結構最多意味著兩個版本:當前版本,以及下一個版本。沒有遺留版本需要維護。所有人不用操心舊的技術,舊的版本,所有隻有最新的版本,只需要關心最新的版本。

這樣就給敏捷開發帶來極大的好處,每年做個位的發佈,每次發佈幾百個新的特性新的版本也不會改變用戶的體驗,新的特性可以根據用戶需要開啟,通過特性管理來開關。

新版本發佈前,提供沙箱環境來允許用戶提前試用新版本的系統。如果做 bug 修復,則是在所有租戶層面上進行統一修復的。

對於用戶應用的發佈進行嚴格管理,防止對其他租戶產生影響,通過提供沙箱環境來讓用戶驗證新應用發佈,並通過成千上萬的自動化測試保證用戶的正常功能。

在運行期間,不作任何底層 DDL 操作,不會做表的創建,也不會做表的變更,只可能在極少數的更新週期時候進行。

作者簡介:程彥,曾就職於阿里數字供應鏈事業部擔任多年供應鏈計劃域研發,目前在阿里數據中臺負責相關商業化產品開發。

掃碼瞭解更多技術內容與客戶案例:

image.png

Leave a Reply

Your email address will not be published. Required fields are marked *