聊聊多版本業(yè)務(wù)模型設(shè)計(jì)
本文轉(zhuǎn)載自微信公眾號(hào)「編了個(gè)程」,作者Yasin x 。轉(zhuǎn)載本文請(qǐng)聯(lián)編了個(gè)程公眾號(hào)。
最近業(yè)務(wù)上用到比較多的多版本場(chǎng)景。這里總結(jié)一下多版本業(yè)務(wù)模型設(shè)計(jì)的思路。
多版本需求梳理
先梳理一下多版本的一般訴求:
- 同一個(gè)數(shù)據(jù)經(jīng)過(guò)多次編輯后,會(huì)產(chǎn)生多個(gè)版本,其中歷史版本不能刪除掉,因?yàn)榭赡苡猩舷掠卧谑褂茫?/li>
- 多版本通常用于配置中,最新一個(gè)版本的配置通??梢远啻涡薷?、測(cè)試,確定后再發(fā)布;
- 已經(jīng)發(fā)布的歷史版本不能隨便修改,因?yàn)橛袛?shù)據(jù)在使用;
- 在消費(fèi)側(cè),一般默認(rèn)是使用最新已發(fā)布的版本;
- 多版本可能會(huì)有發(fā)布審批、與上一個(gè)版本的diff等需求場(chǎng)景;
多版本狀態(tài)機(jī)設(shè)計(jì)
一個(gè)多版本的業(yè)務(wù)模型,通常會(huì)有以下的狀態(tài)機(jī)。其中“廢棄”不是必須的,回滾操作也不是必須的(回滾操作會(huì)給代碼和表設(shè)計(jì)帶來(lái)很大的復(fù)雜性),發(fā)布中間可能會(huì)有發(fā)布中、審批中等狀態(tài)。
草稿可以在原版本編輯,但已發(fā)布的數(shù)據(jù)再編輯,就會(huì)生成一個(gè)新版本的草稿。
有時(shí)候也會(huì)有下線操作,這個(gè)時(shí)候所有版本的狀態(tài)就會(huì)被改為“已下線”。
多版本表設(shè)計(jì)
對(duì)于多版本而言,你需要有一個(gè)唯一標(biāo)識(shí)這個(gè)業(yè)務(wù)數(shù)據(jù)的字段,可以叫id?或者code。
同時(shí),需要一個(gè)字段來(lái)標(biāo)識(shí)版本,這個(gè)版本建議是一個(gè)遞增的數(shù)字,叫version?。有些業(yè)務(wù)期望版本是業(yè)務(wù)輸入的,或者有一個(gè)版本說(shuō)明的概念,那可以新增一個(gè)字段叫version_desc。
我們可以把唯一標(biāo)識(shí)和版本拼接起來(lái),作為這個(gè)數(shù)據(jù)在這個(gè)版本的唯一鍵,可以叫code_version?。通常是拼接成一個(gè)字符串,中間用某種特定的分隔符來(lái)區(qū)分,比如#?。那code_version?可能就長(zhǎng)這樣:A12334#3?。這里就要求code?里面不能有分隔符#,不然代碼邏輯處理起來(lái)就比較麻煩。
這里說(shuō)一下這個(gè)拼接字段的必要性,因?yàn)樯舷掠瓮鶗?huì)存code + version。那上下游在列表查詢等場(chǎng)景來(lái)查詢數(shù)據(jù)的時(shí)候,如果沒(méi)有這個(gè)字段,只能循環(huán)一個(gè)個(gè)查,不能用where批量查詢。
另外一個(gè)必要的字段就是status來(lái)標(biāo)識(shí)當(dāng)前版本的狀態(tài)。
還有一個(gè)非必須的字段is_last_version?,用來(lái)標(biāo)識(shí)當(dāng)前這條數(shù)據(jù)是不是它的最新版本,無(wú)論是草稿態(tài)還是已發(fā)布還是已廢棄,它都會(huì)變成true。這里在待會(huì)兒下文的查詢要點(diǎn)中會(huì)解釋它的用處。如果不用這個(gè)字段也能查,但是需要group by order,整體查詢語(yǔ)句麻煩,效率低。在寫的時(shí)候多維護(hù)一下這個(gè)數(shù)據(jù),會(huì)讓查詢的時(shí)候方便很多。
其它的都是審計(jì)字段了。最終的建表語(yǔ)句可能長(zhǎng)這樣:
生產(chǎn)端和消費(fèi)端的查詢要點(diǎn)
生產(chǎn)端就是配置數(shù)據(jù)的地方,消費(fèi)端就是使用的地方。生產(chǎn)端和消費(fèi)端有一些區(qū)別,生產(chǎn)端往往要看最新的版本,包括草稿等狀態(tài),還要能修改。而消費(fèi)端一般只用最新已發(fā)布的版本。
生產(chǎn)端
生產(chǎn)端的查詢,可以按照is_last_version為true來(lái)過(guò)濾,這樣就只查每一個(gè)code的最新版本的數(shù)據(jù)。
同時(shí),每個(gè)code也應(yīng)該返回一個(gè)version?列表,是這個(gè)數(shù)據(jù)code_version的集合,以便用戶查看和跳轉(zhuǎn)歷史版本。
生產(chǎn)端的寫入,需要維護(hù)好狀態(tài)、版本、is_last_version等字段。在編輯的時(shí)候,要判斷當(dāng)前的狀態(tài)是草稿態(tài)還是已發(fā)布,如果是已發(fā)布,是要?jiǎng)?chuàng)建一條新的記錄(當(dāng)然這個(gè)在前端判斷也是可以的,但后端要做好校驗(yàn),防止頁(yè)面沒(méi)刷新等場(chǎng)景造成臟數(shù)據(jù))。
消費(fèi)端
消費(fèi)端的查詢,需要查詢最新已發(fā)布版本,一般是通過(guò)狀態(tài)來(lái)過(guò)濾,比如status = Online。
但這里根據(jù)狀態(tài)過(guò)濾有一個(gè)問(wèn)題:歷史已發(fā)布的版本怎么辦?如果一個(gè)code發(fā)布了3個(gè)版本,那豈不是會(huì)查出來(lái)3條數(shù)據(jù)?要解決這個(gè)辦法有兩種思路:
- 狀態(tài)機(jī)添加一個(gè)Online_history的狀態(tài),在寫入的時(shí)候維護(hù)這種狀態(tài);
- 表增加一個(gè)is_last_online_version,在寫入的時(shí)候維護(hù)這個(gè)字段;
我個(gè)人比較喜歡用第一種方案,少維護(hù)一個(gè)字段,僅僅多維護(hù)一個(gè)枚舉就行了。
其它注意事項(xiàng)
上下游
我們?cè)谏舷掠蔚慕涌诮换ブ校艘鶕?jù)code?查最新已發(fā)布版本這種消費(fèi)端場(chǎng)景外,通常用code_version來(lái)交互。這樣在DB中可以直接命中一條數(shù)據(jù),查詢起來(lái)也方便。
這個(gè)數(shù)據(jù)和其他數(shù)據(jù)的關(guān)系,也通常使用code_version來(lái)存,因?yàn)椴煌姹娟P(guān)聯(lián)的數(shù)據(jù)可能不同。
diff
diff往往是利用領(lǐng)域模型json化后來(lái)diff。這里的diff能力可以做成一個(gè)通用的服務(wù),傳入old json和new json,返回哪些是新增的,哪些是刪除的,哪些是變更的。內(nèi)部的邏輯一般是利用json_path和遞歸的方法來(lái)做。
diff的難點(diǎn)是做成配置化,配置哪些屬性參與diff,哪些屬性ignore diff。diff出來(lái)之后,可能枚舉等需要key轉(zhuǎn)換成label,外部有一個(gè)轉(zhuǎn)換函數(shù),或者前端去轉(zhuǎn)。
另一塊需要注意的就是數(shù)組的順序。有些字段雖然到j(luò)son是數(shù)組,但業(yè)務(wù)上本身是順序無(wú)關(guān)的,這種數(shù)據(jù)的比對(duì)會(huì)更麻煩一些。
回滾
回滾其實(shí)很麻煩,包括已發(fā)布的回滾到上一個(gè)版本、發(fā)布中的回滾到草稿態(tài)。主要是前者很麻煩,尤其是有上下游使用了這個(gè)版本的數(shù)據(jù),一般是不允許輕易回滾的。
如果有這類場(chǎng)景,多半是沒(méi)有上下游,比如服務(wù)發(fā)布、應(yīng)用發(fā)布等。這種回滾,當(dāng)前版本的數(shù)據(jù)一般也不會(huì)刪除,而是設(shè)置成一個(gè)特殊的狀態(tài)。下次編輯上一個(gè)版本的時(shí)候,生成的version也不是+1, 而是+2甚至是+n,還得查一遍庫(kù),比較麻煩。
所以如果不是有特殊的需求,可以不做已發(fā)布的回滾,它會(huì)帶來(lái)很多復(fù)雜性。