讀寫分離水太深,你把握不住,讓叔來——命令查詢權(quán)責(zé)分離模式
多年以前,那時(shí)我正年輕,做技術(shù)如魚得水,甚至一度希望自己能當(dāng)一輩子的一線程序員。
但是我又有兩個(gè)小愿望想要達(dá)成:一個(gè)是想多掙點(diǎn)錢;另一個(gè)就是對項(xiàng)目的技術(shù)棧和架構(gòu)選型能多有點(diǎn)主動(dòng)權(quán)。
多掙點(diǎn)錢是因?yàn)楫?dāng)時(shí)我剛結(jié)婚不久,有自己的家庭規(guī)劃,所以掙錢的欲望也蠻強(qiáng)。
而想有多點(diǎn)技術(shù)主動(dòng)權(quán)的原因則是當(dāng)時(shí)領(lǐng)導(dǎo)很賞識我,有些東西逐漸的放權(quán)讓我做,我嘗到了甜頭,所以,也有了自己的一些小野心。
而正巧就在那時(shí)候,領(lǐng)導(dǎo)給我了一個(gè)現(xiàn)在看來職業(yè)生涯中還挺重要的機(jī)會(huì)。
當(dāng)時(shí),廣告聯(lián)盟正是發(fā)展的如火如荼的時(shí)候,公司也想?yún)⑴c進(jìn)去分杯羹,于是決定從零開始搞一套廣告平臺(tái)。
而我正好也有些類似的開發(fā)經(jīng)驗(yàn),且做事還算靠譜,于是,領(lǐng)導(dǎo)便想著讓我去當(dāng)這套系統(tǒng)的技術(shù)負(fù)責(zé)人。
如果我能把系統(tǒng)做好,對我來說絕對是個(gè)證明自己的機(jī)會(huì),對以后達(dá)成我的兩個(gè)小愿望有好處。對我誘惑很大。
只是,老天給你開了一扇門,就總要給你關(guān)一扇窗。這個(gè)機(jī)會(huì)不僅僅是我領(lǐng)導(dǎo)看上了,當(dāng)時(shí),還有另外一個(gè)部門的老大也瞄上了。
不得已,上了高層會(huì)議討論。討論來討論去的結(jié)果就是學(xué)習(xí)當(dāng)時(shí)別的公司的做法,內(nèi)部競爭。
兩個(gè)部門做各做一套平臺(tái),然后各放到線上運(yùn)營一陣子,誰做得好誰就能得到公司全力投入的機(jī)會(huì)。
好吧,機(jī)會(huì)變成了冒險(xiǎn)。只是到此時(shí),我也并不能退縮。一旦我退縮會(huì)連累賞識我的領(lǐng)導(dǎo),而且將來在公司的發(fā)展也會(huì)嚴(yán)重受阻,只能沖了。
為了贏得這場競爭,我和這套系統(tǒng)的產(chǎn)品負(fù)責(zé)人也溝通了許久。最后定下來了兩個(gè)必須實(shí)現(xiàn)的目標(biāo):
1. 這套系統(tǒng)功能一定要盡量多,尤其是提供給相關(guān)業(yè)務(wù)人員的功能要多。
之所以要這樣,是因?yàn)楝F(xiàn)在是內(nèi)部競爭。而對于內(nèi)部競爭,使用我們這套系統(tǒng)的業(yè)務(wù)人員話語權(quán)其實(shí)非常大,他們的滿意度很可能是最終評估的勝負(fù)手。
同時(shí),我們也計(jì)劃為投放在我們這套系統(tǒng)的廣告主們多準(zhǔn)備一些體驗(yàn)度非常好的數(shù)據(jù)追蹤和分析功能,這樣能最大的增加我們產(chǎn)品的吸引力。
2. 這套系統(tǒng)的穩(wěn)定性和可靠性要求非常高,有時(shí)候哪怕為此做一些過度設(shè)計(jì)和實(shí)現(xiàn)也是值得的。
這里要解釋下穩(wěn)定性和可靠性在我們當(dāng)時(shí)那個(gè)場景里的含義。穩(wěn)定性就是要保證性能是穩(wěn)定的,也就是說我們的系統(tǒng)響應(yīng)時(shí)間應(yīng)該盡全力保證在一個(gè)很短的時(shí)間內(nèi)響應(yīng)。
而可靠性則是我們的系統(tǒng)應(yīng)該盡全力保證不出錯(cuò),因?yàn)槌鲥e(cuò)很可能就會(huì)造成用戶流失,導(dǎo)致我們的產(chǎn)品失敗。
定完目標(biāo)以及產(chǎn)品給完需求后,我就和團(tuán)隊(duì)進(jìn)入了異常艱苦的開發(fā)工作。那時(shí)候,我真的是付出了我全身心的心血。
其實(shí),我本來是個(gè)享受生活勝過埋頭苦干的人。雖然此前工作也很忙碌,但是空閑日子也是過得很愜意的。聽聽歌,看看電影,有時(shí)和老婆找家餐廳享用美食,時(shí)不時(shí)的也會(huì)踢一場酣暢淋漓的足球。
可是,自從開始投入了這套廣告系統(tǒng)的開發(fā)以后,悠閑的日子就一去不復(fù)返了。
我記得那時(shí)候我下班是踉踉蹌蹌的走,上班又是踉踉蹌蹌的來。當(dāng)時(shí)最大的心愿就是有張床,躺下去永遠(yuǎn)別有人叫醒我。
可是即使這樣辛苦,我依然遇到了數(shù)不清的難題,這些橫亙在開發(fā)路上的硬骨頭,導(dǎo)致我的開發(fā)目標(biāo)一再被調(diào)整。
其中最麻煩的,就是高并發(fā)的性能問題。
當(dāng)時(shí)我的經(jīng)驗(yàn)尚淺,Java 說實(shí)話周邊的生態(tài)也并不完善。能用來承載訪問的也就是緩存和數(shù)據(jù)庫。同時(shí),由于版權(quán)等問題,我還只能選擇 MySQL 數(shù)據(jù)庫。
為了解決這些性能問題,我還特意把官方的 MySQL 手冊打印了出來,天天鉆研。
開始的時(shí)候,為了抗住預(yù)想中的超高并發(fā)量,我采用的是當(dāng)時(shí)很流行的讀寫分離模式。

