本文將介紹聚合以及與其高度相關(guān)的并發(fā)主題。
我在之前已經(jīng)說過,初學(xué)者第一步需要將業(yè)務(wù)邏輯盡量放到實(shí)體或值對(duì)象中,給實(shí)體“充血”,這樣可以讓業(yè)務(wù)邏輯高度內(nèi)聚,并為你提供業(yè)務(wù)邏輯的唯一訪問點(diǎn)。而聚合則是第二步,它將多個(gè)相關(guān)業(yè)務(wù)概念包裝到單一的概念中,從而大幅簡化系統(tǒng)設(shè)計(jì),由于受傳統(tǒng)數(shù)據(jù)建模思維影響,我在聚合方面吃過大虧,花了將近一年才真正用起來,為了你少走彎路,我會(huì)把一些要點(diǎn)總結(jié)出來供你參考。
什么是聚合?
聚合包裝一組高度相關(guān)的對(duì)象,作為一個(gè)數(shù)據(jù)修改的單元。
聚合最外層的對(duì)象稱為聚合根,它是一個(gè)實(shí)體。聚合根劃分出一個(gè)清晰的邊界,聚合根外部的對(duì)象,不能直接訪問聚合根內(nèi)部對(duì)象,如果需要訪問內(nèi)部對(duì)象,必須首先訪問聚合根,再導(dǎo)航到聚合的內(nèi)部對(duì)象。
聚合代表很強(qiáng)的包含關(guān)系,聚合內(nèi)部的對(duì)象脫離了聚合,應(yīng)該是毫無意義的,或不是你真正關(guān)注的,它是聚合的一個(gè)組成部分,這與UML中的組成聚合概念相近。
聚合的作用
簡化系統(tǒng)設(shè)計(jì)
在剛開始接觸DDD時(shí),我們受到傳統(tǒng)數(shù)據(jù)建模思維影響,根據(jù)范式要求設(shè)計(jì)出多張表,會(huì)很自然的每張表映射成一個(gè)實(shí)體,每個(gè)實(shí)體都是聚合。我最初就是這樣使用的,干了一年左右才醒悟過來,雖然這樣也可以實(shí)現(xiàn)功能。
那么這有什么問題?
當(dāng)把每個(gè)表映射成獨(dú)立的聚合時(shí),我們在思考問題的時(shí)候,會(huì)把每個(gè)表作為獨(dú)立對(duì)等的概念進(jìn)行思考,從而使你的大腦分不清主次,淹沒在錯(cuò)綜復(fù)雜的表關(guān)系中。
現(xiàn)在如果系統(tǒng)有100張數(shù)據(jù)庫表,每張表以任意方式關(guān)聯(lián),映射成100個(gè)聚合。你在進(jìn)行思考時(shí),以相同方式對(duì)待這100個(gè)聚合,很快就會(huì)頭暈?zāi)垦!S薪?jīng)驗(yàn)的開發(fā)者知道通過切割模塊可以降低復(fù)雜度,但各個(gè)模塊之間錯(cuò)綜復(fù)雜的關(guān)系依然存在。
如果通過聚合的方式進(jìn)行思考,情況則大不相同。把高度相關(guān)的概念封裝到一個(gè)聚合中,并且將聚合中的對(duì)象盡量使用值對(duì)象建模,不僅可以減少表數(shù)量,在概念上也更加簡單和清晰?,F(xiàn)在假定還是100張表,每5張表映射到一個(gè)聚合中,那么具有20個(gè)聚合。我們在思考問題時(shí),整個(gè)聚合成為一個(gè)獨(dú)立思考的單元,聚合內(nèi)部的附屬對(duì)象已經(jīng)成為二等公民,你并不需要隨時(shí)想到它們。由于聚合根外部對(duì)象只能直接訪問聚合根,所以復(fù)雜的關(guān)系被封裝到聚合內(nèi)部。我們現(xiàn)在只需要考慮聚合根之間的關(guān)系,整個(gè)系統(tǒng)設(shè)計(jì)會(huì)大幅簡化,系統(tǒng)的耦合度得到控制。
另一方面,聚合對(duì)倉儲(chǔ)產(chǎn)生影響。由于倉儲(chǔ)代表的是聚合的集合,換句話說,每個(gè)聚合應(yīng)該擁有一個(gè)倉儲(chǔ)。如果每個(gè)表都映射為聚合,那么會(huì)導(dǎo)致大量的倉儲(chǔ),哪怕采用了依賴注入框架,整個(gè)系統(tǒng)的依賴復(fù)雜度還是非常高。
強(qiáng)制實(shí)施相關(guān)對(duì)象上的一致性規(guī)則
如果一組相關(guān)對(duì)象需要滿足某些業(yè)務(wù)規(guī)則,并且這幾個(gè)對(duì)象是離散的獨(dú)立對(duì)象,那么實(shí)施一致性規(guī)則就非常困難。你可能需要在每個(gè)用到的地方進(jìn)行各種判斷,從而導(dǎo)致復(fù)雜度和冗余。
我?guī)缀踉诿科恼露冀o你反復(fù)強(qiáng)調(diào)充血模型的重要性,是想激起你的注意。對(duì)于上面的問題,實(shí)際上是需要一個(gè)統(tǒng)一的驗(yàn)證點(diǎn)。能夠給你提供唯一的業(yè)務(wù)邏輯訪問點(diǎn)的位置就在實(shí)體中,所以把這一組相關(guān)對(duì)象組合為一個(gè)聚合,并在聚合上強(qiáng)制實(shí)施驗(yàn)證規(guī)則可以很好的解決問題。
對(duì)并發(fā)更新提供保護(hù)
從聚合的定義可以看出,聚合不僅是一組對(duì)象的抽象概念,而且還要做一些實(shí)際工作,即作為一個(gè)整體更新數(shù)據(jù)。數(shù)據(jù)更新很容易就會(huì)碰到并發(fā)問題,聚合有義務(wù)提供相關(guān)支持來解決并發(fā)沖突,這是通過使用樂觀離線鎖來完成的。
并發(fā)是一個(gè)復(fù)雜的問題,僅了解一點(diǎn)樂觀離線鎖并不能順利完成相關(guān)工作。有些業(yè)務(wù)場景需要使用悲觀離線鎖進(jìn)行補(bǔ)充。另外,數(shù)據(jù)庫也有自己的并發(fā)模型,同樣有樂觀和悲觀模式,那么,聚合中使用的并發(fā)模型與數(shù)據(jù)庫中的并發(fā)模型關(guān)系怎樣?
對(duì)并發(fā)問題認(rèn)識(shí)不清,輕則導(dǎo)致系統(tǒng)性能低下,重則導(dǎo)致數(shù)據(jù)錯(cuò)亂,所以我將在本文對(duì)開發(fā)中可能碰到的并發(fā)問題進(jìn)行簡單介紹。
聚合的選擇
很多程序員都喜歡追求設(shè)計(jì)的“正確性”,比如他會(huì)問,這一堆對(duì)象中哪個(gè)才是正確的聚合。只要是設(shè)計(jì)問題,由于每個(gè)人理解不同,肯定答案不一樣。更有經(jīng)驗(yàn)的開發(fā)人員能夠得到更好的設(shè)計(jì),更接近于“標(biāo)準(zhǔn)答案”,但那是建立在充分理解的基礎(chǔ)上。如果一個(gè)高手告訴你某個(gè)類應(yīng)該是聚合,你卻沒有真正理解他的用意,這種情況可能導(dǎo)致你設(shè)計(jì)出一個(gè)艱澀的系統(tǒng)。所以正確性是因人而異的,你應(yīng)該因地制宜,而不是人云亦云。
另外,高手告訴你的聚合也不見得是合適的,因?yàn)樗灰欢私饽愕臉I(yè)務(wù)實(shí)際情況,聚合不僅受邏輯上的概念影響,并且還受到并發(fā)、性能等因素制約。
下面介紹選擇聚合的一般性規(guī)律,可以幫助你進(jìn)行一些決策。
第一步,尋找具有包含或組成關(guān)系的相關(guān)對(duì)象。
某些對(duì)象有附屬的子項(xiàng),比如訂單Order和訂單項(xiàng)OrderItem,它們具有包含關(guān)系,訂單包含訂單項(xiàng)的集合,或者可以認(rèn)為一個(gè)訂單是由N個(gè)訂單項(xiàng)組成的。
找到的N組相關(guān)對(duì)象成為聚合的候選,能不能成為聚合需要經(jīng)過后面的篩選。
第二步,考慮聚合內(nèi)部的子對(duì)象集合,是否需要被聚合根外部的對(duì)象直接訪問,如果需要,將其從聚合中移出,并建模為獨(dú)立聚合。
雖然一個(gè)對(duì)象可能從概念上被另一個(gè)對(duì)象包含,但如果這種包含關(guān)系很弱,一般意味著子對(duì)象離開該聚合可能仍然有意義,外界對(duì)象希望能夠直接和它打交道。
第三步,聚合內(nèi)部導(dǎo)致并發(fā)沖突嚴(yán)重時(shí),進(jìn)行聚合拆分。
前兩步是從概念上選擇聚合,但聚合還受到其它因素影響,比如并發(fā)、性能等。
通過樂觀離線鎖可以保證,兩次提交的聚合不會(huì)發(fā)生更新丟失。如果聚合只包含它本身,出現(xiàn)沖突的可能性就很小。但由于聚合中往往包含集合,甚至是多個(gè)集合,所以各個(gè)集合之間的修改可能導(dǎo)致并發(fā)沖突很嚴(yán)重。
比如一個(gè)聚合中包含兩個(gè)實(shí)體集合,用戶A正在編輯聚合的第一組實(shí)體集合,與此同時(shí),用戶B 開始編輯同一個(gè)聚合的第二組實(shí)體集合,第一個(gè)人提交成功,第二個(gè)人將更新失敗。
如果用戶經(jīng)常需要對(duì)聚合內(nèi)的不同集合進(jìn)行單獨(dú)編輯,這就說明聚合中的概念可能具有獨(dú)立性,應(yīng)該拆分出來。當(dāng)聚合內(nèi)部集合經(jīng)常導(dǎo)致更新失敗時(shí),果斷進(jìn)行拆分是必須的。
設(shè)計(jì)一個(gè)大型聚合,除了可能經(jīng)常導(dǎo)致并發(fā)沖突外,還可能導(dǎo)致低下的性能。比如酒店包含不同的房型,每個(gè)房型包含不同的價(jià)格政策,每種價(jià)格政策的價(jià)格又不同,價(jià)格可能每隔幾天都會(huì)變化,如果把酒店作為一個(gè)大型聚合,把其它都作為集合包含進(jìn)來,創(chuàng)建一個(gè)酒店聚合的開銷可能很驚人。
當(dāng)聚合中的子對(duì)象集合的層級(jí)超過2級(jí),比如子對(duì)象又包含孫對(duì)象集合,需要考慮是否會(huì)導(dǎo)致并發(fā)和性能問題。另外一個(gè)聚合中包含子對(duì)象集合的數(shù)量也需要控制,比如一個(gè)聚合包含10個(gè)子對(duì)象集合,出現(xiàn)沖突的可能性就會(huì)很大。還有一個(gè)問題是,包含的子對(duì)象集合的元素個(gè)數(shù)也要考慮,比如一個(gè)商品,需要記錄商品的價(jià)格變動(dòng)歷史,由于價(jià)格是商品的一個(gè)屬性,所以可能會(huì)把價(jià)格變動(dòng)歷史也放到商品中。如果價(jià)格經(jīng)常變動(dòng),比如每天2次,一年就會(huì)產(chǎn)生700條記錄,可以看到,有些子對(duì)象集合剛開始數(shù)據(jù)量不大,但會(huì)持續(xù)增加,這種情況也需要進(jìn)行聚合拆分。
如果一個(gè)聚合良好表達(dá)了一個(gè)整體概念,把附屬信息都封裝起來,并且沒有導(dǎo)致并發(fā)沖突經(jīng)常發(fā)生,還性能良好,可以認(rèn)為設(shè)計(jì)相當(dāng)成功了,當(dāng)然,這很不容易。
識(shí)別聚合(邊界)的方法
聚合的核心概念:
聚合可能包含一個(gè)或多個(gè)對(duì)象,有一個(gè)根,是數(shù)據(jù)修改和持久化的最小單元。
識(shí)別方法:
- 考慮對(duì)象是否可以獨(dú)立存在
- 對(duì)象是否會(huì)直接和其他對(duì)象打交道
- 對(duì)象之間是否有不變性(Invariants),不變性指聚合內(nèi)對(duì)象之間不管如何變化總是必須滿足某個(gè)數(shù)據(jù)一致性規(guī)則
聚合設(shè)計(jì)與實(shí)現(xiàn)原則
- 將真正有不變性的對(duì)象聚合在一起
- 聚合應(yīng)盡量小
- 聚合之間的關(guān)聯(lián)用ID,不用引用
- 只保留必須的關(guān)聯(lián),單向關(guān)聯(lián)
- 聚合內(nèi)強(qiáng)一致性,聚合間考慮最終一致性
- 持久化聚合時(shí),總是完全覆蓋
并發(fā)問題及解決方案
上面介紹了聚合的基本概念,由于聚合更新與并發(fā)密切相關(guān),下面將介紹應(yīng)用程序開發(fā)中隨時(shí)可能碰到的并發(fā)問題,并討論相關(guān)解決方案。同時(shí),將應(yīng)用程序級(jí)別的并發(fā)模型與數(shù)據(jù)庫事務(wù)級(jí)別的并發(fā)模型進(jìn)行比較,這樣可以對(duì)并發(fā)解決方案有更清晰的認(rèn)識(shí)。
數(shù)據(jù)一致性問題
如果多個(gè)操作同時(shí)集中在同一條數(shù)據(jù)上,就可能造成并發(fā),導(dǎo)致數(shù)據(jù)不一致。并發(fā)產(chǎn)生的數(shù)據(jù)不一致現(xiàn)象主要有以下幾種:
1. 臟讀
當(dāng)事務(wù)A正在更新數(shù)據(jù),但還未提交,另一個(gè)事務(wù)B獲取了正在更新的數(shù)據(jù),發(fā)生臟讀。由于當(dāng)前數(shù)據(jù)處于中間狀態(tài),如果事務(wù)A更新失敗,則發(fā)生回滾,將導(dǎo)致事務(wù)B讀取的數(shù)據(jù)是錯(cuò)誤的。
臟讀有百害而無一利,應(yīng)該盡量避免。
2. 不可重復(fù)讀
事務(wù)A讀取了需要的數(shù)據(jù),另一個(gè)事務(wù)B對(duì)這些數(shù)據(jù)進(jìn)行了更改,當(dāng)事務(wù)A準(zhǔn)備用這些數(shù)據(jù)進(jìn)行計(jì)算時(shí),實(shí)際上數(shù)據(jù)已經(jīng)被改變了,這種情況稱為不可重復(fù)讀。換句話說,在同一個(gè)事務(wù)中,兩次發(fā)出相同條件的Select語句獲取的結(jié)果不同。
不可重復(fù)讀大部分時(shí)候都不是問題,在一次計(jì)算中,應(yīng)該使用老版本的數(shù)據(jù),還是必須使用最新的數(shù)據(jù)進(jìn)行計(jì)算,這是一個(gè)業(yè)務(wù)問題。
3. 幻讀
事務(wù)A使用范圍條件讀取了需要的數(shù)據(jù),另一個(gè)事務(wù)B在該范圍添加了一些數(shù)據(jù),當(dāng)事務(wù)A準(zhǔn)備用剛才獲取的數(shù)據(jù)進(jìn)行精確統(tǒng)計(jì)時(shí),但實(shí)際上還有漏網(wǎng)之魚,這種情況稱為幻讀。
絕大部分的系統(tǒng)都不需要考慮這個(gè)問題,避免幻讀只在某些高精度的場景下才需要,比如銀行對(duì)帳。
4. 丟失更新
前三種問題主要發(fā)生在數(shù)據(jù)庫事務(wù)級(jí)別,丟失更新則發(fā)生在應(yīng)用程序業(yè)務(wù)級(jí)別。丟失更新的概念很簡單,就是后一個(gè)人把前一個(gè)人的操作覆蓋了,導(dǎo)致前一個(gè)人的更新丟失。
客戶Customer,它有三個(gè)屬性:標(biāo)識(shí)Id,名稱Name,描述Description,其中一條數(shù)據(jù)為:Id=1,Name=”a”,Description=”Hello”。
現(xiàn)在張三把Id為1的客戶編輯界面打開,然后就吃飯去了。
李四對(duì)Id=1的客戶進(jìn)行編輯,修改了Name為“b”,保存成功。
張三吃完飯回來,繼續(xù)干活,他把Description改成”Haha”,保存之后,李四修改的Name=”b”又變回Name=”a”,李四的工作白干了。
丟失更新是嚴(yán)重的數(shù)據(jù)修改錯(cuò)誤,應(yīng)該堅(jiān)決避免。
5. 重復(fù)更新
重復(fù)更新是前面幾種問題的變體,由于危害很大,所以我專門把它拿出來討論。
重復(fù)更新在概念上也很簡單,本來只允許執(zhí)行一次的操作,現(xiàn)在執(zhí)行了多次。
考慮一個(gè)在線充值的場景,現(xiàn)在用戶在第三方支付平臺(tái)支付了100元,第三方支付平臺(tái)向你的系統(tǒng)發(fā)送了一個(gè)支付成功的確認(rèn),你的系統(tǒng)現(xiàn)在需要為充值編號(hào)為1對(duì)應(yīng)的客戶余額增加100。假定你開啟了一個(gè)數(shù)據(jù)庫事務(wù)來完成這個(gè)操作,正在執(zhí)行的過程中,第三方支付平臺(tái)系統(tǒng)抽筋,又向你的系統(tǒng)重復(fù)發(fā)送了一次支付確認(rèn)請(qǐng)求,如下圖所示。

