- 工信部備案號 滇ICP備05000110號-1
- 滇公安備案 滇53010302000111
- 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證 B1.B2-20181647、滇B1.B2-20190004
- 云南互聯(lián)網(wǎng)協(xié)會(huì )理事單位
- 安全聯(lián)盟認證網(wǎng)站身份V標記
- 域名注冊服務(wù)機構許可:滇D3-20230001
- 代理域名注冊服務(wù)機構:新網(wǎng)數碼
隨著(zhù)業(yè)務(wù)發(fā)展,很多系統需要經(jīng)歷服務(wù)拆分的過(guò)程。微服務(wù)化過(guò)程踩坑也是很正常的事。如果在服務(wù)拆分之前做好充分準備,能幫我們少走很多彎路。本文主要從服務(wù)依賴(lài),接口版本,隔離,數據一致等方面說(shuō)說(shuō)微服務(wù)化過(guò)程應該注意的點(diǎn)。
一、循環(huán)依賴(lài)問(wèn)題
微服務(wù)化之后服務(wù)之間會(huì )存在各種依賴(lài)關(guān)系,不過(guò)依賴(lài)需要遵循一定的規則,不能太隨意。否則,就會(huì )出現循環(huán)依賴(lài)的問(wèn)題,而且會(huì )讓調用關(guān)系變得錯綜復雜難于維護。下面是服務(wù)依賴(lài)的幾條規則:
1,上層服務(wù)可以調用下層服務(wù)。
2,同級服務(wù)之間不能產(chǎn)生依賴(lài)關(guān)系,及不能產(chǎn)生調用關(guān)系。
3,下層服務(wù)不能調用上層服務(wù)。
4,服務(wù)之間的調用關(guān)系只能是單向的。
例如,在電商系統里包括支付服務(wù)(Pay),庫存服務(wù)(Inventory),訂單服務(wù)(Order)。支付服務(wù)和庫存服務(wù)屬于基礎服務(wù),訂單服務(wù)屬于上層服務(wù)。支付服務(wù)和庫存服務(wù)是同級的服務(wù),他們之間不能存在調用關(guān)系。訂單服務(wù)屬于上層服務(wù),訂單服務(wù)可以調用支付服務(wù)和庫存服務(wù),但是支付服務(wù)和庫存服務(wù)不能調用上層的訂單服務(wù)。
假設我們不管這些規則,讓Order和Pay可以互相調用。這樣就會(huì )產(chǎn)生循環(huán)依賴(lài),Order調用Pay,Pay也調用Order,這樣彼此都會(huì )依賴(lài)對方。
循環(huán)依賴(lài)導致哪些問(wèn)題?
1,無(wú)限遞歸調用
假如,Order調用Pay的A方法,Pay調用Order的B方法。然后,A方法里又調用了Order的B方法,B方法里又調用了Pay的A方法。這樣就會(huì )產(chǎn)生無(wú)限的遞歸調用,后果自然不言而喻了。
2,部署依賴(lài)問(wèn)題
假設Order,Pay,Inventory彼此之間都可以通過(guò)API互相調用。當API接口發(fā)生變更時(shí),為了讓其他服務(wù)能夠正常調用,API需要重新編譯。如果Order和Pay的API都有變化,上線(xiàn)發(fā)布時(shí)就需要特別小心。為了保證發(fā)布成功,就需要根據服務(wù)間API的依賴(lài)關(guān)系,詳細考慮先打包部署哪個(gè)服務(wù),后打包部署哪個(gè)服務(wù),才不至于發(fā)布失敗。如果有更多的服務(wù)呢?比如10幾個(gè),梳理依賴(lài)關(guān)系都會(huì )把人搞瘋的。
3,另外,循環(huán)依賴(lài)會(huì )讓服務(wù)間的調用關(guān)系變得錯綜復雜,系統難于維護。
二、接口版本兼容
一些初中級程序員往往會(huì )忽略接口變更的問(wèn)題,經(jīng)常會(huì )因為接口變更導致線(xiàn)上問(wèn)題。比如某個(gè)小型電商平臺的訂單服務(wù)調用支付服務(wù)的某個(gè)接口,產(chǎn)品突然提了一個(gè)需求,這個(gè)需求需要在這個(gè)支付接口上加一個(gè)參數。開(kāi)發(fā)這個(gè)需求的是個(gè)新手,他直接在原來(lái)的接口方法上實(shí)現了需求并加上了參數,聯(lián)調測試通過(guò)后就發(fā)布上線(xiàn)了。結果剛上線(xiàn)訂單服務(wù)就開(kāi)始報錯,因為方法變了,加了參數,訂單服務(wù)找不到老的方法了。所以就會(huì )一直報錯,直到訂單服務(wù)上線(xiàn)為止。
所以我們一定要注意接口版本問(wèn)題。我們可以新加一個(gè)方法去重載老的方法,在新方法里實(shí)現新的功能,新方法的定義除了多一個(gè)參數外,其他的和老方法一樣。也就是給老方法加了一個(gè)新版本。
這樣在支付服務(wù)上線(xiàn)后,訂單服務(wù)上線(xiàn)之前就不會(huì )報錯了,因為老方法仍然可用。訂單服務(wù)上線(xiàn)后就直接切到了新版本的方法。
如果我們服務(wù)框架選用的是Dubbo,當一個(gè)接口的實(shí)現,出現不兼容升級時(shí),可以用Dubbo的版本號過(guò)渡,版本號不同的服務(wù)相互間不引用。
可以按照以下的步驟進(jìn)行版本遷移:
1. 在低壓力時(shí)間段,先升級一半提供者為新版本
2. 再將所有消費者升級為新版本
3. 然后將剩下的一半提供者升級為新版本
老版本服務(wù)提供者配置:
新版本服務(wù)提供者配置:
老版本服務(wù)消費者配置:
新版本服務(wù)消費者配置:
三、關(guān)于隔離的考慮
1.數據隔離:
實(shí)際上,服務(wù)化的其中一個(gè)基本原則就是數據隔離,不同服務(wù)應該有自己的專(zhuān)屬數據庫,而不應該共用相同的數據庫,數據訪(fǎng)問(wèn)可以通過(guò)服務(wù)接口或者消息隊列的方式。
很多公司微服務(wù)化后,只做了代碼工程的拆分,不同服務(wù)對應的數據仍然存放在同一個(gè)數據庫中。這樣做至少存在四個(gè)問(wèn)題:
(1)數據安全問(wèn)題。別人的服務(wù)不但可以訪(fǎng)問(wèn)你的數據,而且還能修改和刪除你的數據。
(2)導致數據庫連接耗盡。一旦某個(gè)服務(wù)的開(kāi)發(fā)者寫(xiě)了一個(gè)慢SQL,并且這個(gè)服務(wù)也沒(méi)有合理限制連接數??赡軙?huì )消耗掉所有的數據庫連接,進(jìn)而造成訪(fǎng)問(wèn)相同數據庫的其他服務(wù)拿不到數據庫連接,無(wú)法訪(fǎng)問(wèn)數據庫。
(3)表關(guān)聯(lián)查詢(xún)。無(wú)法避免其他服務(wù)的開(kāi)發(fā)者,為了快速上線(xiàn)某些需求。直接查詢(xún)其他服務(wù)的表,或者跨服務(wù)做表關(guān)聯(lián)查詢(xún)。這樣會(huì )造成服務(wù)間的耦合越來(lái)越嚴重。
(4)表結構變化的影響。如果某個(gè)服務(wù)直接依賴(lài)于其他服務(wù)的數據,一旦表結構發(fā)生任何變化,比如修改表名或者字段。很可能會(huì )產(chǎn)生災難性后果。
2.部署隔離:
我們經(jīng)常會(huì )遇到秒殺業(yè)務(wù)和日常業(yè)務(wù)依賴(lài)同一個(gè)服務(wù),以及C端服務(wù)和內部運營(yíng)系統依賴(lài)同一個(gè)服務(wù)的情況,比如說(shuō)都依賴(lài)支付服務(wù)。而秒殺系統的瞬間訪(fǎng)問(wèn)量很高,可能會(huì )對服務(wù)帶來(lái)巨大的壓力,甚至壓垮服務(wù)。內部運營(yíng)系統也經(jīng)常有批量數據導出的操作,同樣會(huì )給服務(wù)帶來(lái)一定的壓力。這些都是不穩定因素。所以我們可以將這些共同依賴(lài)的服務(wù)分組部署,不同的分組服務(wù)于不同的業(yè)務(wù),避免相互干擾。
3.業(yè)務(wù)隔離:
以秒殺為例。從業(yè)務(wù)上把秒殺和日常的售賣(mài)區分開(kāi)來(lái),把秒殺做為營(yíng)銷(xiāo)活動(dòng),要參與秒殺的商品需要提前報名參加活動(dòng),這樣我們就能提前知道哪些商家哪些商品要參與秒殺,可以根據提報的商品提前生成商品詳情靜態(tài)頁(yè)面并上傳到CDN預熱,提報的商品庫存也需要提前預熱,可以將商品庫存在活動(dòng)開(kāi)始前預熱到Redis,避免秒殺開(kāi)始后大量訪(fǎng)問(wèn)穿透到數據庫。
四、數據一致性問(wèn)題
做了微服務(wù)拆分后,還可能會(huì )出現數據不一致的問(wèn)題。比如支付服務(wù)中,支付狀態(tài)發(fā)生變更后要通知訂單服務(wù)修改對應訂單的狀態(tài)。如果支付服務(wù)沒(méi)有正常通知到訂單服務(wù),或者訂單服務(wù)接到通知后沒(méi)能正常處理通知,就會(huì )導致支付服務(wù)的支付狀態(tài)和訂單服務(wù)的支付狀態(tài)不一致,也就是數據會(huì )不一致。
1.那么如何避免數據不一致的問(wèn)題產(chǎn)生呢?
我們通常所說(shuō)的服務(wù)間數據一致性,主要包括數據強一致性和最終一致性。對于強一致性,使用的業(yè)務(wù)場(chǎng)景很少,而且會(huì )有明顯的性能問(wèn)題。所以這里我們主要討論最終一致性。
一般我們可以采用如下幾種方式來(lái)保證服務(wù)間數據的最終一致:
2.定時(shí)任務(wù)重試,同步調用接口
這種方式,采用定時(shí)任務(wù)去掃表,每次定時(shí)任務(wù)掃描所有未成功的記錄,并發(fā)起重試。注意,要保證重試操作的冪等性。
這種方式的優(yōu)點(diǎn)是:實(shí)現簡(jiǎn)單。缺點(diǎn)是:需要啟動(dòng)專(zhuān)門(mén)的定時(shí)任務(wù),定時(shí)任務(wù)存在一定的時(shí)間間隔,實(shí)時(shí)性會(huì )比較差。而且同步接口調用的方式,耦合較重,有時(shí)無(wú)法避免循環(huán)依賴(lài)的問(wèn)題。
比如,Order服務(wù)可以調用Pay,Pay做為基礎服務(wù)不應該調用Order。當Pay的某筆交易狀態(tài)發(fā)生變更后,需要通知Order。如果采用定時(shí)任務(wù)的方式就需要Order提供一個(gè)接口,定時(shí)任務(wù)掃描過(guò)程中同步調用這個(gè)接口去更新Order的訂單狀態(tài)。這樣又違反了單向依賴(lài)的原則,形成了循環(huán)依賴(lài)。
異步消息隊列,發(fā)送事務(wù)型消息
如上圖,以電商下單流程為例。下單流程最后一步,通知WMS撿貨出庫,是異步消息走消息隊列。
按上面代碼,大家不難發(fā)現問(wèn)題!如果發(fā)送撿貨出庫消息失敗,數據就會(huì )不一致!有人說(shuō)我可以在代碼上加上重試邏輯和回退邏輯,發(fā)消息失敗就重發(fā),多次重試失敗所有操作都回退。這樣一來(lái)邏輯就會(huì )特別復雜,回退失敗要考慮,而且還有可能消息已經(jīng)發(fā)送成功了,但是由于網(wǎng)絡(luò )等問(wèn)題發(fā)送方?jīng)]得到MQ的響應。還有可能出現發(fā)送方宕機的情況。這些問(wèn)題都要考慮進(jìn)來(lái)!
幸好,有些消息隊列幫我們解決了這些問(wèn)題。比如阿里開(kāi)源的RocketMQ(目前已經(jīng)是Apache開(kāi)源項目),4.3.0版本開(kāi)始支持事務(wù)型消息(實(shí)際上早在貢獻給Apache之前曾經(jīng)支持過(guò)事務(wù)消息,后來(lái)被閹割了,4.3.0版本重新開(kāi)始支持事務(wù)型消息)。
先看看RocketMQ發(fā)送事務(wù)型消息的流程:
(1)發(fā)送半消息(所有事務(wù)型消息都要經(jīng)歷確認過(guò)程,從而確定最終提交或回滾(拋棄消息),未被確認的消息稱(chēng)為“半消息”或者“預備消息”,“待確認消息”)
(2)半消息發(fā)送成功并響應給發(fā)送方
(3)執行本地事務(wù),根據本地事務(wù)執行結果,發(fā)送提交或回滾的確認消息
(4)如果確認消息丟失(網(wǎng)絡(luò )問(wèn)題或者生產(chǎn)者故障等問(wèn)題),MQ向發(fā)送方回查執行結果
(5)根據上一步驟回查結果,確定提交或者回滾(拋棄消息)
看完事務(wù)型消息發(fā)送流程,有些讀者可能沒(méi)有完全理解,不要緊,我們來(lái)分析一下!
問(wèn)題1:假如發(fā)送方發(fā)送半消息失敗怎么辦?
半消息(待確認消息)是消息發(fā)送方發(fā)送的,如果失敗,發(fā)送方自己是知道的并可以做相應處理。
問(wèn)題2:假如發(fā)送方執行完本地事務(wù)后,發(fā)送確認消息通知MQ提交或回滾消息時(shí)失敗了(網(wǎng)絡(luò )問(wèn)題,發(fā)送方重啟等情況),怎么辦?
沒(méi)關(guān)系,當MQ發(fā)現一個(gè)消息長(cháng)時(shí)間處于半消息(待確認消息)的狀態(tài),MQ會(huì )以定時(shí)任務(wù)的方式主動(dòng)回查發(fā)送方并獲取發(fā)送方執行結果。這樣即便出現網(wǎng)絡(luò )問(wèn)題或者發(fā)送方本身的問(wèn)題(重啟,宕機等),MQ通過(guò)定時(shí)任務(wù)主動(dòng)回查發(fā)送方基本都能確認消息最終要提交還是回滾(拋棄)。當然出于性能和半消息堆積方面的考慮,MQ本身也會(huì )有回查次數的限制。
問(wèn)題3:如何保證消費一定成功呢?
RocketMQ本身有ack機制,來(lái)保證消息能夠被正常消費。如果消費失?。ㄏ⒂嗛喎匠鲥e,宕機等原因),RocketMQ會(huì )把消息重發(fā)回Broker,在某個(gè)延遲時(shí)間點(diǎn)后(默認10秒后)重新投遞消息。
結合上面幾個(gè)同步調用hmily完整代碼如下:
如果執行到TransactionListenerImpl.executeLocalTransaction方法,說(shuō)明半消息已經(jīng)發(fā)送成功了,也說(shuō)明OrderService.makePayment方法的四個(gè)步驟都執行成功了,此時(shí)tcc也到了confirm階段,所以在TransactionListenerImpl.executeLocalTransaction方法里可以直接返回LocalTransactionState.COMMIT_MESSAGE 讓 MQ提交這條消息,同時(shí)將該訂單信息和對應的消息狀態(tài)保存在共享map里,以備確認消息發(fā)送失敗時(shí)MQ回查消息狀態(tài)使用。
3.采用TCC,SAGA,Seata等框架
以上就是微服務(wù)化后需要注意的幾點(diǎn)。
、
售前咨詢(xún)
售后咨詢(xún)
備案咨詢(xún)
二維碼
TOP