危險!水很深,讓叔來 —— 談談命令查詢權責分離模式(CQRS)

四猿外發表於2021-06-03

多年以前,那時我正年輕,做技術如魚得水,甚至一度希望自己能當一輩子的一執行緒序員。

但是我又有兩個小願望想要達成:一個是想多掙點錢;另一個就是對專案的技術棧和架構選型能多有點主動權。

多掙點錢是因為當時我剛結婚不久,有自己的家庭規劃,所以掙錢的慾望也蠻強。

而想有多點技術主動權的原因則是當時領導很賞識我,有些東西逐漸的放權讓我做,我嚐到了甜頭,所以,也有了自己的一些小野心。

而正巧就在那時候,領導給我了一個現在看來職業生涯中還挺重要的機會。

當時,廣告聯盟正是發展的如火如荼的時候,公司也想參與進去分杯羹,於是決定從零開始搞一套廣告平臺。

而我正好也有些類似的開發經驗,且做事還算靠譜,於是,領導便想著讓我去當這套系統的技術負責人。

如果我能把系統做好,對我來說絕對是個證明自己的機會,對以後達成我的兩個小願望有好處。對我誘惑很大。

只是,老天給你開了一扇門,就總要給你關一扇窗。這個機會不僅僅是我領導看上了,當時,還有另外一個部門的老大也瞄上了。

不得已,上了高層會議討論。討論來討論去的結果就是學習當時別的公司的做法,內部競爭。

兩個部門做各做一套平臺,然後各放到線上運營一陣子,誰做得好誰就能得到公司全力投入的機會。

好吧,機會變成了冒險。只是到此時,我也並不能退縮。一旦我退縮會連累賞識我的領導,而且將來在公司的發展也會嚴重受阻,只能衝了。

為了贏得這場競爭,我和這套系統的產品負責人也溝通了許久。最後定下來了兩個必須實現的目標:

1. 這套系統功能一定要儘量多,尤其是提供給相關業務人員的功能要多。

之所以要這樣,是因為現在是內部競爭。而對於內部競爭,使用我們這套系統的業務人員話語權其實非常大,他們的滿意度很可能是最終評估的勝負手。

同時,我們也計劃為投放在我們這套系統的廣告主們多準備一些體驗度非常好的資料追蹤和分析功能,這樣能最大的增加我們產品的吸引力。

2. 這套系統的穩定性和可靠性要求非常高,有時候哪怕為此做一些過度設計和實現也是值得的。

這裡要解釋下穩定性和可靠性在我們當時那個場景裡的含義。穩定性就是要保證效能是穩定的,也就是說我們的系統響應時間應該盡全力保證在一個很短的時間內響應。

而可靠性則是我們的系統應該盡全力保證不出錯,因為出錯很可能就會造成使用者流失,導致我們的產品失敗。

定完目標以及產品給完需求後,我就和團隊進入了異常艱苦的開發工作。那時候,我真的是付出了我全身心的心血。

其實,我本來是個享受生活勝過埋頭苦幹的人。雖然此前工作也很忙碌,但是空閒日子也是過得很愜意的。聽聽歌,看看電影,有時和老婆找家餐廳享用美食,時不時的也會踢一場酣暢淋漓的足球。

可是,自從開始投入了這套廣告系統的開發以後,悠閒的日子就一去不復返了。

我記得那時候我下班是踉踉蹌蹌的走,上班又是踉踉蹌蹌的來。當時最大的心願就是有張床,躺下去永遠別有人叫醒我。

可是即使這樣辛苦,我依然遇到了數不清楚的難題,這些橫亙在開發路上的硬骨頭,導致我的開發目標一再被調整。

其中最麻煩的,就是高併發的效能問題。

當時我的經驗尚淺,Java 說實話周邊的生態也並不完善。能用來承載訪問的也就是快取和資料庫。同時,由於版權等問題,我還只能選擇 MySQL 資料庫。

為了解決這些效能問題,我還特意把官方的 MySQL 手冊列印了出來,天天鑽研。

開始的時候,為了抗住預想中的超高併發量,我採用的是當時很流行的讀寫分離模式。

但是,實際測試下來,總是有各種不滿意的地方。其中最麻煩的就是各種複雜查詢的效能。

我說過為了獲得內部競爭的勝利,這套系統我們儘可能想去往高併發、多功能這兩個目標上靠。所以,為了這兩個目標,這套系統其實多了很多方便業務人員使用的功能,並且這個功能設想的目標是:

在高併發下,也依然保持穩定和流暢。

其中,最典型的一個業務就是可以實時更新的廣告投放排行功能。

這個廣告投放排行需求是這樣的:

  • 首先,我們的使用者要能在管理後臺看到他們自己的投放廣告排行,排名是根據消費的金額和點選次數等指標來排次序。
  • 其次,在我們的後臺,也給業務人員也搞了個這麼個排名,不同的是它是個全域性的,是我們所有客戶投放的廣告的一個總排行。
  • 然後,這個排名要能實時的根據消費金額和點選次數的變化而變化。當然,這個實時可以搞成準實時,只要別延遲太過也可以。

