分布式事务最终一致性-CAP框架轻松搞定
前言
對(duì)于分布式事務(wù),常用的解決方案根據(jù)一致性的程度可以進(jìn)行如下劃分:
強(qiáng)一致性(2PC、3PC):數(shù)據(jù)庫(kù)層面的實(shí)現(xiàn),通過鎖定資源,犧牲可用性,保證數(shù)據(jù)的強(qiáng)一致性,效率相對(duì)比較低。
弱一致性(TCC):業(yè)務(wù)層面的實(shí)現(xiàn),通過預(yù)留或鎖定部分資源,最后通過確認(rèn)或取消操作完成事務(wù)的處理。比如A向B轉(zhuǎn)款500元,A賬號(hào)會(huì)凍結(jié)500元,其他操作正常,B接收轉(zhuǎn)款時(shí),也不能直接入賬,而是將500元放到預(yù)留空間,只有經(jīng)過確認(rèn)之后,A才正式扣錢,B才正式入賬;如果取消把A的500塊解凍,B也不會(huì)入賬。
最終一致性(本地消息表):不管經(jīng)過多少個(gè)服務(wù)節(jié)點(diǎn),最終數(shù)據(jù)一致就行。比如下單成功之后,需要庫(kù)存服務(wù)扣減庫(kù)存,如果庫(kù)存扣減失敗,不管是重試,還是最后人工處理,最后確保訂單和庫(kù)存數(shù)據(jù)能對(duì)上就行;為保證用戶體驗(yàn),及時(shí)通過中間狀態(tài)的形式反饋給用戶,比如常見的出票中、數(shù)據(jù)處理中等。
對(duì)于強(qiáng)一致性和弱一致性的解決方案一般針對(duì)數(shù)據(jù)一致性和時(shí)效性要求特別高的業(yè)務(wù)場(chǎng)景,通常會(huì)犧牲暫時(shí)的可用性來滿足一致性的要求;由于為保證一致性,會(huì)鎖定資源,在高并發(fā)的業(yè)務(wù)場(chǎng)景不是最佳選擇,所以很多系統(tǒng)在業(yè)務(wù)需求允許的情況下,基本上都會(huì)采用最終一致性方案。
正文
1.1 最終一致性簡(jiǎn)述
顧名思義就是保證數(shù)據(jù)最后的一致性就行了。如果中間節(jié)點(diǎn)發(fā)生失敗,系統(tǒng)為了減少代價(jià),一般不會(huì)自動(dòng)回滾,而是通過重試機(jī)制和人工參與的方式對(duì)失敗數(shù)據(jù)進(jìn)行處理,從而保證系統(tǒng)高并發(fā)場(chǎng)景下高可用的數(shù)據(jù)一致性需求。
1.2 解決方案
目前用得最多的方案是結(jié)合本地消息表進(jìn)行實(shí)現(xiàn),再加上后臺(tái)任務(wù)、消息隊(duì)列中間件就可以更好的實(shí)現(xiàn)分布式事務(wù)的處理。
解決方案流程本地消息表:就是在對(duì)應(yīng)業(yè)務(wù)數(shù)據(jù)庫(kù)中增加的一張消息表;這張表存儲(chǔ)業(yè)務(wù)產(chǎn)生的消息,通過本地事務(wù)保證業(yè)務(wù)數(shù)據(jù)和消息數(shù)據(jù)的一致性。在消息表中通過一個(gè)狀態(tài)來標(biāo)識(shí)業(yè)務(wù)是否執(zhí)行成功,如果失敗,后臺(tái)任務(wù)就進(jìn)行重試。
1.2.2 CAP框架簡(jiǎn)介
CAP 是一個(gè)EventBus(事件總線),同時(shí)也是一個(gè)在微服務(wù)或者SOA系統(tǒng)中解決分布式事務(wù)問題的一個(gè)框架,基于CAP理論思想進(jìn)行封裝的。采用模塊化設(shè)計(jì),具有高度的可擴(kuò)展性,可靠并且易于更改。
對(duì)于分布式事務(wù)的處理,CAP 框架采用的是“異步確保”這種方案,即本地消息表。官方支持的數(shù)據(jù)存儲(chǔ)方式有SQL Server、MySQL、PostgreSql、MongoDB、In-Memory(內(nèi)存),由于是開源項(xiàng)目,社區(qū)大佬也提供了其他數(shù)據(jù)存儲(chǔ)支持,如:Oracle、SQLite、SmartSql等。
在分布式系統(tǒng),各節(jié)點(diǎn)需要進(jìn)行消息傳輸,CAP框架提供以下幾種方式RabbitMQ、Kafka、Redis Streams(Redis 5.0支持)、Azure Service Bus、Amazon SQS、In-Memory Queue,使用方式都差不多。
CAP的架構(gòu)圖如下:
架構(gòu)圖上圖簡(jiǎn)要說明:
有兩個(gè)微服務(wù),服務(wù)A和服務(wù)B;
服務(wù)A中通過本地事務(wù)的方式,將事件消息和業(yè)務(wù)邏輯進(jìn)行事務(wù)保存(事件消息保存在本地消息表中),保證業(yè)務(wù)邏輯和消息的一致性和可靠性;關(guān)于消息的處理和保存CAP已經(jīng)封裝在內(nèi)部;
CAP內(nèi)部定時(shí)調(diào)度任務(wù)將消息發(fā)布到消息隊(duì)列中;
服務(wù)B訂閱到消息,將其保存到服務(wù)B的本地消息表中,CAP已經(jīng)封裝好,只需按照說明使用即可;
如果業(yè)務(wù)處理失敗,服務(wù)B中集成的CAP會(huì)根據(jù)配置的定時(shí)任務(wù)策略進(jìn)行重試,直到處理成功為止;
主要的理論就說那么多,更多詳細(xì)內(nèi)容,請(qǐng)進(jìn)下方傳送門:
官網(wǎng):https://cap.dotnetcore.xyz/
github:https://github.com/dotnetcore/CAP/blob/master/README.zh-cn.md
接下來就到擼碼時(shí)刻,CAP由于封裝比較好,所以使用起來比較簡(jiǎn)單。
1.3 擼碼實(shí)踐
以下的業(yè)務(wù)場(chǎng)景是為了案例演示,目的是體現(xiàn)CAP的實(shí)踐,所以業(yè)務(wù)邏輯都只是模擬,切勿當(dāng)真。
1.3.1 環(huán)境準(zhǔn)備
演示中要用到RabbitMQ,為了安裝方便,這里使用Docker的方式,直接通過鏡像運(yùn)行,簡(jiǎn)單,快速方便。關(guān)于Docker的實(shí)踐,后續(xù)會(huì)專門出系列文章。這里就先總結(jié)一下Docker的安裝和RabbitMQ在Docker中的運(yùn)行步驟,采用的主機(jī)環(huán)境是我之前買的阿里云服務(wù)器(CentOS 7);演示用的數(shù)據(jù)庫(kù)是SqlServer。
Docker安裝
1、移除移動(dòng)舊版本
sudo?yum?remove?docker?\docker-client?\docker-client-latest?\docker-common?\docker-latest?\docker-latest-logrotate?\docker-logrotate?\docker-engine2、安裝需要的依賴包
sudo?yum?install?-y?yum-utils3、設(shè)置鏡像倉(cāng)庫(kù)
sudo?yum-config-manager?\--add-repo?\https://download.docker.com/linux/centos/docker-ce.repo4、更新Yum軟件包索引
sudo?yum?makecache?fast?#?提高安裝速度5、開始安裝Docker
sudo?yum?install?docker-ce?docker-ce-cli?containerd.io6、啟動(dòng)Docker
sudo?systemctl?start?docker7、測(cè)試Docker
sudo?docker?run?hello-world?#?運(yùn)行Hello-world 安裝成功RabbitMQ在Docker中安裝和運(yùn)行
1、一行命令直接指定鏡像運(yùn)行,如果本地找不到鏡像,會(huì)去遠(yuǎn)程倉(cāng)儲(chǔ)里去找。
docker?run?-d?--hostname?my-rabbit?--name?cap-rabbit?-p?8888:15672?-p?5672:5672?-p?5671:5671?-p?1883:1883?rabbitmq:3-management這里先不細(xì)說命令了,后續(xù)聊Docker的時(shí)候好好說說。命令需要注意的是主機(jī)端口和容器端口的映射。
2、運(yùn)行成功后就可以訪問啦,默認(rèn)用戶名和密碼:guest/guest;
這里訪問的地址端口是8888,那是在啟動(dòng)容器的時(shí)候?qū)⒅鳈C(jī)端口8888和容器端口15672進(jìn)行了映射。
這就是選擇Dokcer安裝的原因,超級(jí)快;如果用傳統(tǒng)的方式,還得安裝語(yǔ)言環(huán)境,還得配置,最后才能安裝;Docker通過鏡像的方式直接運(yùn)行即可。
如果小伙伴新增用戶之后不能訪問,或者程序連接報(bào)錯(cuò),可以排查是否有權(quán)限訪問,如下:
注:如果小伙伴用的是云服務(wù)器,需要配置安全組,允許端口訪問;另外如果程序和RabbitMq所在的主機(jī)不是同一臺(tái)機(jī)器,主機(jī)防火墻也需要放開對(duì)應(yīng)的端口。
1.3.2 開始擼碼
項(xiàng)目準(zhǔn)備
這里模擬兩個(gè)服務(wù),一個(gè)是訂單服務(wù),一個(gè)是庫(kù)存服務(wù),兩都用到EF(Code First),如果小伙伴對(duì)EF入門還不熟,<<跟我一起學(xué).NetCore之EF Core 實(shí)戰(zhàn)入門,一看就會(huì)>>這篇文章超詳細(xì),肯定能幫到你;所以接下來就上幾張關(guān)鍵的圖就行啦。
項(xiàng)目結(jié)構(gòu):
OrderDbContext:
Startup中注冊(cè)服務(wù):
庫(kù)存服務(wù)的代碼和這個(gè)類似。
通過遷移并更新到數(shù)據(jù)庫(kù)時(shí),會(huì)生成如下數(shù)據(jù)庫(kù)和表:
集成CAP
這里因?yàn)橛玫氖荝abbitMQ、SqlServer,所以需要引入以下幾個(gè)包;如果用其他消息隊(duì)列或數(shù)據(jù)庫(kù),可以引入對(duì)應(yīng)的包。
因?yàn)?strong>訂單服務(wù)是在Respository層使用CAP,所以對(duì)應(yīng)的包就在這層引用;
庫(kù)存服務(wù)是直接在Controller那層引用,這里就不重復(fù)截圖啦。
訂單服務(wù)和庫(kù)存服務(wù)都是在各自項(xiàng)目的Startup文件中注冊(cè)CAP相關(guān)服務(wù),并配置相關(guān)信息,如下圖:
集成完畢之后,啟動(dòng)項(xiàng)目(不需要手動(dòng)自己遷移),在各自業(yè)務(wù)數(shù)據(jù)庫(kù)中就自動(dòng)生成兩個(gè)消息表,用于后續(xù)消息的存儲(chǔ),如下:
編寫業(yè)務(wù)代碼
訂單服務(wù),在訂單生成成功之后,向庫(kù)存服務(wù)發(fā)送消息,業(yè)務(wù)邏輯如下:
圖中用到的_capPublisher是通過構(gòu)造函數(shù)注入的。訂單服務(wù)其他層的代碼就不用截圖了,就是簡(jiǎn)單調(diào)用,源碼地址在文末。
庫(kù)存服務(wù)直接訂閱就行,演示案例中是直接在StockController中進(jìn)行訂閱,如下:
//?標(biāo)記為不實(shí)Action [NonAction] //?訂閱消息,參數(shù)和發(fā)布時(shí)指定的參數(shù)一致 [CapSubscribe("Order.Create.Success")] public?void?UpdateStock(OrderEntity?order) {//throw?new?Exception("扣減庫(kù)存異常了~~~");//?為了測(cè)試,庫(kù)存里面沒有數(shù)據(jù)的話,先模擬一條數(shù)據(jù)bool?bHaveData?=?_stockDbContext.Stock.Any();if(!bHaveData){StockEntity?stock?=?new?StockEntity{Id?=?Guid.NewGuid(),ProductNo?=?"Product001",StockCount?=?100,UpdateDate?=?DateTime.Now};_stockDbContext.Stock.Add(stock);_stockDbContext.SaveChanges();}//?模擬扣減庫(kù)存using?var?trans?=?_stockDbContext.Database.BeginTransaction(_capPublisher,?autoCommit:?false);try{//?根據(jù)產(chǎn)品編號(hào)找到產(chǎn)品var?product?=?_stockDbContext.Stock.Where(s?=>?s.ProductNo?==?order.ProductNo).FirstOrDefault();//?扣減庫(kù)存之后保存product.StockCount?=?product.StockCount?-?order.Count;_stockDbContext.Update(product);_stockDbContext.SaveChanges();//?可以繼續(xù)向下發(fā)布流程,比如庫(kù)存扣減成功,下一步到物流服務(wù)進(jìn)行相關(guān)處理,可以繼續(xù)發(fā)布消息//?_capPublisher.Publish();trans.Commit();Console.WriteLine(order.OrderNo);}catch?(Exception?ex){trans.Rollback();} }可以看到,訂閱很簡(jiǎn)單,直接標(biāo)上[CapSubscribe("Order.Create.Success")]這個(gè)Attribute就行了,如果消息狀態(tài)為失敗,后續(xù)CAP的定時(shí)任務(wù)會(huì)根據(jù)定時(shí)策略調(diào)用此方法。
1.3.3 運(yùn)行看效果
正常流程,下單成功,扣減庫(kù)存成功
將訂單服務(wù)(端口5000)和庫(kù)存服務(wù)(端口6000)都啟動(dòng)起來。
訂單服務(wù)中增加了OrderController,里面有一個(gè)GenerateOrder的接口,直接調(diào)用即可:
這里使用Postman工具進(jìn)行測(cè)試,如下:
庫(kù)存服務(wù)就會(huì)訂閱到信息,如下:
業(yè)務(wù)流程完成之后,訂單和庫(kù)存數(shù)據(jù)整體一致了,回過頭來看看消息表,看看里面有什么消息,如下:
異常流程模擬,下單成功,扣減庫(kù)存失敗
在扣減服務(wù)邏輯方法中手動(dòng)拋出異常,代碼如下:
然后啟動(dòng)項(xiàng)目重新測(cè)試,再下一個(gè)訂單試試;操作后,先來看看消息表,如下:
注:CAP在默認(rèn)情況下,發(fā)送和消費(fèi)消息的過程中失敗會(huì)立即重試 3 次,在 3 次以后將進(jìn)入重試輪詢;重試將在發(fā)送和消費(fèi)消息失敗的 4分鐘后 開始,這是為了避免設(shè)置消息狀態(tài)延遲導(dǎo)致可能出現(xiàn)的問題;后續(xù)就會(huì)每隔1分鐘之后重試一次,默認(rèn)的最高重試次數(shù)為50次,當(dāng)達(dá)到50次時(shí),就不會(huì)重試了。
現(xiàn)在知道問題了,優(yōu)化代碼,重新啟動(dòng),即把拋異常的代碼注釋掉,看看會(huì)不會(huì)自動(dòng)處理,如下:
如上圖,稍等一會(huì),消息就自動(dòng)處理了,業(yè)務(wù)數(shù)據(jù)符合預(yù)期,保證一致性。這個(gè)是CAP內(nèi)部定時(shí)讀取消息表,根據(jù)狀態(tài)不斷重試業(yè)務(wù)邏輯,直到成功為止。CAP的全自動(dòng)是不是感覺比較便捷,寫最少的代碼,解決了最難搞的分布式事務(wù)。
修改默認(rèn)的配置
在實(shí)際業(yè)務(wù)場(chǎng)景中,默認(rèn)配置可能不太實(shí)用,可以在注冊(cè)服務(wù)時(shí)進(jìn)行默認(rèn)配置更改,如下:
配置修改之后的測(cè)試這里就不截圖了,留給小伙伴們動(dòng)手試試吧。
案例代碼地址:https://gitee.com/CodeZoe/microservies-demo/tree/main/CapDemo
總結(jié)
關(guān)于分布式事務(wù)的實(shí)操,把最常用的最終一致性方案簡(jiǎn)單分享了一下,小伙伴可以根據(jù)自己的業(yè)務(wù)場(chǎng)景,趕緊動(dòng)手試試吧;
其他方案會(huì)在后續(xù)的文章中加上,主要還是以實(shí)用為主,已經(jīng)不咋用的就沒必要再說啦。
文章中提及到Docker和RabbitMQ,我已經(jīng)在著手準(zhǔn)備這塊的文章了,關(guān)注“Code綜藝圈”,和我一起學(xué)習(xí)吧;
總結(jié)
以上是生活随笔為你收集整理的分布式事务最终一致性-CAP框架轻松搞定的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TIOBE 发布 8 月编程语言榜单:C
- 下一篇: 为什么我的 Func 如此之慢?