上面的過程執(zhí)行完畢,你的系統(tǒng)給客戶充值200元,客戶非常滿意,以為你買一送一。
從上面可以看到,該程序員雖然不懂并發(fā),但還是有防御編程意識(shí),在事務(wù)開始的最前面,通過充值狀態(tài)判斷來防止重復(fù)充值。
通過狀態(tài)判斷的方式一般可以抵擋大部分的重復(fù)更新操作,只在運(yùn)氣極背的時(shí)候碰上并發(fā)而導(dǎo)致錯(cuò)誤,由于并發(fā)極難重現(xiàn),而且在數(shù)據(jù)量比較大時(shí)也不容易通過肉眼觀察出來,所以碰到這種問題一般都是不了了之。
如果你的系統(tǒng)需要和錢打交道,那么加強(qiáng)并發(fā)知識(shí)的學(xué)習(xí)就非常有必要,這可以讓你的公司少賠一點(diǎn)錢。
Sql Server數(shù)據(jù)庫并發(fā)模型與事務(wù)隔離級(jí)別
觀察前三種并發(fā)問題,都是讀和寫之間并發(fā)造成的。Sql Server數(shù)據(jù)庫為了解決讀寫并發(fā)沖突,首先引入了悲觀并發(fā)模型,通過鎖進(jìn)制來解決讀寫沖突。
前面說過,臟讀是必須要避免的問題。Sql Server數(shù)據(jù)庫在讀取前通過獲取共享鎖來解決這個(gè)問題,在更新數(shù)據(jù)時(shí)會(huì)獲取獨(dú)占鎖,由于共享鎖與獨(dú)占鎖無法共存,導(dǎo)致讀取數(shù)據(jù)時(shí),更新被阻塞,或在更新數(shù)據(jù)時(shí),讀取被阻塞,從而解決了臟讀。
雖然臟讀被解決了,但卻引入了讀寫阻塞的問題,在有一些數(shù)據(jù)量和并發(fā)量的系統(tǒng)上,性能可能表現(xiàn)得很低下。有一些程序員發(fā)現(xiàn)可以通過添加鎖提示W(wǎng)ith(NoLock)獲得更好的性能,這其實(shí)是走回了老路。With(NoLock)鎖提示將默認(rèn)的事務(wù)隔離級(jí)別(讀已提交)降低為讀未提交,讀未提交事務(wù)隔離級(jí)別在讀取數(shù)據(jù)前不獲取共享鎖,所以不會(huì)阻塞,但它會(huì)導(dǎo)致臟讀。更好的方法是通過添加緩存機(jī)制,以及數(shù)據(jù)讀寫分離,將頻繁的查詢從主庫卸載。
從Sql Server 2005開始支持樂觀并發(fā)模型,它通過在修改或刪除數(shù)據(jù)前將數(shù)據(jù)的老版本存儲(chǔ)到臨時(shí)數(shù)據(jù)庫TempDB的版本存儲(chǔ)區(qū)來解決讀寫并發(fā)導(dǎo)致的不一致,并解決了讀寫阻塞問題。Sql Server為樂觀并發(fā)提供了兩個(gè)新的事務(wù)隔離級(jí)別——快照隔離級(jí)別和讀已提交快照隔離級(jí)別。
快照隔離級(jí)別解決了不可重復(fù)讀和幻讀的問題,但需要犧牲更多的更新性能(因?yàn)樵谛薷幕騽h除數(shù)據(jù)前需要先備份到版本存儲(chǔ)區(qū))和TempDB存儲(chǔ)空間。由于大部分系統(tǒng)不可重復(fù)讀和幻讀都不是大問題,所以一般推薦使用讀已提交快照隔離級(jí)別,它不僅開銷更小,而且行為上與悲觀模型更兼容。
悲觀并發(fā)模型還包括另外兩個(gè)事務(wù)隔離級(jí)別,可重復(fù)讀隔離級(jí)別通過把共享鎖生命周期延長到事務(wù)結(jié)束來解決不可重復(fù)讀的問題,而可序列化隔離級(jí)別通過鍵范圍鎖或表鎖來限制查詢范圍內(nèi)的添加,解決了幻讀。這兩個(gè)事務(wù)隔離級(jí)別一般不要使用,因?yàn)閷⒐蚕礞i的持續(xù)時(shí)間延長會(huì)導(dǎo)致更大范圍的阻塞,另外延長共享鎖持續(xù)時(shí)間可能導(dǎo)致轉(zhuǎn)換死鎖。可以通過使用更新鎖或快照隔離級(jí)別來代替這兩個(gè)事務(wù)隔離級(jí)別。
在上面重復(fù)更新的例子中,進(jìn)行充值狀態(tài)判斷是防止重復(fù)更新的關(guān)鍵,該范例之所以抵擋不住并發(fā),是因?yàn)樵讷@取充值記錄時(shí),默認(rèn)獲取的是共享鎖,由于多個(gè)事務(wù)均可以獲取共享鎖,且共享鎖默認(rèn)生命周期非常短暫,所以讓另一個(gè)事務(wù)有了可趁之機(jī)。解決辦法很簡單,在獲取充值記錄時(shí)添加鎖提示W(wǎng)ith(UpdLock),這樣在充值記錄L1上獲取到更新鎖,更新鎖的特點(diǎn)是只有一個(gè)事務(wù)能夠獲取更新鎖,生命周期持續(xù)到事務(wù)結(jié)束或成功轉(zhuǎn)換為獨(dú)占鎖,這樣在事務(wù)1獲取到充值記錄L1時(shí),該記錄被更新鎖鎖定,事務(wù)2在開啟事務(wù)后,準(zhǔn)備獲取充值記錄L1時(shí)就被阻塞,直到事務(wù)1提交事務(wù)。當(dāng)事務(wù)1成功提交事務(wù)時(shí),充值狀態(tài)已改為“已充值”,所以事務(wù)2進(jìn)行判斷時(shí)就會(huì)跳出事務(wù),后續(xù)充值不會(huì)被執(zhí)行。
使用With(UpdLock)解決重復(fù)更新需要手工編寫存儲(chǔ)過程,對(duì)于面向?qū)ο箝_發(fā)很明顯不太適用。
聚合通過引入樂觀離線鎖可以解決丟失更新和重復(fù)更新的問題。
樂觀離線鎖
觀察上面丟失更新的例子,張三把操作界面一打開就吃飯去了,請(qǐng)問如何通過數(shù)據(jù)庫事務(wù)解決這個(gè)問題?
數(shù)據(jù)庫事務(wù)在開啟之后,會(huì)鎖定大量資源,如果它在某些數(shù)據(jù)上獲取了獨(dú)占鎖,在事務(wù)提交之前不會(huì)釋放,所以對(duì)事務(wù)的一個(gè)基本要求就是執(zhí)行要快。很明顯,你不能在張三把界面一打開的時(shí)候,就開一個(gè)事務(wù)等待他輸入,在保存的時(shí)候再提交事務(wù),因?yàn)樗妮斎霑r(shí)間不確定,可能導(dǎo)致一個(gè)很長時(shí)間的事務(wù)。
可以看到,數(shù)據(jù)庫的并發(fā)模型也不是萬能的,對(duì)于上面的場景需要使用應(yīng)用程序級(jí)別的并發(fā)控制。如果張三和李四不會(huì)經(jīng)常修改同一條記錄,就可以使用樂觀離線鎖來解決更新丟失的問題。
樂觀是指并發(fā)沖突機(jī)率很低,離線是指操作不是在同一個(gè)數(shù)據(jù)庫事務(wù)中完成的,比如打開編輯頁面時(shí)使用一個(gè)事務(wù)進(jìn)行讀取,中間則與數(shù)據(jù)庫事務(wù)無關(guān),在保存時(shí)會(huì)開啟另一個(gè)事務(wù)進(jìn)行更新,可以看到這個(gè)過程是跨數(shù)據(jù)庫事務(wù)的操作。樂觀鎖的優(yōu)勢是最大化系統(tǒng)并發(fā)度。
樂觀離線鎖通過為每行數(shù)據(jù)添加一個(gè)版本號(hào)來識(shí)別當(dāng)前數(shù)據(jù)的版本,在獲取數(shù)據(jù)時(shí)將版本號(hào)保存下來,更新數(shù)據(jù)時(shí)將版本號(hào)作為Where中的過濾條件,如果該記錄被更新,則版本號(hào)會(huì)發(fā)生變化,所以導(dǎo)致更新數(shù)據(jù)時(shí)影響行數(shù)為0,通過引發(fā)一個(gè)并發(fā)更新異常讓你了解數(shù)據(jù)已經(jīng)被別人更新。
樂觀離線鎖不僅可以解決丟失更新,而且同樣可以解決重復(fù)更新。當(dāng)?shù)诙€(gè)操作獲得充值聚合時(shí),如果充值狀態(tài)為“未充值”,它繼續(xù)后面的步驟。第一個(gè)操作更新完成后版本號(hào)發(fā)生改變,當(dāng)?shù)诙€(gè)操作試圖提交更新時(shí),就會(huì)檢測到并發(fā)沖突。在并發(fā)異常處理中,甚至對(duì)第二個(gè)操作進(jìn)行重試都是安全的,因?yàn)樗匦芦@取充值聚合時(shí),充值狀態(tài)已經(jīng)為“已充值”,這樣就攔截了非法操作??梢钥吹?,重復(fù)更新的問題,不管用哪種方法,都需要根據(jù)狀態(tài)判斷進(jìn)行防御編程。
Sql Server數(shù)據(jù)庫提供了Timestamp的數(shù)據(jù)類型來支持樂觀離線鎖,每當(dāng)有數(shù)據(jù)插入或更新,這個(gè)字段會(huì)自動(dòng)生成版本數(shù)據(jù)。
與此同時(shí),Entity Framework也提供了IsRowVersion來配置樂觀離線鎖。
從上面的描述可以看出,樂觀離線鎖是應(yīng)用程序級(jí)別的并發(fā)模型,與數(shù)據(jù)庫的樂觀并發(fā)模型沒有什么關(guān)系,雖然Sql Server數(shù)據(jù)庫的樂觀并發(fā)模型也有行版本的概念。這也意味著你在應(yīng)用程序級(jí)別使用的是樂觀鎖,而Sql Server數(shù)據(jù)庫中卻使用的是悲觀鎖。
使用樂觀離線鎖的前提是并發(fā)沖突機(jī)率很低,如果沖突機(jī)率很高,使用樂觀離線鎖雖然不會(huì)導(dǎo)致系統(tǒng)數(shù)據(jù)錯(cuò)亂,但會(huì)導(dǎo)致用戶十分抓狂,因?yàn)槊看伪4娉晒Χ夹枰\(yùn)氣。
對(duì)于沖突機(jī)率很高的場景,需要引入悲觀離線鎖,下面繼續(xù)介紹。
悲觀離線鎖
一個(gè)100人的客服團(tuán)隊(duì),他們的工作是對(duì)某種申請(qǐng)單進(jìn)行處理??头幚硪粋€(gè)申請(qǐng)單的時(shí)間大致5分鐘,每成功處理一個(gè)申請(qǐng)單可提成1元,每當(dāng)用戶提交一個(gè)申請(qǐng)單,所有客服都可以看見。
一個(gè)編號(hào)為1的申請(qǐng)單過來了,為了爭取拿到那一元錢提成,100名客服爭先恐后的打開業(yè)務(wù)處理界面并開始授理。一名18歲的小妹眼明手快,只花了3分零2秒就提交了,“耶,1元到手”。另一名小妹花了3分零8秒,提交的時(shí)候,系統(tǒng)彈出一個(gè)友情提示“由于你的動(dòng)作較慢,1元提成已經(jīng)被人捷足先登了”。之后,接二連三的失敗,大家只能感嘆自己運(yùn)氣不好,另外有點(diǎn)走神,希望下一次可以拿到提成。
故事說完了,該系統(tǒng)采用樂觀離線鎖設(shè)計(jì),雖然整個(gè)操作沒有導(dǎo)致數(shù)據(jù)出錯(cuò),但整個(gè)客服團(tuán)隊(duì)的辦事效率低得嚇人,近乎串行操作。
解決上面的問題,有兩個(gè)常見辦法。
一種辦法是通過一套自動(dòng)調(diào)度策略開發(fā)一個(gè)申請(qǐng)單自動(dòng)分配服務(wù),申請(qǐng)單一來,未處理前就已經(jīng)確定好由誰處理了,這樣就不會(huì)造成激烈的競爭,使用樂觀離線鎖也許就能滿足需求。
另一種辦法是使用悲觀離線鎖,開發(fā)一個(gè)鎖管理器,鎖管理器需要在數(shù)據(jù)庫中建表,記錄鎖定時(shí)間,鎖定人,業(yè)務(wù)編號(hào)等信息,在申請(qǐng)單列表界面的每行都放一個(gè)“鎖定”按鈕,當(dāng)?shù)谝粋€(gè)人點(diǎn)擊“鎖定”按鈕時(shí),向鎖管理器添加鎖記錄,一旦被鎖定,其它人不能編輯操作界面或進(jìn)行提交,界面控件應(yīng)該處于凍結(jié)狀態(tài),更嚴(yán)格的甚至不能打開編輯界面。
使用這種方案有一些問題,在點(diǎn)擊“鎖定”按鈕時(shí)可能存在并發(fā)問題,這可以通過為鎖管理器的業(yè)務(wù)編號(hào)建立唯一索引,保證不會(huì)在同一個(gè)業(yè)務(wù)編號(hào)上插入兩條鎖定記錄,當(dāng)然,這要求你的業(yè)務(wù)編號(hào)可能是Guid,不然唯一性需要添加更多屬性來識(shí)別。
既然允許鎖定,就需要有解鎖功能,解鎖可以通過簡單的刪除鎖定數(shù)據(jù)來完成。當(dāng)編輯完成時(shí),還需要對(duì)該業(yè)務(wù)編號(hào)自動(dòng)解鎖。也可能需要根據(jù)角色權(quán)限進(jìn)行解鎖,當(dāng)某個(gè)客服鎖定數(shù)據(jù)后就下班回家了,這導(dǎo)致其它人無法處理,所以更高級(jí)別的小組長可能允許對(duì)他的下級(jí)鎖定的數(shù)據(jù)進(jìn)行解鎖。
如果需要強(qiáng)大的鎖管理器,你可以仿照Sql Server悲觀鎖進(jìn)行設(shè)計(jì),加入鎖模式、鎖粒度、持續(xù)時(shí)間等要素。
可以看到,悲觀離線鎖,在實(shí)現(xiàn)和操作上并不簡單,它只應(yīng)該成為樂觀離線鎖的補(bǔ)充。
粗粒度鎖與隱含鎖
你可以把樂觀離線鎖放到每個(gè)實(shí)體中,但這樣太復(fù)雜,把樂觀離線鎖放到聚合根上,則整個(gè)聚合都可以獲得并發(fā)控制能力,這稱為粗粒度鎖。
另外,可以在聚合根和映射的層超類型上將樂觀離線鎖封裝起來,稱為隱含鎖。
總結(jié)
聚合的概念
- 聚合包裝一組高度相關(guān)的對(duì)象,作為一個(gè)數(shù)據(jù)修改的單元。
- 聚合根是聚合最外層的實(shí)體對(duì)象,它劃分了一個(gè)邊界,聚合根外部的對(duì)象,不能直接訪問聚合根內(nèi)部對(duì)象。
- 聚合體現(xiàn)了封裝的思想。每個(gè)聚合有一個(gè)根和一個(gè)邊界,邊界定義了一個(gè)聚合內(nèi)部有哪些實(shí)體或值對(duì)象,根是聚合內(nèi)的某個(gè)實(shí)體;
- 聚合聚合根開始導(dǎo)航,絕對(duì)不能繞過聚內(nèi)部的對(duì)象之間可以相互引用,但是聚合外部如果要訪問聚合內(nèi)部的對(duì)象時(shí),必須通過合根直接訪問聚合內(nèi)的對(duì)象,也就是說聚合根是外部可以保持對(duì)它的引用的唯一元素;
- 聚合內(nèi)除根以外的其他實(shí)體的唯一標(biāo)識(shí)都是本地標(biāo)識(shí),也就是只要在聚合內(nèi)部保持唯一即可,因?yàn)樗鼈兛偸菑膶儆谶@個(gè)聚合的;
- 聚合根負(fù)責(zé)與外部其他對(duì)象打交道并維護(hù)自己內(nèi)部的業(yè)務(wù)規(guī)則;
- 基于聚合的以上概念,我們可以推論出從數(shù)據(jù)庫查詢時(shí)的單元也是以聚合為一個(gè)單元,也就是說我們不能直接查詢聚合內(nèi)部的某個(gè)非根的對(duì)象;
- 聚合內(nèi)部的對(duì)象可以保持對(duì)其他聚合根的引用;
- 刪除一個(gè)聚合根時(shí)必須同時(shí)刪除該聚合內(nèi)的所有相關(guān)對(duì)象,因?yàn)樗麄兌纪瑢儆谝粋€(gè)聚合,是一個(gè)完整的概念;
聚合的作用
- 簡化系統(tǒng)設(shè)計(jì)。不要采用每個(gè)表對(duì)應(yīng)一個(gè)實(shí)體,每個(gè)實(shí)體都是聚合的設(shè)計(jì)方式。
- 強(qiáng)制實(shí)施相關(guān)對(duì)象上的一致性規(guī)則。
- 對(duì)并發(fā)更新提供保護(hù)。
聚合的選擇
- 尋找具有包含或組成關(guān)系的相關(guān)對(duì)象。
- 聚合內(nèi)部的子對(duì)象需要被聚合根外部的對(duì)象直接訪問時(shí),進(jìn)行聚合拆分。
- 聚合內(nèi)部導(dǎo)致并發(fā)沖突嚴(yán)重時(shí),進(jìn)行聚合拆分。
識(shí)別聚合:
從業(yè)務(wù)的角度深入分析哪些對(duì)象它們的關(guān)系是內(nèi)聚的,是一個(gè)整體;所謂關(guān)系是內(nèi)聚的,是指這些對(duì)象之間會(huì)保持一個(gè)固定規(guī)則,固定規(guī)則是指在數(shù)據(jù)變化時(shí)必須保持不變的一致性規(guī)則。
識(shí)別聚合根:
- 判斷是否有獨(dú)立存在的意義
- 判斷是否可以被從聚合外部直接訪問
- 判斷實(shí)體的ID是否會(huì)獨(dú)立的出現(xiàn)在外面
聚合的例子

并發(fā)問題及解決方案
- Sql Server默認(rèn)為悲觀并發(fā)模型,會(huì)讀寫阻塞,不要使用With(NoLock)鎖提示解決該問題,考慮通過添加緩存機(jī)制,讀寫分離,修改為樂觀并發(fā)模型來消除讀寫阻塞。
- 通過為聚合根添加樂觀離線鎖解決丟失更新和重復(fù)更新的問題。
- 僅在樂觀離線鎖導(dǎo)致大量更新失敗,且沒有更好的解決辦法時(shí),才引入悲觀離線鎖。
- 應(yīng)用程序級(jí)別的并發(fā)模型與數(shù)據(jù)庫事務(wù)的并發(fā)模型沒有太多關(guān)系,不要混為一談。