但是,實(shí)際測試下來,總是有各種不滿意的地方。其中最麻煩的就是各種復(fù)雜查詢的性能。
我說過為了獲得內(nèi)部競爭的勝利,這套系統(tǒng)我們盡可能想去往高并發(fā)、多功能這兩個(gè)目標(biāo)上靠。所以,為了這兩個(gè)目標(biāo),這套系統(tǒng)其實(shí)多了很多方便業(yè)務(wù)人員使用的功能,并且這個(gè)功能設(shè)想的目標(biāo)是:
在高并發(fā)下,也依然保持穩(wěn)定和流暢。
其中,最典型的一個(gè)業(yè)務(wù)就是可以實(shí)時(shí)更新的廣告投放排行功能。
這個(gè)廣告投放排行需求是這樣的:
首先,我們的用戶要能在管理后臺(tái)看到他們自己的投放廣告排行,排名是根據(jù)消費(fèi)的金額和點(diǎn)擊次數(shù)等指標(biāo)來排次序。 其次,在我們的后臺(tái),也給業(yè)務(wù)人員也搞了個(gè)這么個(gè)排名,不同的是它是個(gè)全局的,是我們所有客戶投放的廣告的一個(gè)總排行。 然后,這個(gè)排名要能實(shí)時(shí)的根據(jù)消費(fèi)金額和點(diǎn)擊次數(shù)的變化而變化。當(dāng)然,這個(gè)實(shí)時(shí)可以搞成準(zhǔn)實(shí)時(shí),只要?jiǎng)e延遲太過也可以。
本身呢,做排行榜由于用的指標(biāo)比較多,就需要寫很復(fù)雜的 SQL 去數(shù)據(jù)庫中查詢。再加上個(gè)需要實(shí)時(shí)變化,那就得不停的去數(shù)據(jù)庫中查詢。
而對于這種情況,我無論如何優(yōu)化總是得不到滿意的結(jié)果。如果我緩存這個(gè)排行呢,由于這個(gè)排行需要各種統(tǒng)計(jì)加排序,所以從數(shù)據(jù)庫中查詢出來后,還需要各種模型轉(zhuǎn)換,如果并發(fā)量上來,查詢再轉(zhuǎn)換,性能真的掉的飛快。

