(本文適于使用Aaf框架的開發者閱讀)
1. 基本原理
在Aaf框架中,“對象”和“存儲”的關系映射有一個關鍵的紐帶StorageAlias,即“存儲別名”,同樣一個類型,在不同的存儲別名下,可以自由映射到任意存儲“位置”。
“位置”有兩個元素決定,一個是存儲上下文StoargeContext,另外一個是數據表名TableName。缺省的StorageContext在Persistence.Config中配置,缺省的數據表名就是類的名稱。
所有的映射關系,存儲在兩個地方,一個地方是TypeDescription的ExtendedAttributes屬性中,這里存儲的映射主要來源于 配置文件Persistence.Config中的配置。關系的建立是在Aaf啟動階段,由PersistenceMappingService初始化完 成,調用StorageContextMappingService中的GetStorageAliases方法取得 Persistence.Config中的存儲別名的配置信息。此外,默認的存儲上下文和默認的表名,也初始在此擴展特性中。
通過Persistence.Config方式配置的映射,是“相對穩定”的映射,數量也在一個非常有限的級別上,例如我們可以將訂單信息按照 交易類型的不同,存儲到不同的數據庫上,也可以將交易完畢的訂單信息搬遷到歷史庫上,我們就可以定義出來形如 “EscortOrderByHistory”這樣的存儲別名,一看就知道是“擔保交易的歷史交易數據”。
另外一個存儲映射關系的地方是存儲別名信息助手StorageAliasesInfoAssistant,助手的映射關系有三個來源:IStorageAliasTeller提供、手動動態添加、數據庫參數配置。
通過IStorageAliasTeller方式,我們可以根據系統要求,動態 的解析一個存儲別名,不僅如此,我們還可以根據AgileObject.Id自定義規則,推導出其對應的存儲別名,例如我們可以將用戶按照一定規則分表存 儲,然后在用戶的Id中包含存儲別名的信息,這樣,我們就可以非常方便的將海量用戶拆分到若干分表當中,實現數據的分表存儲。此外,與IDataRadiationClassifier搭 配起來,還可以實現“多維存儲”, 就是將同樣一份數據,按照不同的應用需要,散射到多個存儲位置(有可能是散射到不同的數據庫上,例如商品信息散射出一個專門供查詢的庫上;也有可能是同一 數據庫的不同表上,例如訂單信息根據買賣不同散射到特定的數據表上。),同時自動維護各個數據庫上數據的一致性。這樣就可以分身有術,讓多個的數據庫分擔 查詢壓力。
手動動態添加,是通過IStorageAliasesInfoAssistant的RegisterDynamicStorageAlias方法,將已知存儲位置添加到映射表中,同時返回一個GUID碼作為存儲別名。
數據庫參數配置,這種情形是存儲映射信息是以參數的形式存儲在數據庫中的, 在Aaf.ParaService加載過程中,會自動生成一個Aaf.StorageAliases的配置節點,節點路徑是Aaf.StorageAliases/[StorageAlias](存儲別名,可配多個),節點配置格式是:
[
StorageContext1,TableName1 \n
AppId1: StorageContext2,TableName2 \n
AppId2: StorageContext3,TableName3 \n
]
其特別的地方是在分布式環境中,根據AppID的不同將數據持久化到特定的存儲上。
?
?
2. 應用實例
在5173系統中,有許多應用,尤其以用戶、發布單、訂單及資金明細比較典型。下面我們一起去看看。
2.1. 用戶數據分拆
用戶數據是基礎數據,而且是一種會持續增長的基礎數據,這種數據的膨脹會讓其性能表現越來越差,因為是基礎數據又不能刪除或者搬移,我們處理這種數據的基本策略是將所有用戶按照一定的策略散射到多個數據表中,以緩解單表的壓力,下面我們分析一下實現過程:
1、 將所有用戶數據按照注冊日期段分組,每一個分組共用一個獨立的數據表,分拆存儲,分拆策略啟用有一個時間基線,之前的數據依然存儲在UserInfo中, 自此時間基線之后的數據便存儲在形如UserInfo_X的數據表中。例如,設立時間基線為2009-5-22,每隔90天建立一個新組,某用戶注冊時間 為2009-12-2號,與時間基線相差194 天,除以90得2,存儲在UserInfo_2中。
2、 創建一個類UserInfoSaTeller,實現IStorageAliasTeller接口
3、 將UserInfoSaTeller注冊到別名信息助手,注冊方法如下: PersistenceMappingService.StorageAliasesInfoAssistant.RegisterTeller(typeof(User), new UserInfoSaTeller()),這個注冊在UserService的Run方法中。
4、 保存新用戶時,內核將調用UserInfoSaTeller的GetPrimarySaveStorageAlias方法,取得存儲別名,傳入參數是靈便對象的Id號,因此我們需要構建一個含有分表特征信息的Id,我們現用的Id為“US09120282660351-00F7”,其中紅色加粗部分為注冊日期,通過注冊日期,我們可以計算出一個特定的存儲別名,UserInfo$2, 內核將繼續調用UserInfoSaTeller的GetTableName,傳入參數就是GetPrimarySaveStorageAlias計算出 來的“UserInfo$2”,很顯然我們輕易可以得出實際的存儲表名“UserInfo_2”。可見,我們只要按照規則構造好用戶的Id,程序便可以自 動識別出來應該放到哪里存儲。
5、 根據Id號取用戶實例的過程與保存過程基本類似,內核調用UserInfoSaTeller的GetPrimaryLoadStorageAliases方法,取得加載時存儲別名,之后的映射關系與對象創建時相同。對于用戶數據,保存和獲取時的存儲別名是一致的。
6、 登陸時怎么辦?登陸使用的用戶名進行登陸,而不是用戶Id。沒什么太好的辦法,基本思路還是遍歷所有分表,好在,我們的分表時間區間都是以月為單位,十年 下來也不過一百多個表。此外,當一次遍歷完成之后,我們可以將此用戶對應的存儲別名狀態記錄在某個地方,例如Cookie中,這樣減少遍歷的頻度。
2.2. 發布單數據分拆
發布單數據的拆分策略稍微復雜一點。根據實際應用需要,除了發布單默認的存儲位置之外,發布單還有幾種存儲形態:根據用戶分組、根據游戲、交易 完成的歷史數據。根據用戶分組分拆,是指把所有用戶在邏輯上分成若干個用戶組,每個用戶組共用一個存儲表;根據游戲更容易理解,即每一款游戲共用一個存儲 表;交易完成的歷史數據,就是那些單子不會再發生變化,這種數據,主要通過數據庫Job定期搬移的。與用戶數據分拆不同的是,發布單是一份數據在不同的地 方用不同的策略存儲,即多維存儲(游戲維度、用戶分組維度)。下面我們分析一下數據創建和加載的過程:
1、 先看創建過程。因為是多維存儲,BizOffer需要實現IDataRadiationClassifier接口,Aaf內核在工作時將調用其中的GetDataRadiationClasses方法,來獲取每個維度的維度標識。
2、 使用維度標識,調用IStorageAliasTeller的GetDimensionStorageAlias方法,翻譯出維度對應的存儲別名,形如:Search$0043和BizOfferBy023F-Escort,加黑的分別是“游戲標識”和“用戶邏輯分組標識”。
3、 解釋下用戶邏輯分組的來歷,我們將用戶分成1000個邏輯小組,分組的方法依然與注冊時間、時間基線相關,用兩個時間的差(天數)對1000取模,這樣就 會得到一組數字,將這組數字用四位16進制格式化,就是散射用戶的邏輯分組標記,這個標記,我們也在用戶Id中記錄了,“US09120282660351-00F7”,即藍色加粗部分。當一個用戶創建發布單的時候,我們便知道其邏輯分組為00F7。游戲標識要簡單一些,每一款游戲都會有一個自增長的表號,此表號也是用四位16進制表示。注意,用戶分組目前是固定為1000個組的,而游戲分組是不斷增長的。
4、 接下來就是分別對每個維度的存儲別名進行存儲動作,通過IStorageAliasTeller的GetContextName和GetTableName,取得實際的存儲位置信息。
兩個維度路徑是:
存儲別名–>存儲上下文名稱–>數據庫名稱–>數據表名
游戲散射:Search$0043–> RadiationOffer1–> SearchOffer–> BizOfferby0043
用戶散射:BizOfferBy023F-Escort–>OfferDataRadiations–>OfferDR–>BizOfferBy023F
5、 還有一個維度是歷史庫的發布單數據,這個是Job搬遷創造的,數據表的命名規則是一個月三張表,按旬存儲,形如:BizOfferby200607_1
。雖然創建過程不是Aaf做的,但查詢數據時是Aaf做的,后面我們在做查詢分析的時候要考慮這部分數據。
6、 在單條數據加載的時候,重點是確定查詢策略了,就是什么數據該先從哪個維度去找,策略確定之后,就是拼存儲別名來定位查詢的數據庫表了。我們現在的查詢策 略是先查找當前庫,拼EscortByCurrent的存儲別名,此存儲別名在Persistence.Config中配置了映射關系;接著檢查“用戶分 組維度”;再接著,查找歷史庫。
幾個查詢路徑如下:
當前庫:EscortByCurrent–>ConsignmentByCurrent–>Consignment–>BizOffer
用戶散射:BizOfferBy023F-Escort–>OfferDataRadiations–>OfferDR–>BizOfferBy023F
按發布單結束時間歷史庫(后臺使用):Escort|200912_1–>OfferHistory–>OfferHistory–> BizOfferby200912_1
按發布單創建時間歷史庫(前臺使用):
Escort~200912_1–> OfferHistorybyCreatedDate–> OfferHistorybyCreatedDate–> BizOfferby200912_1
7、 接下來我們梳理一下各種發布單列表是怎么出來的,當一個查詢過來的時候,我們首先判斷查詢條件中是否含有用戶信息,如果有,直接走“用戶分組維度”。如果 沒有用戶信息,至少會包含一個游戲信息,即GameId,走游戲維度的存儲。注意,列表查詢沒有走“默認維度”,這樣默認維度就可以專心用于交易了。
8、 8、 有關發布單查詢,還沒有結束。就是另有一個發布單搜索對象BizOfferSearch,這個是另外生成的一份數據,專門供搜索用,與當前交易平臺隔離。同樣也有兩個維度:
存儲別名–>存儲上下文名稱–>數據庫名稱–>數據表名
游戲散射:Search$0043–> RadiationOffer1–> SearchOffer–> BizOfferSearchby0043
用戶散射:BizOfferSearchBy023F-Escort–>OfferDataRadiations–>OfferDR–>BizOfferSearchBy023F
2.3. 訂單數據分拆
訂單的數據分拆方法類似,根據實際應用需要,除默認存儲外,也有幾個維度的存儲:買家分組散射、賣家分組散射、歷史庫數據。從技術實現方式來講完全一個套路。。
1、 訂單創建過程與發布單雷同,不再贅述。需要注意的是,買賣維度的數據表名中也是附加了用戶分組標識。一筆訂單,會因為買家和賣家的分組不同,而存在于不同標識的分表中。
2、 訂單取得過程,先從默認庫查找,找不到再去買家維度找,找不到再去賣家維度找,還找不到就到歷史庫中找。
維度路徑是:
存儲別名–>存儲上下文名稱–>數據庫名稱–>數據表名
買家散射:OrderByBuyer023F-Escort–> OrderDataRadiations–> OrderDR –> OrderByBuyer023F
賣家散射:OrderBySeller009B-Escort–> OrderDataRadiations–>OrderDR–> OrderBySeller0339
歷史庫:Escort|200912_1–>OrderHistory–>OrderHistory–> Orderby200912_1
3、 在前臺列表查詢中,買賣雙方呈現自己的訂單時,是分別從買賣維度即OrderDR這個庫中取數據的。后臺列表查詢,走的是默認維度的訂單庫。同時,對于交易完成的數據,到歷史庫OrderHistory中查詢。
2.4. 資金分表
AccountDetail,除默認在外,還有用戶分組維度、歷史庫維度。
1、 多維存儲路徑:
存儲別名–>存儲上下文名稱–>數據庫名稱–>數據表名
用戶散射:AccountDetailBy00F7–>DataRadiations–>BkDR–> AccountDetailBy00F7
歷史庫:200912_1–>BkHistory–> BkHistory–> AccountDetailBy200912_1
2、 特別要說明的是,每種資金類型都有自己的獨立的類,他們如何共用AccountDetail這樣一個存儲的呢?原來所有的獨立的資金類型都派生自 AccountDetail,而AccountDetail中設置的“[AgileObjectStorage(TableName = "AccountDetail", ContextName="BkUser")]”,也被繼承下來了。其實更重要的是它們也繼承了的IDataRadiationClassifier實 現,此外,在PaymentService的Run方法中,幾乎所有類型都注冊了AccountDetailSaTeller,于是實現了所有類型的存儲 規則統一。
2.5. 映射列表
以下是開發環境中的映射關系列表:
對于這些類型,如果要修改字段,每一個關聯維度上對應的分表都要變更,我們經常碰到的缺字段,就是某個維度上有缺失。
類型 | 數據庫名 | 數據表名 | 存儲上下文名 | 存儲別名 |
User | BkUser (默認主庫) | UserInfo_1 UserInfo_2… UserInfo_n | BkUser | UserInfo$1 UserInfo$2… UserInfo$n |
BizOffer | Consignment (默認主庫) | BizOffer | ConsignmentByCurrent | EscortByCurrent |
SearchOffer (游戲散射) | BizOfferby0000 … BizOfferBy07D0… | RadiationOffer1 | Search$0000 … Search$07D0… | |
OfferDR (用戶散射) | BizOfferBy0000 … BizOfferBy03E8 | OfferDataRadiations | BizOfferBy0000-Escort … BizOffeBy03E8-Escort | |
OfferHistory (歷史庫By交易完成時間) | … BizOfferby200912_1 BizOfferby200912_2 BizOfferby200912_3 … | OfferHistory | … Escort|200912_1 Escort|200912_2 Escort|200912_3… | |
OfferHistorybyCreatedDate (歷史庫By創建時間) | … BizOfferby200911_1 BizOfferby200911_2 BizOfferby200911_3 … | OfferHistorybyCreatedDate | … Escort~200911_1 Escort~200911_2 Escort~200911_3… | |
BizOfferSearch | Consignment (默認主庫) | BizOfferSearch | ConsignmentByCurrent | EscortByCurrent |
SearchOffer (游戲散射) | BizOfferSearchby0000 … BizOfferSearchBy07D0 … | RadiationOffer1 | Search$0000 … Search$07D0 … | |
OfferDR (用戶散射) | BizOfferSearchBy0000 … BizOfferSearchBy03E8 | OfferDataRadiations | BizOfferSearchBy0000-Escort … BizOfferSearchBy03E8-Escort | |
Order | Consignment (默認主庫) | Order | ConsignmentByCurrent | EscortByCurrent |
OrderDR (買家散射) | OrderByBuyer0000 … OrderByBuyer03E8 | OrderDataRadiations | OrderByBuyer0000-Escort … OrderByBuyer03E8-Escort | |
OrderDR (賣家散射) | OrderBySeller0000 … OrderBySeller03E8 | OrderDataRadiations | OrderBySeller0000-Escort … OrderBySeller03E8-Escort | |
OrderHistory (歷史庫) | … Orderby200912_1 Orderby200912_2 Orderby200912_3 … | OrderHistory | … Escort|200912_1 Escort|200912_2 Escort|200912_3 … | |
AccountDetail | BkUser (默認主庫) | AccountDetail | BkUser | Current |
BkDR (用戶散射) | AccountDetailBy0000 … AccountDetailBy03E8 | DataRadiations | AccountDetailBy0000 … AccountDetailBy03E8 | |
BkHistory (歷史庫) | .. . AccountDetailBy200912_1 AccountDetailBy200912_2 AccountDetailBy200912_3 … | BkHistory | … 200912_1 200912_2 200912_3… | |
說明: | Escort,是交易類型,此處只是舉例,應用中可能是其它標識。 |
除了上面的映射,自定義映射還有一些,順著IstorageAliasTeller的實現類去尋找吧。