https://zhuanlan.zhihu.com/p/38968012
Event Sourcing也叫事件溯源,是這些年另一個(gè)越來越流行的概念,是大神Martin Fowler提出的一種架構(gòu)模式。簡單來說,它有幾個(gè)特點(diǎn):
- 整個(gè)系統(tǒng)以事件為驅(qū)動,所有業(yè)務(wù)都由事件驅(qū)動來完成。
- 事件是一等公民,系統(tǒng)的數(shù)據(jù)以事件為基礎(chǔ),事件要保存在某種存儲上。
- 業(yè)務(wù)數(shù)據(jù)只是一些由事件產(chǎn)生的視圖,不一定要保存到數(shù)據(jù)庫中。
什么是Event Sourcing
這么說可能還是比較難以理解,我們來舉個(gè)栗子。這是一個(gè)賬戶余額管理的例子:

在這個(gè)圖中,中間的是我們的賬戶對象,它有幾個(gè)時(shí)間處理函數(shù)create(), deposit(), withdraw(),分別用于處理新建賬戶、賬戶存款和取款的操作。
左邊的就是一個(gè)個(gè)的事件,它是一個(gè)事件的流,根據(jù)用戶請求或者從其他地方產(chǎn)生。在這里例子當(dāng)中,有3個(gè)事件:AccountCreated, AccountDeposited, AccountWithdrawed,分別相當(dāng)于賬戶創(chuàng)建的事件,存款的事件和取款的事件。當(dāng)這些事件產(chǎn)生的時(shí)候,我們會觸發(fā)上面的Account對象的相應(yīng)的處理函數(shù)。
右邊的就是這個(gè)Account對象處理完左邊的4個(gè)事件以后,最新的數(shù)據(jù)狀態(tài)。具體的處理過程就是:
- 系統(tǒng)產(chǎn)生一個(gè)新建賬戶的事件
AccountCreated,Account對象來處理這個(gè)事件,事件里面的Id是1234,系統(tǒng)先嘗試著找id是1234的賬戶,發(fā)現(xiàn)沒有,于是新建一個(gè)Account對象,并在它上面調(diào)用create()的處理函數(shù),也就是初始化了id和余額。 - 然后,有一個(gè)
AccountDeposited的事件,對應(yīng)的Account對象的id是1234,系統(tǒng)找到之前創(chuàng)建的對象,在它上面應(yīng)用deposit()處理函數(shù),也就是增加的余額的操作,更新了賬戶余額。 - 系統(tǒng)又收到一個(gè)存款事件,跟上面一樣,又更新了一次余額。
- 收到一個(gè)取款事件,還是找到id是1234的賬戶,在它上面調(diào)用
withdraw()的處理函數(shù),進(jìn)行取款操作,更新余額。 - 最后,1234這個(gè)賬戶的余額是200,也就是右邊的數(shù)據(jù)狀態(tài)。
同時(shí),上面的這些事件需要持久保存在數(shù)據(jù)庫或其他地方,而account的數(shù)據(jù)狀態(tài)卻不需要保存,我們只是在需要獲得account當(dāng)前的數(shù)據(jù)狀態(tài)的時(shí)候,通過這個(gè)account相關(guān)的事件,調(diào)用他們的處理函數(shù),重新生成當(dāng)前狀態(tài)。當(dāng)然,每次都這樣調(diào)用處理函數(shù)勢必會造成資源的浪費(fèi),因?yàn)樗枰獜臄?shù)據(jù)庫中取得所有這個(gè)account的事件,然后依次調(diào)用處理函數(shù)。所以一般我們可以把這個(gè)account的最新狀態(tài),以一種視圖的方式保存在數(shù)據(jù)庫中。
上面這個(gè)方式和過程,就是我們說的Event Sourcing,也就是以事件為源的處理模式。
Event Sourcing的構(gòu)成
通過上面的例子,我們理解了Event Sourcing(事件溯源),下面我們再來看看Event Sourcing包含哪些部分。
聚合對象
在上面的例子中,Account對象就是一個(gè)聚合對象,它里面包含賬戶的基本信息,也包含了對賬戶操作時(shí)的處理方法,也就是幾個(gè)事件處理函數(shù)。了解領(lǐng)域驅(qū)動設(shè)計(jì)的人這時(shí)候應(yīng)該就想到,這個(gè)Account對象其實(shí)就是一個(gè)領(lǐng)域模型,Account這個(gè)領(lǐng)域模型需要的業(yè)務(wù)操作,由它自己提供。
每個(gè)聚合對象都有一個(gè)Id,用于唯一標(biāo)識這個(gè)對象,所以系統(tǒng)中不同的賬戶就會有不同的Account對象。
Event Store
我們說了,在Event Sourcing模式當(dāng)中,所有的事件都是要保存到數(shù)據(jù)庫(或其他存儲,下面就直接說數(shù)據(jù)庫了)中的,這個(gè)存儲就叫Event Store。
每個(gè)事件應(yīng)該也包含一個(gè)它要處理的聚合對象的id,以及事件的順序,查詢的時(shí)候就是根據(jù)聚合對象的id從數(shù)據(jù)庫中找到相關(guān)的事件,并按照生成的事件或序號排序。
Event Store除了提供事件數(shù)據(jù)的存儲、查詢功能以外,還可以提供事件的重現(xiàn)等功能。事件的重現(xiàn),就是將截止某一個(gè)時(shí)間的所有事件取出來,調(diào)用他的處理函數(shù),生成當(dāng)時(shí)那個(gè)時(shí)間點(diǎn)的業(yè)務(wù)狀態(tài)。所以在重現(xiàn)之前,如果我們的業(yè)務(wù)數(shù)據(jù)的狀態(tài),通過視圖的形式保存到了數(shù)據(jù)庫中,我們需要先清除相應(yīng)的數(shù)據(jù)。正是由于Event Sourcing模式的這個(gè)以事件為源的特性,所以我們才有可能提供這樣的歷史重現(xiàn)的功能。
聚合資源庫
一般情況下,我們的聚合對象的數(shù)據(jù)狀態(tài)是不會保存在數(shù)據(jù)庫當(dāng)中的。每當(dāng)系統(tǒng)要獲得某一個(gè)賬戶的數(shù)據(jù)的時(shí)候,都是從Event Store當(dāng)中取出所有相關(guān)聚合對象的事件,然后依次的調(diào)用這些事件的處理方法,“聚合”出該領(lǐng)域?qū)ο笞钚碌臄?shù)據(jù)狀態(tài)。這個(gè),就是聚合資源庫需要提供的功能。
視圖
上面我們也說了,如果每次都重新“聚合”出對象,獲取當(dāng)前的狀態(tài),會浪費(fèi)很多資源。所以,我們可以在某個(gè)事件發(fā)生的時(shí)候,將這個(gè)聚合對象的最新數(shù)據(jù)狀態(tài),寫到一個(gè)表中,這個(gè)表可以叫做物化視圖。
查詢
由于我們提供了專門的視圖表,將聚合對象的最新狀態(tài)保存在數(shù)據(jù)庫中,那我們在查詢的時(shí)候,可以通過該物化視圖去查詢,而不是通過聚合對象的資源庫去查詢。
Event Sourcing與CQRS
CQRS,是 Command Query Responsibility Segregation的縮寫,也就是通常所說的讀寫隔離。在上面,我們說,為了性能考慮,將聚合對象的數(shù)據(jù)狀態(tài)用物化視圖的形式保存,可以用于數(shù)據(jù)的查詢操作,也就是我們把數(shù)據(jù)的更新與查詢的流程隔離開來。我們通過事件來更新聚合對象的數(shù)據(jù)狀態(tài),同時(shí)由另一個(gè)處理器處理相同的事件,來更新物化視圖的數(shù)據(jù)。
所以,Event Sourcing與CQRS有著天然的聯(lián)系,所以也經(jīng)常會有人把他們放在一起討論。實(shí)際上,CQRS是在使用Event Sourcing模式以后,又使用了物化視圖的情況下,所產(chǎn)生的額外的好處。
下圖就是使用Event Sourcing好CQRS模式以后的一個(gè)簡單的流程圖:

