<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          拯救祭天的程序員——事件溯源模式

          共 5616字,需瀏覽 12分鐘

           ·

          2021-06-01 22:33

          一、事前

          你相信嗎?曾經(jīng)有一段日子,我?guī)缀鯖]接到過合格的產(chǎn)品需求。

          開局幾句話,技術(shù)全靠猜。

          總是以為簡單的需求

          曾經(jīng),我從產(chǎn)品那里接到過這么一個(gè)需求:

          對系統(tǒng)的用戶進(jìn)行分級,不同級別的用戶有不同的福利。

          依然如常,無圖無文檔,只是這么一句話。我知道,需求一句話,分析五日功嘛。為了項(xiàng)目能持續(xù)發(fā)展,我只好自己分析自己搞了。

          從業(yè)務(wù)上看,目前的用戶對象尚無等級一說,我們先為用戶對象加上個(gè)級別屬性。又因?yàn)椴煌挠脩舻燃?,可享受到不同的福利。比如:達(dá)到 3 級的用戶,可以享受購物 9.5 折優(yōu)惠,物流費(fèi)用全免,客服快速回復(fù)等。

          所以,我做出設(shè)計(jì)如下:

          首先,我把每個(gè)等級用戶該享受的福利放到一個(gè)列表里。這個(gè)用來供前端展示用戶當(dāng)前可享受到的福利。

          然后,在每一項(xiàng)福利中,我去設(shè)定一個(gè)可享受此福利的最低級別。只有用戶的級別超過這個(gè)最低級別的時(shí)候,才可以享受到此項(xiàng)福利。比如,支付優(yōu)惠 9.5 折,我只需要在支付服務(wù)中打包個(gè)支付權(quán)利 9.5 折這種東西,然后設(shè)定個(gè)最低級別即可。

          這事兒看著是如此簡單,所以,實(shí)現(xiàn)方案也沒什么特殊的。當(dāng)用戶每次升級的時(shí)候,我只需要更新用戶級別即可。

          這個(gè)時(shí)候,需求比較初級,要求也不高。在滿足升級條件后,需要用戶主動點(diǎn)擊升級。同時(shí),再填寫一些相關(guān)信息,申請一些專屬的福利就可以了。

          好,設(shè)計(jì),開發(fā),上線一條龍走起來!

          需求變成坑

          過了一陣子,我們的運(yùn)營們勇于探索,勤于開拓,去搞了一堆資源互換回來。當(dāng)我聽說此事時(shí),心里已經(jīng)預(yù)感不妙了。

          果然,沒兩天,我們的產(chǎn)品高高興興地通知我,由于兄弟團(tuán)隊(duì)愿意和我們的項(xiàng)目進(jìn)行合作,因此用戶的福利將得到極大的豐富,那些更加豐富的福利全都由兄弟團(tuán)隊(duì)提供。

          所以,請我簡單的搞一下,對接上這些合作方,進(jìn)一步提升我們系統(tǒng)的粘性。

          如常,依然沒有任何文檔,我依然只能自己分析。

          現(xiàn)在,根據(jù)我豐富的被折騰經(jīng)驗(yàn),我知道開始有坑了。當(dāng)我對接合作方接口的時(shí)候,他們都需要我傳入一些特定的用戶標(biāo)識過去,可以讓雙方共享用戶。

          需求開始復(fù)雜了,不過慶幸的是,我改改代碼就可以了,還好還好,我松了口氣……

          好,設(shè)計(jì),開發(fā),上線一條龍走起!

          可惜,我們的業(yè)務(wù)就像一群群的蜜蜂一樣,你永遠(yuǎn)不知道他們會給你帶來什么樣的花朵。

          沒過過久,產(chǎn)品告訴我,幾個(gè)兄弟團(tuán)隊(duì)想和我們一起搞一次超級大活動。我覺得天黑了……

          沒文檔沒有產(chǎn)品原型,依然就是微信中的來來往往。

          我知道此時(shí),我得往深里想想了。需求是可以肆意妄為的,而我能阻止業(yè)務(wù)需求的肆意妄為嗎?不能,所以,我要考慮一整套彈性的方案,能應(yīng)對這些千變?nèi)f化,又漫天飛舞的需求。

          二、初見

          隱患的伊始

          來看看這個(gè)見鬼的大活動吧。

          首先,按照設(shè)計(jì),如果合作方們想要和我們一起大聯(lián)歡,那么我們就要把用戶升級的信息告訴他們。這樣,合作方們才能進(jìn)行驗(yàn)證,并提供用戶級別對應(yīng)的福利。所以,當(dāng)我們的用戶升級的時(shí)候,我需要每次都把這件事同步給我們的合作方。

          又因?yàn)槲覀兪呛投鄠€(gè)兄弟團(tuán)隊(duì)合作,比如,和物流團(tuán)隊(duì)合作,和支付團(tuán)隊(duì)合作。在這種情況下,不同合作方的互動邏輯是分布在不同的服務(wù)中的。

          此時(shí),我有兩種方案可供選擇:

          1. 在用戶服務(wù)里,用戶升級時(shí),立即主動的通過接口去調(diào)用分布在不同的服務(wù)上的相關(guān)邏輯,把用戶升級這件事同步到合作方那里。但是,這個(gè)方案有個(gè)很大的問題——因?yàn)槲覀冃枰{(diào)用其他服務(wù)的接口,這就造成服務(wù)和服務(wù)之間耦合起來了。將來有點(diǎn)小改動,可能都需要我們改代碼。

          2. 在微服務(wù)里,其實(shí)是很推崇使用消息隊(duì)列的。當(dāng)用戶升級時(shí),我只需發(fā)送消息到消息隊(duì)列中,然后讓相關(guān)的服務(wù)去訂閱這個(gè)消息即可。這個(gè)方案,使用消息隊(duì)列可以解耦服務(wù)之間的關(guān)系。

          因?yàn)槲⒎?wù)本身的目的就是解耦和靈活,并且第二個(gè)方案和我們架構(gòu)是適配的,因此我選擇了第二個(gè)方案。

          在第二個(gè)方案中,正因?yàn)橄⒖梢园逊?wù)之間進(jìn)行解耦,所以,當(dāng)用戶升級的時(shí)候,我只需要操作用戶服務(wù)數(shù)據(jù)庫中的用戶表進(jìn)行升級,并把升級這事兒包裹成消息扔到消息隊(duì)列中即可。

          我甚至可以把更新用戶表發(fā)送升級消息到隊(duì)列包裝成一個(gè)事務(wù)。

          好,設(shè)計(jì),開發(fā),上線一條龍走起!

          這就是能應(yīng)對后續(xù)不斷變化的技術(shù)方案嗎?事實(shí)證明,并不能,因?yàn)?,這套方案即將會被變化的需求給徹底擊垮。

          問題的大爆發(fā)

          斗轉(zhuǎn)星移,時(shí)空變幻。需求如滾滾的流水般涌來,而我們的技術(shù)方案如同一套無論如何增強(qiáng)也不夠健壯的大壩。

          經(jīng)過幾度需求的變換,此時(shí)用戶升級已經(jīng)變成了滿足條件后自動升級;我們合作的兄弟團(tuán)隊(duì)也日益增多;我們的服務(wù)也越拆越多……在這些汩汩涌出的變化中,問題已經(jīng)如同潛伏在水底的鱷魚,即將爬上岸來獵取幾個(gè)程序員來祭天了。

          問題的跡象一開始出現(xiàn)在用戶升級的數(shù)據(jù)上。那時(shí),我們接連被運(yùn)營們提的問題所困擾。

          有些運(yùn)營人員發(fā)現(xiàn),某些用戶升級過快了,用戶的升級速度已經(jīng)遠(yuǎn)遠(yuǎn)超出了當(dāng)初設(shè)計(jì)時(shí)預(yù)估的速度了。

          而這種過快的升級不僅使得運(yùn)營人員無法及時(shí)構(gòu)思和設(shè)計(jì)后續(xù)的運(yùn)營活動,還使得我們的運(yùn)營成本快速的上漲,并因此給公司經(jīng)營帶來了一定的損失。

          當(dāng)然,如同以往一樣,業(yè)務(wù)是從來不會出錯(cuò)的,出錯(cuò)的永遠(yuǎn)是技術(shù)。這不,出問題的原因都給我們安排的明明白白了:

          很可能是程序出了 bug,因?yàn)槌隽四承┘夹g(shù)性的故障,導(dǎo)致用戶升級的時(shí)候沒有一級級的升上去,出現(xiàn)了跳躍性的升級…………

          在追蹤問題的時(shí)候,我們猛然發(fā)現(xiàn)了這個(gè)技術(shù)方案的一個(gè)缺陷:由于根本沒有預(yù)料到用戶升級的重要性,我們的很多用戶升級相關(guān)的日志并未開啟,并且沒有存儲任何用戶升級的歷史記錄。

          這瞬間成了一筆糊涂賬,我無 fuck 可說。

          雪上加霜的是,又有用戶們投訴,他們總是在某些時(shí)候會出現(xiàn)一些卡頓。我們再一查,發(fā)現(xiàn)是用戶升級導(dǎo)致的數(shù)據(jù)庫問題。

          最早的設(shè)計(jì)是用戶升級直接更新數(shù)據(jù)庫表,但是大意了:

          • 當(dāng)用戶數(shù)量出現(xiàn)大漲的時(shí)候。
          • 新用戶初期升級難度小,所以升級很頻繁。

          忽略了這兩個(gè)因素,這就造成了我們的數(shù)據(jù)庫有點(diǎn)承受不住這種頻繁的更新。

          而且,在查這些問題的時(shí)候,以前有些用戶投訴的問題也隨之被挖了出來。比如,用戶升級后有些福利卻沒有給他們,悲催的是這些痕跡也沒有被完整的留下來……

          糊涂賬加糊涂賬成了筆爛賬。

          啊,我要被祭天了嗎?

          跺腳后智商重新占領(lǐng)高地

          現(xiàn)在來看看我們要面臨的問題吧。

          首先出場的是用戶升級沒法追根溯源的問題。因?yàn)槲覀兠看斡脩羯?,需要通知相關(guān)的服務(wù),然后還得保證每個(gè)相關(guān)的服務(wù)處理成功了,到此時(shí),用戶升級才算真正的成功。所以,為了能還技術(shù)們一個(gè)清白,能別搞得成為爛賬,就必須把用戶的每次升級給記錄下來,并且還得把每個(gè)相關(guān)服務(wù)對升級事件的處理也記錄在案。

          下一個(gè)要解決的小兄弟是數(shù)據(jù)庫更新的問題。這個(gè)數(shù)據(jù)庫更新該怎么辦?緩存后同步?那緩存本身的更新出現(xiàn)了問題怎么辦?驗(yàn)證唄!怎么驗(yàn)證?每次升級時(shí)候去和歷史記錄核對一遍嗎?

          這時(shí)候,我的腦袋里開始進(jìn)入了混沌狀態(tài)。不知道該怎么辦了。

          有點(diǎn)著急啊,怎么辦呢?只好去看看網(wǎng)上有沒有什么方案可以提供一些思路。

          最終,這就促成了我對事件溯源(Event Sourcing)模式的初見。

          當(dāng)我看到事件溯源的時(shí)候,我腳一跺,我感覺我的智商回來了。

          事件溯源拯救快被祭天的我

          首先,咱們看看事件溯源是什么樣的。

          以咱們現(xiàn)在搞得用戶升級為例,說一下事件溯源模式:

          用戶升級時(shí),我們只需要把用戶升級這件事通過 Event Store 這個(gè)中間件傳給支付服務(wù)、物流服務(wù)等這些相關(guān)的服務(wù)。然后,支付服務(wù)、物流服務(wù)之類的處理完用戶升級通知給他們的事件后,會也創(chuàng)建一個(gè)事件對象,放到 Event Store 里。

          這里的 Event Store 其實(shí)主要是用來做兩件事:

          • 傳遞事件
          • 存儲事件歷史

          那么,事件溯源是怎么來搞定我面臨的這些問題的呢?

          首先,如果我們要追根溯源,就需要把用戶升級和用戶升級后相關(guān)服務(wù)做得處理都要存起來,形成一個(gè)完整的業(yè)務(wù)鏈條。有了這個(gè)鏈條,才能被稱為追根溯源。

          事件溯源模式正好告訴大家,有事兒就要存起來!

          其次,當(dāng)我們用戶升級的時(shí)候把事件存儲下來之后,我們還需要實(shí)時(shí)去更新級別嗎?

          我們來分析一下:用戶升級的真正目的是什么?從業(yè)務(wù)角度來說,其實(shí)就是通過提供各種福利去提升用戶的活躍度。那么,這件事需要實(shí)時(shí)嗎?似乎不必須,因?yàn)橛脩魩缀醪惶赡苌壓篑R上去使用對應(yīng)的福利。

          好,如果可以不實(shí)時(shí),那么用戶升級這件事兒就能避免實(shí)時(shí)更新數(shù)據(jù)庫了。

          如果我們在開始把歷史事件存儲下來了之后,其實(shí)可以在凌晨的時(shí)候去定時(shí)根據(jù)用戶級別發(fā)生的事件,去把用戶的級別升級到正確的級別。

          所以可以看到了,事件溯源在這事兒上把我的兩個(gè)問題全解決了。

          這就是我和事件溯源模式的初見。而在今后的技術(shù)生涯中,它將會經(jīng)常陪伴著我。

          三、認(rèn)識

          真正認(rèn)識下事件溯源模式吧

          事件溯源總結(jié)下來其實(shí)只有如下二個(gè)核心特點(diǎn):

          1. 把觸發(fā)業(yè)務(wù)數(shù)據(jù)變化的原因包裝成了事件對象——如果把這件事兒抽象的看待一下,就是我們可以把業(yè)務(wù)中任何需要注意的情況發(fā)生變化時(shí),都可以包裝成事件。

          2. 這些包裝成事件的業(yè)務(wù)數(shù)據(jù)會按照事件發(fā)生的順序,被持久化存儲到專門的地方——需要專門說一下這個(gè)事件按照順序存放的問題,在事件溯源模式中,按照事件發(fā)生的順序持久化存儲是非常重要的一件事。如果一個(gè)模式中的事件沒有嚴(yán)格按照事件順序進(jìn)行持久化存儲,其實(shí)很難說這個(gè)模式會是一個(gè)合格的事件溯源模式。

          所以事件溯源模式就做了兩件事:

          • 定義什么樣的業(yè)務(wù)邏輯可以被定義為事件;
          • 把定義好的事件在發(fā)生后給按順序記錄下來。

          事件溯源常伴吾身

          認(rèn)識到了事件溯源的核心特點(diǎn)后,我在后面的開發(fā)生涯里反復(fù)的使用了這個(gè)模式去幫我解決不同業(yè)務(wù)的特定場景的問題。比如訂單的狀態(tài)更新,再比如秒殺活動的性能問題。

          在不斷地使用事件溯源過程中,我總結(jié)出了需要使用事件溯源的一些場景。當(dāng)遇到類似的場景時(shí),我總是會第一時(shí)間嘗試用事件溯源模式來解決問題。

          這些場景是:

          • 想知道關(guān)鍵數(shù)據(jù)被更改時(shí),意圖、原因或者目的時(shí);

          • 更新數(shù)據(jù)確實(shí)性能出現(xiàn)了問題,一時(shí)之間也沒辦法通過硬件升級或者大規(guī)模集群去解決這個(gè)問題;

          • 還原某些現(xiàn)場,或者想通過一些數(shù)據(jù)重復(fù)的還原線上環(huán)境是非常重要的事情;

          而事實(shí)證明,在這些場景中使用事件溯源也確實(shí)不負(fù)我望,并且還帶來了很多額外的好處:

          1. 由于事件可以按照順序存儲,所以可以搞成追加方式去持久化,而這種追加操作來持久化事件的方式可以放到前臺,對用戶體驗(yàn)或者性能要求很高的地方。這樣不會引發(fā)前臺卡頓。同時(shí)呢,可以讓事件能跟水流一樣,被引入到后臺任務(wù)中慢慢處理。

          2. 事件本身是一種場景記錄,所以,利用這些記錄的時(shí)候,可以根據(jù)自身情況,在任何合適的時(shí)間,合適的環(huán)境,去根據(jù)事件去實(shí)施或者復(fù)現(xiàn)某些業(yè)務(wù)狀態(tài)。

          3. 事件的存儲本身可以被當(dāng)成一種審計(jì)日志,只要記錄的信息夠全,事件溯源本身就會天然的變成可靠安全的審計(jì)數(shù)據(jù)。

          4. 事件溯源本身可以和各種事件驅(qū)動的系統(tǒng)相融合,非常適合擴(kuò)展和對接各類靠事件驅(qū)動的應(yīng)用和系統(tǒng)。

          5. 事件溯源不會給已經(jīng)非常復(fù)雜的業(yè)務(wù)對象增加復(fù)雜度。比如,一個(gè)訂單對象,根據(jù)訂單對象設(shè)計(jì)訂單表的時(shí)候,可能還得搞個(gè)備注字段用來存儲一些更新時(shí)的說明;可能還得搞個(gè)最近更新時(shí)間記錄下最近更新發(fā)生在什么時(shí)候;甚至可能由于本身業(yè)務(wù)狀態(tài)的復(fù)雜,還得特意拆解成幾個(gè)不同的狀態(tài)字段……

          總之,隨著我對事件溯源認(rèn)識的逐漸加深,我覺得自身已經(jīng)開始有了微服務(wù)專家的氣質(zhì)。

          四、不滿

          當(dāng)然,太陽底下沒啥新鮮事兒。任何新東西的引入總會帶來一些不足,同時(shí)呢,隨著使用事件溯源模式的次數(shù)增多,我也愈發(fā)認(rèn)識到了這個(gè)模式的不足。

          1. 要存儲的事件數(shù)據(jù)太多了,導(dǎo)致查詢得引入另一個(gè)查詢職責(zé)分離模式(CQRS),才能解決大部分的查詢問題。

          2. 使用事件溯源的時(shí)候由于事件發(fā)生的順序存儲非常重要,所以,使用多線程,多進(jìn)程,集群的時(shí)候,就必須要嚴(yán)格保證事件順序存儲的正確性,一般來說,得給事件對象搞個(gè)時(shí)間戳不說,可能還得引入全局唯一標(biāo)識符產(chǎn)生器去產(chǎn)生事件 ID。

          3. 由于事件本身是個(gè)業(yè)務(wù)對象了,所以,你知道了,它自身一定會進(jìn)化的。所以,還得考慮老版本新版本的共存問題,這種一般至少得給事件結(jié)構(gòu)弄個(gè)版本字段去標(biāo)識事件對象的版本。

          4. 事件存下來了,而且大部分時(shí)候都是附加形式的順序存儲。這就導(dǎo)致查詢事件的時(shí)候沒辦法,只能按照事件標(biāo)識符和事件的時(shí)間之類的做查詢,而這樣的話,其實(shí)就是查詢出來了一個(gè)事件流。如果要場景重現(xiàn)和分析業(yè)務(wù)對象狀態(tài)的時(shí)候,就非得把這個(gè)事件流給整個(gè)重新處理一遍。

          5. 事件溯源這事兒其實(shí)就是人為的松綁了業(yè)務(wù)的一致性要求。但是,業(yè)務(wù)需要的一致性問題依然還是需要另外的處理。比如,我們搞了電商網(wǎng)站,同時(shí)呢,又通過事件溯源模式去落地了庫存商品數(shù)量更新的業(yè)務(wù),又恰巧把庫存的存貨減少的各種原因給設(shè)計(jì)成了不同的事件,那么,當(dāng)庫存因?yàn)榉强蛻粝聠螠p少發(fā)生時(shí),又恰好客戶在下單,這時(shí)候,就需要單獨(dú)的處理他們之間的沖突,去保證狀態(tài)的一致性。

          6. 事件這東西本身可能因?yàn)闃I(yè)務(wù)原因需要各種傳遞,而在這期間,不管使用什么方式去傳播事件,沒人會給你保證事件不會重復(fù)傳播。這時(shí)候,就得考慮處理事件的冪等性。這也是事件溯源帶來的麻煩。

          五、結(jié)尾

          事件溯源模式雖然解決了我的很多問題,但是同時(shí)又因?yàn)橐脒@個(gè)模式,我又增加了很大的工作量。真是金無足赤啊。

          也許這世上根本不存在什么溯源模式,有的只是防止背鍋的無奈罷了。



          你好,我是四猿外。

          一家上市公司的技術(shù)總監(jiān),管理的技術(shù)團(tuán)隊(duì)一百余人。

          我從一名非計(jì)算機(jī)專業(yè)的畢業(yè)生,轉(zhuǎn)行到程序員,一路打拼,一路成長。

          我會通過公眾號,
          把自己的成長故事寫成文章,
          把枯燥的技術(shù)文章寫成故事。

          我建了一個(gè)讀者交流群,里面大部分是程序員,一起聊技術(shù)、工作、八卦。歡迎加我微信,拉你入群。

          推薦閱讀

          我,管理100多人團(tuán)隊(duì)的二三事

          向老李學(xué)習(xí),還得帶著團(tuán)隊(duì)做出成績

          瀏覽 39
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  无码一区二区三区在线 | 操逼热在线观看 | 日韩av在线直播 日韩高清一级免费 | 国产视频一区在线 | 777无码 |