那時(shí)候,我的壓力非常大,腦子一直在想著性能問題,手上的 MySQL 手冊翻得都快爛的掉了頁。就連回到家睡覺時(shí),眼睛閉上腦海里總是想著如何解決這些問題。
最終上線的時(shí)間不斷地逼近,手上的項(xiàng)目卻死死卡在這些性能難題上難以進(jìn)展,競爭對手卻時(shí)不時(shí)聽到內(nèi)部競爭對手順利進(jìn)行到某某程度的消息。
這一切的一切我快扛不住了,內(nèi)心勸自己放棄的聲音也越來越大。
我曾經(jīng)一度認(rèn)為自己是一個(gè)韌性非常強(qiáng)的人,但是現(xiàn)在看來,其實(shí)也就是個(gè)再普通不過的打工仔而已。
我要逃避了,我想去和產(chǎn)品商量就這樣上線吧,我不想管了,是死是活看老天爺吧,賭對方也遇到我這種問題,甚至還不如我。
只是就在我準(zhǔn)備拉上產(chǎn)品最終確定就這樣上線的時(shí)候,我內(nèi)心強(qiáng)烈的不甘阻止了我。我想在我放棄之前,無論如何要知道競爭對手怎么樣了,對方有什么方案和思路可供我參考的。
我找遍了我所有公司的熟人,去不停的打探競爭對手的消息。但是,結(jié)果并不好,因?yàn)閷Ψ奖任易龅母^,他們進(jìn)行了封閉式的開發(fā),而且警惕性非常高。
最終,我只得到了一個(gè)關(guān)鍵詞:CQRS。對方用 CQRS 來解決性能問題?。?!
我年少讀書,那時(shí)還沒有手機(jī),總是能一心一意的做好讀書這件事,讀書效率極高。但是如今有了手機(jī),現(xiàn)在我再讀書,總是時(shí)不時(shí)會(huì)分心去看看手機(jī)里的信息,有時(shí)候?yàn)榱撕煤冒褧x進(jìn)去,還不得不把手機(jī)特意丟在遠(yuǎn)處,防止分心。
而 CQRS 就是這種思路。這個(gè)模式與其說是一種架構(gòu)模式還不如說是一種思想。
CQRS 認(rèn)為一套系統(tǒng)里的操作,總共就分為讀和寫兩大類。如果一套系統(tǒng)不專門把讀和寫專門分開優(yōu)化,那么系統(tǒng)就像我讀書帶著手機(jī)那樣,會(huì)一心兩用,從而因?yàn)楸舜擞绊?,?dǎo)致各自的性能無法達(dá)到最優(yōu)。
所以,讀寫應(yīng)該專門的分開,并分別優(yōu)化。

在 CQRS 里,寫這種行為被稱為命令,而讀行為被稱為查詢。因?yàn)橄胱屗麄兎珠_,所以 CQRS 模式中文翻譯過來就被稱為命令查詢權(quán)責(zé)分離模式。
我知道這套思路之后,本來并不在意,因?yàn)檎б豢?,這套東西其實(shí)和我采用的數(shù)據(jù)庫的讀寫分離是一樣的,就是把讀寫給分開。
但是,我的技術(shù)直覺告訴我,這些并沒有那么簡單。
在計(jì)算機(jī)的世界里,一個(gè)名詞不會(huì)無緣無故出現(xiàn),也不會(huì)無緣無故的開始流行。如果真的和數(shù)據(jù)庫的讀寫分離一樣,那直接叫數(shù)據(jù)庫讀寫分離就好了。一定有什么不一樣了。
我沒再滿足于中文的搜索結(jié)果了,我直接去了 Martin Flower 的網(wǎng)站看原始版本去了。然后,我發(fā)現(xiàn)了這樣一幅架構(gòu)圖。

再結(jié)合他的原文我一下子明白了,是模型,模型的不同!
原來的數(shù)據(jù)庫讀寫分離確實(shí)把讀寫的這兩個(gè)行為分開了,但是它依然有一個(gè)重要的事情沒有做,那就是職責(zé)的分開。

什么叫職責(zé)的分開呢?就是讀寫雙方不要搞同一套模型。而數(shù)據(jù)庫讀寫分離的問題就在這里,它使用了同一個(gè)模型。
使用同一個(gè)模型在這里造成的問題是,這個(gè)模型由于既要考慮讀取數(shù)據(jù)不能太困難,也要考慮寫入數(shù)據(jù)不能太困難。
而這個(gè)恰恰就是違背了 CQRS 中的核心思想:讀寫徹底自由。
如果我們使用 CQRS 思想的話,假設(shè)寫入不需要關(guān)心讀取的問題,讀取數(shù)據(jù)也不用關(guān)心寫入的問題,那么雙方是不是可以徹底放飛自我了?