- 對于Command類型的請求(需要修改數(shù)據(jù)),web層會走通過Event Sourcing更新聚合對象的流程,這時(shí)會有一個(gè)Event Handler的處理類監(jiān)聽相應(yīng)事件,更新物化視圖。
- 對于Query類型的請求,web層會通過相應(yīng)的DAO獲取數(shù)據(jù)返回。
Event Sourcing的優(yōu)點(diǎn)與缺點(diǎn)
Event Sourcing之所以會越來越受到關(guān)注,是因?yàn)樗囊恍﹥?yōu)點(diǎn):
- 方便進(jìn)行溯源與歷史重現(xiàn)
這個(gè)“溯源”的意思是,我們可以通過對保存的事件的分析,知道現(xiàn)在的系統(tǒng)的狀態(tài),是怎么一步一步的變成這樣的。這在一個(gè)大型的業(yè)務(wù)復(fù)雜的應(yīng)用系統(tǒng)里尤為有用。如果沒有使用Event Sourcing模式,即使我們使用完備的log機(jī)制,提供log查詢分析,也很難追溯數(shù)據(jù)的變化過程。
此外,我們還可以根據(jù)歷史的事件,重新生成所有的業(yè)務(wù)狀態(tài)數(shù)據(jù)。我們甚至可以指定我要生成到具體某一時(shí)刻的狀態(tài)。這就好像我們可以自由的穿梭在我們的系統(tǒng)運(yùn)行過程當(dāng)中,查看任何一個(gè)時(shí)間點(diǎn)的狀態(tài)。 - 方便Bug的修復(fù)
由于我們可以重現(xiàn)歷史,所以當(dāng)發(fā)現(xiàn)一個(gè)bug以后,我們可以在修復(fù)完以后,直接重新聚合我們的業(yè)務(wù)數(shù)據(jù),修復(fù)我們的數(shù)據(jù)。如果使用傳統(tǒng)的設(shè)計(jì)方法,我們就需要通過SQL或者寫一段程序,去手動的修改數(shù)據(jù)庫,以使它達(dá)到正常的狀態(tài)。如果這個(gè)bug存在的時(shí)間較長,牽扯的數(shù)據(jù)較多,這將會是一個(gè)非常麻煩的事情。
同時(shí),由于我們可以通過事件分析業(yè)務(wù)的過程,這也經(jīng)常能幫助我們發(fā)現(xiàn)問題。 - 能提供非常好的性能
在Event Sourcing模式下,事件數(shù)據(jù)的保存是一個(gè)一直新增的寫表操作,沒有更新。這在很多情況下都能夠提供一個(gè)非常好的寫的性能,讓系統(tǒng)的接收事件的吞吐量可以很高。
然后聚合對象根據(jù)事件的聚合Id,獲取所有相關(guān)的事件,“聚合”出對象,調(diào)用業(yè)務(wù)方法。但是它的結(jié)果又不需要寫數(shù)據(jù)庫,這里面只有一個(gè)讀操作,其他的操作都是在內(nèi)存中。
最后,由另一個(gè)Event Handler處理這些事件,更新物化視圖的表。在更新數(shù)據(jù)的時(shí)候,我們只需要鎖記錄,所以這個(gè)更新的過程也可以很快。
通過這種模式,我們的系統(tǒng)的壓力最終基本上都落在數(shù)據(jù)庫上,整個(gè)系統(tǒng)里不會有太多鎖和等待(只有并發(fā)的在同一個(gè)聚合對象上處理,才會等待,例如用戶同時(shí)發(fā)了多個(gè)請求進(jìn)行存款取款操作),就可以提供非常好的吞吐量。 - 方便使用數(shù)據(jù)分析等系統(tǒng)
這個(gè)就很明顯了,用了Event Sourcing模式,我們的數(shù)據(jù)就是事件,我們只需要在現(xiàn)有的事件是加上需要的處理方法,來做數(shù)據(jù)分析;或者將event直接發(fā)送到某個(gè)分析系統(tǒng)。我們就不需要為了做數(shù)據(jù)分析,再在系統(tǒng)里定義各種事件,發(fā)送事件等。
當(dāng)然,它也有一些缺點(diǎn):
- 開發(fā)思維的轉(zhuǎn)變
最大的難點(diǎn),估計(jì)就是對開發(fā)人員的思維方式的轉(zhuǎn)變。使用Event Sourcing模式,需要我們從設(shè)計(jì)角度,使用一定的領(lǐng)域驅(qū)動設(shè)計(jì)的方法,從開發(fā)角度,我們又需要使用基于事件的響應(yīng)式編程思維。對于習(xí)慣了傳統(tǒng)的面向?qū)ο蟮某绦騿T來說,這都是一個(gè)不小的挑戰(zhàn)。 - 沒有成熟完善的框架
我們開發(fā)Java的應(yīng)用,現(xiàn)在絕大多數(shù)情況下都會使用Spring,也有很大一部分使用Spring MVC(或Spring Boot)。不管怎么樣,都是基于Spring這個(gè)框架家族進(jìn)行開發(fā)。但是,對于Event Sourcing模式來說,還沒有一個(gè)大而一統(tǒng)的框架,既能提高很好的Event Sourcing模式的實(shí)現(xiàn),又能被廣泛接受,最好還能有一些廠商提高商業(yè)服務(wù),保證整個(gè)生態(tài)的良性發(fā)展。 - 事件的結(jié)構(gòu)的改變
使用Event Sourcing模式,還有一個(gè)問題就是事件結(jié)構(gòu)的改變。由于業(yè)務(wù)的變化,我們設(shè)計(jì)的事件,在結(jié)構(gòu)上可能有一些改變,可能需要添加一些數(shù)據(jù),或者刪除一些數(shù)據(jù)。那么這時(shí)候,想要進(jìn)行方才說的“歷史重現(xiàn)”就會有問題。這時(shí)我們就需要通過某種方式給他提供兼容。 - 從領(lǐng)域模型角度設(shè)計(jì)系統(tǒng),而不是以數(shù)據(jù)庫表為基礎(chǔ)設(shè)計(jì)
這其實(shí)不算是一個(gè)缺點(diǎn),但是由于領(lǐng)域驅(qū)動設(shè)計(jì)并不是廣泛使用的軟件設(shè)計(jì)方式,很多開發(fā)人員對此不了解,相應(yīng)的設(shè)計(jì)和開發(fā)方式也不熟悉,所以這也成為使用Event Sourcing模式開發(fā)需要解決的問題。
領(lǐng)域驅(qū)動設(shè)計(jì)從業(yè)務(wù)分析出發(fā),從領(lǐng)域模型設(shè)計(jì)著手設(shè)計(jì)一個(gè)系統(tǒng),而在設(shè)計(jì)一個(gè)基于Event Sourcing模式的系統(tǒng)時(shí),我們往往也要用到領(lǐng)域模型設(shè)計(jì)的一些方法,從領(lǐng)域模型設(shè)計(jì)開始,設(shè)計(jì)聚合對象和它的業(yè)務(wù)(事件),以及處理方法(Event Handler)。通過領(lǐng)域驅(qū)動設(shè)計(jì),對復(fù)雜的應(yīng)用系統(tǒng)往往能提供更好的設(shè)計(jì)。但是,這種設(shè)計(jì)方式又和我們常用的設(shè)計(jì)方法不一致,有一定的學(xué)習(xí)和實(shí)踐成本。
基于Event Sourcing的分布式系統(tǒng)
如果要開發(fā)一個(gè)基于Event Sourcing模式的分布式系統(tǒng),最簡單的方式就是用2個(gè)服務(wù)分別提供讀和寫的功能。寫服務(wù)接收Command請求,觸發(fā)聚合對象上的處理函數(shù),更新聚合數(shù)據(jù)。然后把這個(gè)事件發(fā)送到一個(gè)MQ隊(duì)列上。讀服務(wù)監(jiān)聽這個(gè)隊(duì)列,獲取事件,更新相應(yīng)的物化視圖的數(shù)據(jù)。同時(shí)所有的Query請求都由讀服務(wù)處理并返回。
對于寫服務(wù),它的數(shù)據(jù)只有事件,是一個(gè)流式的寫操作,還有基于索引的查詢。對于讀服務(wù),我們又可以部署多個(gè)應(yīng)用,進(jìn)一步提供數(shù)據(jù)查詢的性能??梢钥吹剑ㄟ^這么一個(gè)簡單的讀寫分離,我們就能大大提高系統(tǒng)的性能。
什么時(shí)候使用Event Sourcing
使用Event Sourcing有它的優(yōu)點(diǎn)也有缺點(diǎn),那么什么時(shí)候該使用Event Sourcing模式呢?
- 首先是系統(tǒng)類型,如果你的系統(tǒng)有大量的CRUD,也就是增刪改查類型的業(yè)務(wù),那么就不適合使用Event Sourcing模式。Event Sourcing模式比較適用于有復(fù)雜業(yè)務(wù)的應(yīng)用系統(tǒng)。
- 如果你或你的團(tuán)隊(duì)里面有DDD(領(lǐng)域驅(qū)動設(shè)計(jì))相關(guān)的人員,那么你應(yīng)該優(yōu)先考慮使用Event Sourcing。
- 如果對你的系統(tǒng)來說,業(yè)務(wù)數(shù)據(jù)產(chǎn)生的過程比結(jié)果更重要,或者說更有意義,那就應(yīng)該使用Event Sourcing。你可以使用Event Sourcing的事件數(shù)據(jù)來分析數(shù)據(jù)產(chǎn)生的過程,解決bug,也可以用來分析用戶的行為。
- 如果你需要系統(tǒng)提供業(yè)務(wù)狀態(tài)的歷史版本,例如一個(gè)內(nèi)容管理系統(tǒng),如果我想針對內(nèi)容實(shí)現(xiàn)版本管理,版本回退等操作,那就應(yīng)該使用Event Sourcing。
大家可以關(guān)注一下課程:《分布式事務(wù)實(shí)踐 解決數(shù)據(jù)一致性 》
作者:大漠風(fēng)
鏈接:http://www.imooc.com/article/40858
來源:慕課網(wǎng)