消息復(fù)雜計算的抽象和簡化
作者 | 四點
文章來源 | 阿里巴巴淘系技術(shù)
消息客戶端計算的復(fù)雜性
在客戶端的設(shè)計中,一般的分層會至少包含下層的數(shù)據(jù)服務(wù)層和上層的UI層,下層的數(shù)據(jù)模型主要由所在領(lǐng)域決定,相對獨立、穩(wěn)定,而UI則更多變,且會對多種數(shù)據(jù)進行組合。由于UI的相對多變性與模型的相對穩(wěn)定性,在數(shù)據(jù)層和UI之間,就需要對數(shù)據(jù)進行若干的處理才能交給UI展示。比較簡單的情況比如將原始數(shù)據(jù)的時間戳轉(zhuǎn)換為 PD 要求的字符串,如果涉及到對不同數(shù)據(jù)進行關(guān)聯(lián)、分頁加載、變更計算,這部分數(shù)據(jù)處理邏輯就會比較復(fù)雜。
消息作為富客戶端,這部分邏輯非常復(fù)雜,加上狀態(tài)的存在,可以說是消息客戶端中最復(fù)雜的邏輯之一,這種復(fù)雜主要體現(xiàn)在這些維度:
1.本地部分數(shù)據(jù):客戶端只存部分消息數(shù)據(jù),獲取數(shù)據(jù)時本地數(shù)據(jù)不全需要再異步請求服務(wù)端,還需要支持上層指定請求策略,這使得接口無法采用 request-response 的形式,必須使用流式接口,數(shù)據(jù)回調(diào)和結(jié)果回調(diào)的分離,以及多次數(shù)據(jù)回調(diào),增加了處理邏輯的復(fù)雜度;
2.支持變更同步:除了主動拉取,會話消息數(shù)據(jù)需要支持變更的推送,且對于所有的變更,需要保證數(shù)據(jù)(包括緩存和UI展示)的一致性;
3.多個數(shù)據(jù)來源:由于歷史的原因,消息的同一種數(shù)據(jù)(比如會話)存在多個來源,因此需要請求多次,將多次數(shù)據(jù)回調(diào)合并,處理錯誤,還要盡量保證加載速度。經(jīng)過眾多同學的努力,手淘和千牛中下掉了OpenIM、DT兩個數(shù)據(jù)源,今天用戶在手淘和千牛中看的會話和消息,依然有BC、CC、IMBA三個來源;
4.多種數(shù)據(jù)聚合:UI展示需要把會話、消息、Profile(頭像昵稱)、群、群成員信息以及其他業(yè)務(wù)數(shù)據(jù)進行聚合,把相關(guān)的多個不同數(shù)據(jù)按照不同的規(guī)則聚合在一起;
5.支持分頁請求:總的數(shù)據(jù)量比較大,需要通過分頁機制加載,除了標準的分頁加載之外,還要支持定位到某條消息從中間開始加載,這就出現(xiàn)了雙向分頁加載,以及 進入和退出 中間加載時的狀態(tài)轉(zhuǎn)換和異常處理;
6.多條數(shù)據(jù)合并:由于業(yè)務(wù)的需要,消息之間存在更新和替換關(guān)系(比如同一條訂單的物流狀態(tài)更新),拉取的新數(shù)據(jù)要修改已存的消息狀態(tài)數(shù)據(jù),而非僅僅添加在頭部或尾部,新的消息會導(dǎo)致已有消息的更新以及在數(shù)據(jù)結(jié)構(gòu)中的位置變化;
7.數(shù)據(jù)結(jié)構(gòu)復(fù)雜:消息存在列表、樹兩種UI形態(tài),對應(yīng)的狀態(tài)也有兩種形態(tài),對于這些數(shù)據(jù)結(jié)構(gòu)的變更計算邏輯比較復(fù)雜,對于樹來說,還需要支持虛擬節(jié)點計算和結(jié)構(gòu)動態(tài)變化。
這塊邏輯在消息客戶端涉及會話、消息、profile、群、群成員、關(guān)系等所有核心服務(wù)數(shù)據(jù)模型,總計大約25000行代碼,占消息總代碼的8%左右,是核心的數(shù)據(jù)處理。由于這些邏輯很容易耦合在一起,形成一些高維的邏輯,表現(xiàn)為大量的條件分支和遞歸嵌套,這種高維的邏輯很難寫,也很難維護,并且占據(jù)了不少包大小,因此有必要對這些邏輯抽象和簡化。
目標
1.在不同的模型、不同的接口和相似的邏輯之上再建立一層抽象,統(tǒng)一客戶端的數(shù)據(jù)處理;
2.將高維的數(shù)據(jù)處理邏輯簡化為一個更加清晰的處理模型,代碼量下降60%;
3.實現(xiàn)數(shù)據(jù)處理的雙端一致。
消息數(shù)據(jù)處理過程分析
通常會將客戶端劃分為數(shù)據(jù)服務(wù)層、邏輯層、UI層三層,這部分數(shù)據(jù)獲取和計算會被歸到邏輯層。這里的問題在于,數(shù)據(jù)服務(wù)層對應(yīng)于領(lǐng)域定義,UI層對應(yīng)于渲染、動畫和交互事件處理,這樣邏輯層很容易變成一個縫合怪,數(shù)據(jù)請求、數(shù)據(jù)轉(zhuǎn)換、上下文維護、異步處理、遞歸邏輯、狀態(tài)管理、變更同步,所有不屬于另外兩層的部分都會被扔到邏輯層,導(dǎo)致邏輯層的臃腫。
下圖左側(cè)是這個處理過程的工作內(nèi)容和上下游,右側(cè)為數(shù)據(jù)拉取和變更處理的數(shù)據(jù)流向和計算過程:
可以看到,將這部分數(shù)據(jù)處理僅僅定義為邏輯是過于寬泛的,不利于針對性的優(yōu)化,因此有必要進行深入的分析和研究。
在對會話、消息、profile、群、群成員、關(guān)系6大核心數(shù)據(jù)處理鏈路進行歸納、分解、分析和綜合之后,我們可以將數(shù)據(jù)處理過程簡化為如下的過程:
1.請求每個通道的 會話\消息 數(shù)據(jù),并將多次結(jié)果回調(diào)合并為一次結(jié)果回調(diào),處理多次數(shù)據(jù)回調(diào),請求會話\消息對應(yīng)的Profile、群、群成員、關(guān)系數(shù)據(jù)、業(yè)務(wù)數(shù)據(jù);
2.建立會話\消息 和Profile、群、群成員、關(guān)系數(shù)據(jù)、業(yè)務(wù)數(shù)據(jù)之間的 關(guān)聯(lián)關(guān)系 ,生成聚合數(shù)據(jù),并處理聚合數(shù)據(jù)之間的依賴、優(yōu)先級和緩存一致性;
3.將數(shù)據(jù)轉(zhuǎn)換為數(shù)組\樹形結(jié)構(gòu),支持請求來的數(shù)據(jù)與數(shù)據(jù)結(jié)構(gòu)中已存的數(shù)據(jù)進行替換、更新合并計算,支持樹結(jié)構(gòu)和虛擬節(jié)點的動態(tài)計算,支持UI局部更新;
4.響應(yīng)各種數(shù)據(jù)的增刪改等變更事件,根據(jù)事件處理計算變更和結(jié)果,保證數(shù)據(jù)的一致性;
5.在進入和退出中間加載時,處理各種數(shù)據(jù)緩存、關(guān)聯(lián)關(guān)系、加載信息的正確性;
6.支持特殊邏輯,如實時新數(shù)據(jù)不按照時間排序,而是直接添加在頭部或尾部;
7.中間每個邏輯的異常處理、超時機制、線程同步、上屏時間優(yōu)化、日志、監(jiān)控等邏輯。
我們可以將這些邏輯分為兩類:
作為計算的邏輯
對應(yīng)于上面的過程2、3、4、5、6。
如果我們將這塊邏輯看做黑盒,關(guān)心它的輸入輸出和功能,可以得出這塊邏輯的核心工作是將各種各樣的輸入數(shù)據(jù)轉(zhuǎn)換為特定的輸出數(shù)據(jù),這完美的對應(yīng)著計算的概念的結(jié)論,即:
基于計算的概念,可以將這段計算過程形式化地抽象為一個函數(shù) f,從而實現(xiàn)對狀態(tài)計算的抽象,上圖很直觀的體現(xiàn)了入?yún)檩斎牒彤斍盃顟B(tài),輸出為新的狀態(tài)和結(jié)果:
f :: (Input, State1) -> (State2, Result)
來分析一下函數(shù)f的入出參和形式:
第一,這里的Input可以能是拉取回來的數(shù)據(jù),也可以是增刪改等數(shù)據(jù)變更,或者是消息已讀等明確的事件。這里我們可以通過定義插入、更新、刪除三個來統(tǒng)一所有的事件,因為所有的事件邏輯上都必定可以唯一的映射到這三個事件上(盡管實際上,由于部分服務(wù)不具備計算變更細節(jié)的能力,我們還支持了RemoveAll和Reload兩個事件)。
第二,結(jié)合高階函數(shù),Input實際上已經(jīng)決定了這個函數(shù)的形式,即對于一個數(shù)據(jù)插入事件,其對應(yīng)的f必然為 \state -> insert someData into state 的形式,即 Input 已經(jīng)包含在 f 的實現(xiàn)中了,因此可以將函數(shù) f 進一步簡化為:
f :: (State1) -> (State2, Result)
其中 f 的形式由輸出的事件決定,這樣就得到了一個非常簡化的函數(shù)抽象。
第三,上面的分析還能得出一個推論,即事件和函數(shù)是等價的(可以互相轉(zhuǎn)換),這使得我們可以通過處理事件來實現(xiàn)對函數(shù)的處理,從而可以通過數(shù)據(jù)的處理來優(yōu)化計算的性能,可以看到,數(shù)據(jù)和過程邊界的打破賦予我們更強的能力。
第四,對于 State 參數(shù),需要包含聚合后的數(shù)據(jù),因此需要處理數(shù)據(jù)的關(guān)聯(lián),一般的,我們可以將數(shù)據(jù)的關(guān)聯(lián)場景抽象為 一個主數(shù)據(jù)對應(yīng)多個附屬數(shù)據(jù)的形式,通過定義一個 pair 函數(shù)來進行關(guān)聯(lián)關(guān)系的判斷:
pair :: (mainData, subData) -> Bool
這樣就可以通過注入 pair 函數(shù)來實現(xiàn)主數(shù)據(jù)和附屬數(shù)據(jù)的關(guān)聯(lián),然后將有關(guān)聯(lián)關(guān)系的數(shù)據(jù)進行聚合。
第五,State還涉及數(shù)據(jù)\樹形結(jié)構(gòu)計算,這里在不同的場景是不一樣的,可以抽象為一個 DataStructure ,定義增刪改查接口,然后在不同的場景使用不同的 DataStructure。
作為結(jié)構(gòu)化數(shù)據(jù)獲取的邏輯
對應(yīng)于上面的過程1、6。
這部分邏輯的作用是會話消息Profile等數(shù)據(jù)的獲取和變更事件監(jiān)聽,由于6大服務(wù)的接口各不相同,之前的實現(xiàn)是一一對接。通過抽象之后,我們可以通過定義具備拉取接口和變更接口的Inject來實現(xiàn)這部分邏輯的抽象,這屬于標準操作,不再贅述。
這部分數(shù)據(jù)獲取的第二個特點是請求的平行分發(fā)和垂直組合,舉例來說,有多個通道決定了數(shù)據(jù)請求時需要平行的請求每個通道,每個通道的請求則根據(jù)不同的請求策略和每一步的回調(diào)數(shù)據(jù)決定下一次請求(這里與標準的 Future/Promise 的區(qū)別在于,F(xiàn)uture/Promise前后步驟的任務(wù)是不同的,后面的邏輯需要前面的數(shù)據(jù),這里前后步驟的邏輯相同,可能上一步請求本地,下一次請求遠端,因此可以比 Future/Promise 更簡化)。
如果不進行抽象,這里是一個至少三維的邏輯,即對于多個通道的多個步驟進行主數(shù)據(jù)的獲取,然后對獲取的主數(shù)據(jù)再獲取附屬數(shù)據(jù),邏輯會寫的非常復(fù)雜。這里的關(guān)鍵在于每個通道的請求,每個步驟的請求都是非常相似的,主要是多次請求的結(jié)構(gòu)不同,并且數(shù)據(jù)請求的結(jié)構(gòu)由參數(shù)和數(shù)據(jù)決定,因此可以把它稱為結(jié)構(gòu)化數(shù)據(jù)獲取,即這里可以通過對請求結(jié)構(gòu)的抽象進行簡化。
可以定義出結(jié)構(gòu)化數(shù)據(jù)獲取任務(wù)平行和垂直組合的核心函數(shù):
dispatch :: [param] -> [task]
compose :: strategy -> task
其中 dispatch 函數(shù)對應(yīng)于Rx中的 flatMap,不過由于手淘iOS沒有集成 RxSwift 和 OpenCombine,官方的 Combine 框架要iOS13之上才能使用,因此只能自己實現(xiàn)一個輕量的。
這樣通過 dispatch 和 compose 將任務(wù)進行結(jié)構(gòu)化組合實現(xiàn)任務(wù)獲取的抽象和簡化。
技術(shù)方案
核心技術(shù)方案
核心模塊:
1.MergeDispatcher : 實現(xiàn)數(shù)據(jù)獲取的結(jié)構(gòu)化,并將數(shù)據(jù)和變更統(tǒng)一為變更,處理所有的異常
2.Calculator : 實現(xiàn)主數(shù)據(jù)和附屬數(shù)據(jù)的關(guān)聯(lián)和聚合,計算的多線程同步,變更上報
3.DataStructure : 進行主數(shù)據(jù)的結(jié)構(gòu)計算
此外,Inject為計算提供請求接口和變更事件,為所有數(shù)據(jù)的注入點,上層通過 ModelService 獲取計算后有聚合數(shù)據(jù)構(gòu)成的數(shù)據(jù)結(jié)構(gòu),以及變更事件。
調(diào)用關(guān)系與數(shù)據(jù)流向
ModelService會使用初始化DataStructure、CalCulator、主數(shù)據(jù)、附屬數(shù)據(jù)的Inject,并用來初始化MergeDispatcher
1.當UI需要數(shù)據(jù)時,調(diào)用ModelService的load接口;
2.ModelService直接調(diào)用MergeDispatcher的load接口;
3.MergeDispatcher 平行調(diào)用主數(shù)據(jù)Inject的load接口,在每次回調(diào)主數(shù)據(jù)時,調(diào)用附屬數(shù)據(jù)Inject的load接口請求附屬數(shù)據(jù),根據(jù)場景執(zhí)行對應(yīng)的超時邏輯,將主數(shù)據(jù)和附屬數(shù)據(jù)給到Calculator進行計算,超時后的數(shù)據(jù)也繼續(xù)給到Calculator進行計算;
4.Calculator 執(zhí)行計算的多線程同步,更新主數(shù)據(jù)、附屬數(shù)據(jù)、關(guān)聯(lián)關(guān)系的緩存,生成聚合數(shù)據(jù),并將主數(shù)據(jù)給到DataStructure計算結(jié)構(gòu),然后將返回的全量和變更進行上報;
5.數(shù)據(jù)結(jié)構(gòu)接收數(shù)據(jù)后,對當前狀態(tài)(數(shù)據(jù)結(jié)構(gòu))執(zhí)行增刪改操作,并返回對應(yīng)的新狀態(tài)和變更數(shù)組。
技術(shù)效果
最終,我們實現(xiàn)了計算和數(shù)據(jù)獲取的分離,計算過程全部在Calculator,數(shù)據(jù)獲取主要在MergeDispatcher,兩部分獨立實現(xiàn),不再耦合,將邏輯層次從原來的模型數(shù)量 * 接口數(shù)量 * 數(shù)據(jù)結(jié)構(gòu) 降為 事件數(shù)量 * 數(shù)據(jù)結(jié)構(gòu),處理模型非常清晰,且適用于任意模型。
針對計算過程,邏輯上抽象出一個高階計算函數(shù) f :: (State1) -> (State2, Result),這個函數(shù)形式上非常簡單,卻緊緊抓住這種復(fù)雜狀態(tài)計算的本質(zhì),讓我們得以統(tǒng)一計算過程,整個計算過程的正確性有完備的理論基礎(chǔ),后續(xù)新增模型不會增加計算邏輯。
針對數(shù)據(jù)獲取,我們將數(shù)據(jù)類型化為主數(shù)據(jù)和附屬數(shù)據(jù),并針對由請求的結(jié)構(gòu)進行抽象,實現(xiàn)了所有的數(shù)據(jù)獲取的統(tǒng)一和簡化。