比如,寫入數(shù)據(jù)由于不需要考慮讀取,那我大可以使用 Json 格式,使用 XML 格式之類的非標(biāo)準(zhǔn)格式,甚至直接寫個(gè)日志都可以。而讀取數(shù)據(jù)則根本不需要考慮寫入的問題,我甚至可以弄成一個(gè)容易搜索的索引格式來。
而 CQRS 在我看來,正是解決卡死我的性能問題的靈丹妙藥。
以廣告排行這個(gè)問題為例,廣告排行麻煩就麻煩在,每次加載排行榜需要有很復(fù)雜的查詢,去數(shù)據(jù)庫中讀取數(shù)據(jù)。
如果能徹底地把排行榜的讀取和排行榜依賴的那些點(diǎn)擊、消費(fèi)指標(biāo)的更新分開,那我苦惱的排行榜性能問題就能迎刃而解。
我費(fèi)勁心思后,仿照 CQRS 的原版思想搞了一個(gè)這樣的設(shè)計(jì)思路:

這里,數(shù)據(jù)統(tǒng)計(jì)就是廣告排名需要的點(diǎn)擊、消費(fèi)等數(shù)據(jù)。這些數(shù)據(jù)會(huì)被放到一個(gè)單獨(dú)的數(shù)據(jù)庫中,這個(gè)數(shù)據(jù)庫只用來寫入,不考慮讀。
然后,展示廣告排行的功能本身又會(huì)單獨(dú)從緩存中把廣告排行的模型直接讀取出來展示出去,而不用專門再做什么轉(zhuǎn)換了。也不存在什么復(fù)雜查詢的問題。
但是,我們的需求是要準(zhǔn)實(shí)時(shí)的讓廣告排行根據(jù)點(diǎn)擊、消費(fèi)等數(shù)據(jù)自動(dòng)更新,那么如果寫入數(shù)據(jù)和讀取數(shù)據(jù)模型分開了,該怎么辦呢?
多年以前,當(dāng)我第一次在網(wǎng)上買東西的時(shí)候,心里有個(gè)疑問:我下了個(gè)訂單,賣我東西的商家是怎么知道的?莫非要一直盯著?
這個(gè)問題到我親自開發(fā)電商系統(tǒng)的時(shí)候才知道,當(dāng)我們下單的時(shí)候,需要發(fā)一個(gè)通知給對應(yīng)的商家,告訴商家哪個(gè)客戶購買了哪個(gè)商品。
所以,廣告排行自動(dòng)更新的解決方案有了,和電商下單通知商家的道理一樣。當(dāng)有數(shù)據(jù)寫入的時(shí)候,我們把寫入的數(shù)據(jù)復(fù)制一份通知給讀取數(shù)據(jù)的模型就可以了。