本身呢,做排行榜由於用的指標比較多,就需要寫很複雜的 SQL 去資料庫中查詢。再加上個需要實時變化,那就得不停的去資料庫中查詢。

而對於這種情況,我無論如何優化總是得不到滿意的結果。如果我快取這個排行呢,由於這個排行需要各種統計加排序,所以從資料庫中查詢出來後,還需要各種模型轉換,如果併發量上來,查詢再轉換,效能真的掉的飛快。

那時候,我的壓力非常大,腦子一直在想著效能問題,手上的 MySQL 手冊翻得都快爛的掉了頁。就連回到家睡覺時,眼睛閉上腦海裡總是想著如何解決這些問題。

最終上線的時間不斷地逼近,手上的專案卻死死卡在這些效能難題上難以進展,競爭對手卻時不時聽到內部競爭對手順利進行到某某程度的訊息。

這一切的一切我快扛不住了,內心勸自己放棄的聲音也越來越大。

我曾經一度認為自己是一個韌性非常強的人,但是現在看來,其實也就是個再普通不過的打工仔而已。

我要逃避了,我想去和產品商量就這樣上線吧,我不想管了,是死是活看老天爺吧,賭對方也遇到我這種問題,甚至還不如我。

只是就在我準備拉上產品最終確定就這樣上線的時候,我內心強烈的不甘阻止了我。我想在我放棄之前,無論如何要知道競爭對手怎麼樣了,對方有什麼方案和思路可供我參考的。

我找遍了我所有公司的熟人,去不停的打探競爭對手的訊息。但是,結果並不好,因為對方比我做的更絕,他們進行了封閉式的開發,而且警惕性非常高。

最終,我只得到了一個關鍵詞:CQRS。對方用 CQRS 來解決效能問題!!!

我年少讀書,那時還沒有手機,總是能一心一意的做好讀書這件事,讀書效率極高。但是如今有了手機,現在我再讀書,總是時不時會分心去看看手機裡的資訊,有時候為了好好把書讀進去,還不得不把手機特意丟在遠處,防止分心。

而 CQRS 就是這種思路。這個模式與其說是一種架構模式還不如說是一種思想。

CQRS 認為一套系統裡的操作,總共就分為讀和寫兩大類。如果一套系統不專門把讀和寫專門分開優化,那麼系統就像我讀書帶著手機那樣,會一心兩用,從而因為彼此影響,導致各自的效能無法達到最優。

所以,讀寫應該專門的分開,並分別優化。

在 CQRS 裡,寫這種行為被稱為命令,而讀行為被稱為查詢。因為想讓他們分開,所以 CQRS 模式中文翻譯過來就被稱為命令查詢權責分離模式

我知道這套思路之後,本來並不在意,因為乍一看,這套東西其實和我採用的資料庫的讀寫分離是一樣的,就是把讀寫給分開。

但是,我的技術直覺告訴我,這些並沒有那麼簡單。

在計算機的世界裡,一個名詞不會無緣無故出現,也不會無緣無故的開始流行。如果真的和資料庫的讀寫分離一樣,那直接叫資料庫讀寫分離就好了。一定有什麼不一樣了。

我沒再滿足於中文的搜尋結果了,我直接去了 Martin Flower 的網站看原始版本去了。然後,我發現了這樣一幅架構圖。

再結合他的原文我一下子明白了,是模型,模型的不同!

原來的資料庫讀寫分離確實把讀寫的這兩個行為分開了,但是它依然有一個重要的事情沒有做,那就是職責的分開。

什麼叫職責的分開呢?就是讀寫雙方不要搞同一套模型。而資料庫讀寫分離的問題就在這裡,它使用了同一個模型。

使用同一個模型在這裡造成的問題是,這個模型由於既要考慮讀取資料不能太困難,也要考慮寫入資料不能太困難。

而這個恰恰就是違背了 CQRS 中的核心思想:讀寫徹底自由

如果我們使用 CQRS 思想的話,假設寫入不需要關心讀取的問題,讀取資料也不用關心寫入的問題,那麼雙方是不是可以徹底放飛自我了?

比如,寫入資料由於不需要考慮讀取,那我大可以使用 Json 格式,使用 XML 格式之類的非標準格式,甚至直接寫個日誌都可以。而讀取資料則根本不需要考慮寫入的問題,我甚至可以弄成一個容易搜尋的索引格式來。

而 CQRS 在我看來,正是解決卡死我的效能問題的靈丹妙藥。

以廣告排行這個問題為例,廣告排行麻煩就麻煩在,每次載入排行榜需要有很複雜的查詢,去資料庫中讀取資料。

如果能徹底地把排行榜的讀取排行榜依賴的那些點選、消費指標的更新分開,那我苦惱的排行榜效能問題就能迎刃而解。

我費勁心思後,仿照 CQRS 的原版思想搞了一個這樣的設計思路:

這裡,資料統計就是廣告排名需要的點選、消費等資料。這些資料會被放到一個單獨的資料庫中,這個資料庫只用來寫入,不考慮讀。

然後,展示廣告排行的功能本身又會單獨從快取中把廣告排行的模型直接讀取出來展示出去,而不用專門再做什麼轉換了。也不存在什麼複雜查詢的問題。

但是,我們的需求是要準實時的讓廣告排行根據點選、消費等資料自動更新,那麼如果寫入資料和讀取資料模型分開了,該怎麼辦呢?

多年以前,當我第一次在網上買東西的時候,心裡有個疑問:我下了個訂單,賣我東西的商家是怎麼知道的?莫非要一直盯著?

這個問題到我親自開發電商系統的時候才知道,當我們下單的時候,需要發一個通知給對應的商家,告訴商家哪個客戶購買了哪個商品。

所以,廣告排行自動更新的解決方案有了,和電商下單通知商家的道理一樣。當有資料寫入的時候,我們把寫入的資料複製一份通知給讀取資料的模型就可以了。

好,現在整套邏輯完整了。

但是,我並沒有急於馬上把 CQRS 這套模式去應用到實際的專案當中。因為,我發現我竟然不知道 CQRS 這套模式的缺點是什麼。

要知道,世界上還不存在完美的解決方案,全都是既有優點又有缺點的。而 CQRS 我竟然覺得很完美的解決了我的問題,這說明我對這套模式的認知還存在問題。

當時,離約定的上線時間已經越來越近了,差不多還剩一週時間。我真的很想閉眼把方案實施下去。

但是,不行,我這個人做事向來喜歡把事情想得通透,把事物認知的十分清楚後再去做。

我決定冒險花兩天去實現兩個功能點,然後親自體驗一下引入 CQRS 的得與失。

當兩天後,我終於發現了問題:引入 CQRS 的模式後,最大的問題在於引入了過度的複雜性

由於需要讀和寫分開,那麼我們開發的工作量無形中被加大了一倍。又引入 CQRS,這變得更復雜了。

因為我們發現,不同的功能,只有使用不同的讀取或者寫入模型才能充分用上 CQRS 的優點。

比如,廣告排行可能使用了快取中介軟體去存取現成的排名。根據關鍵字搜尋各種合適的廣告,可能就得考慮開源的搜尋引擎中介軟體。每引入一種都會增加開發成本、伺服器成本,以及更多的複雜度。

最終,我們的廣告系統按時上線了。

只不過,並沒有廣泛的採用 CQRS 模式,我只是把最重要的功能點用上了 CQRS,其餘的有關效能的問題,我決定暫時放下。

之所以這樣,是因為我覺得大部分的問題,其實是我們過度設計引發的。即使因此我失敗了,我也認了。

我並不想為自己親手打造的系統埋下巨大的隱患,更不想給團隊帶來無謂的工作量,我不想捲成這樣。

上線後,我是如此忐忑,尤其是在上線運營的頭兩個月。

我不知道自己的妥協是否會誘發巨大的問題,我也不知道自己的所作所為是不是真的是對的。

兩個系統的競爭在上線兩個月後就有結果了。

這麼快的得到結果,恰恰就是因為我的對手廣泛的使用了 CQRS 模式。

他從一開始設計的時候,就想著一鳴驚人,他的系統裡引入了七八種中介軟體。把大量的功能拆分成了讀寫兩部分,而這引發了巨大的災難,過度的複雜性,導致整個系統難以控制。

其中最頭痛的就是,由於引入 CQRS,他們必須通過訊息的傳遞去溝通讀寫兩套元件。

但是,當讀取元件收到訊息後,卻發現寫入失敗了。導致使用者看到了對應的資料後,過一段時間,卻發現資料和以前看到的對不上了。

比如,點選次數,開始看到的是 1000 次,結果兩個小時後,發現變成了 999 次了。

這類問題每天都在出現,而他們因為系統太複雜了,查問題、定位問題、解決問題的時間被大大拉長。最後,客戶們紛紛不幹了,公司只好把客戶轉到了我這邊的平臺上。

競爭結束了,我勝利了,可是我真的無法高興地起來。因為今天他因為錯誤的引入新技術失敗了,那明天我又何嘗不會因為誤用新技術新思想而失敗呢?今日的他又何嘗不是明日的我?

願天下程式設計師凡事深思熟悉,謹言慎行!


你好,我是四猿外。

一家上市公司的技術總監,管理的技術團隊一百餘人。

我從一名非計算機專業的畢業生,轉行到程式設計師,一路打拼,一路成長。

我會把自己的成長故事寫成文章,把枯燥的技術文章寫成故事。

歡迎關注我的公眾號,關注之後還可以獲取演算法、高併發等乾貨學習資料

我建了一個讀者交流群,裡面大部分是程式設計師,一起聊技術、工作、八卦。歡迎加我微信,拉你入群。

相關文章