好,現(xiàn)在整套邏輯完整了。
但是,我并沒有急于馬上把 CQRS 這套模式去應(yīng)用到實(shí)際的項(xiàng)目當(dāng)中。因?yàn)?,我發(fā)現(xiàn)我竟然不知道 CQRS 這套模式的缺點(diǎn)是什么。
要知道,世界上還不存在完美的解決方案,全都是既有優(yōu)點(diǎn)又有缺點(diǎn)的。而 CQRS 我竟然覺得很完美的解決了我的問題,這說明我對這套模式的認(rèn)知還存在問題。
當(dāng)時(shí),離約定的上線時(shí)間已經(jīng)越來越近了,差不多還剩一周時(shí)間。我真的很想閉眼把方案實(shí)施下去。
但是,不行,我這個(gè)人做事向來喜歡把事情想得通透,把事物認(rèn)知的十分清楚后再去做。
我決定冒險(xiǎn)花兩天去實(shí)現(xiàn)兩個(gè)功能點(diǎn),然后親自體驗(yàn)一下引入 CQRS 的得與失。
兩天后,我終于發(fā)現(xiàn)了問題:引入 CQRS 的模式后,最大的問題在于引入了過度的復(fù)雜性。
由于需要讀和寫分開,那么我們開發(fā)的工作量無形中被加大了一倍。又引入 CQRS,這變得更復(fù)雜了。
因?yàn)槲覀儼l(fā)現(xiàn),不同的功能,只有使用不同的讀取或者寫入模型才能充分用上 CQRS 的優(yōu)點(diǎn)。
比如,廣告排行可能使用了緩存中間件去存取現(xiàn)成的排名。根據(jù)關(guān)鍵字搜索各種合適的廣告,可能就得考慮開源的搜索引擎中間件。每引入一種都會(huì)增加開發(fā)成本、服務(wù)器成本,以及更多的復(fù)雜度。
最終,我們的廣告系統(tǒng)按時(shí)上線了。
只不過,并沒有廣泛的采用 CQRS 模式,我只是把最重要的功能點(diǎn)用上了 CQRS,其余的有關(guān)性能的問題,我決定暫時(shí)放下。
之所以這樣,是因?yàn)槲矣X得大部分的問題,其實(shí)是我們過度設(shè)計(jì)引發(fā)的。即使因此我失敗了,我也認(rèn)了。
我并不想為自己親手打造的系統(tǒng)埋下巨大的隱患,更不想給團(tuán)隊(duì)帶來無謂的工作量,我不想卷成這樣。
上線后,我是如此忐忑,尤其是在上線運(yùn)營的頭兩個(gè)月。
我不知道自己的妥協(xié)是否會(huì)誘發(fā)巨大的問題,我也不知道自己的所作所為是不是真的是對的。
兩個(gè)系統(tǒng)的競爭在上線兩個(gè)月后就有結(jié)果了。
這么快的得到結(jié)果,恰恰就是因?yàn)槲业膶κ謴V泛的使用了 CQRS 模式。
他從一開始設(shè)計(jì)的時(shí)候,就想著一鳴驚人,他的系統(tǒng)里引入了七八種中間件。把大量的功能拆分成了讀寫兩部分,而這引發(fā)了巨大的災(zāi)難,過度的復(fù)雜性,導(dǎo)致整個(gè)系統(tǒng)難以控制。
其中最頭痛的就是,由于引入 CQRS,他們必須通過消息的傳遞去溝通讀寫兩套組件。
但是,當(dāng)讀取組件收到消息后,卻發(fā)現(xiàn)寫入失敗了。導(dǎo)致用戶看到了對應(yīng)的數(shù)據(jù)后,過一段時(shí)間,卻發(fā)現(xiàn)數(shù)據(jù)和以前看到的對不上了。

比如,點(diǎn)擊次數(shù),開始看到的是 1000 次,結(jié)果兩個(gè)小時(shí)后,發(fā)現(xiàn)變成了 999 次了。
這類問題每天都在出現(xiàn),而他們因?yàn)橄到y(tǒng)太復(fù)雜了,查問題、定位問題、解決問題的時(shí)間被大大拉長。最后,客戶們紛紛不干了,公司只好把客戶轉(zhuǎn)到了我這邊的平臺(tái)上。
競爭結(jié)束了,我勝利了,可是我真的無法高興地起來。因?yàn)榻裉焖驗(yàn)殄e(cuò)誤的引入新技術(shù)失敗了,那明天我又何嘗不會(huì)因?yàn)檎`用新技術(shù)新思想而失敗呢?今日的他又何嘗不是明日的我?
愿天下程序員凡事深思熟悉,謹(jǐn)言慎行!
最后,希望大家能來個(gè)三連,碼字不易,也算是對我原創(chuàng)最大的支持!
你好,我是四猿外。
一家上市公司的技術(shù)總監(jiān),管理的技術(shù)團(tuán)隊(duì)一百余人。
我從一名非計(jì)算機(jī)專業(yè)的畢業(yè)生,轉(zhuǎn)行到程序員,一路打拼,一路成長。
我會(huì)通過公眾號,
把自己的成長故事寫成文章,
把枯燥的技術(shù)文章寫成故事。
我建了一個(gè)讀者交流群,里面大部分是程序員,一起聊技術(shù)、工作、八卦。歡迎加我微信,拉你入群。
推薦閱讀
硬著頭皮寫,硬著頭皮搞:一個(gè)服務(wù)一個(gè)數(shù)據(jù)庫模式(下)
我是如何把微服務(wù)的這個(gè)模式落地的:一個(gè)服務(wù)一個(gè)數(shù)據(jù)庫模式(中)
“一學(xué)就會(huì)”微服務(wù)的架構(gòu)模式:一個(gè)服務(wù)一個(gè)數(shù)據(jù)庫模式(上)
