新聞中心
前言

現(xiàn)如今不管是傳統(tǒng)企業(yè)還是互聯(lián)網(wǎng)公司都在談?wù)撐⒎?wù),微服務(wù)架構(gòu)已經(jīng)成為了互聯(lián)網(wǎng)的熱門話題,同時(shí),微服務(wù)的開發(fā)框架比如Dubbo、SpringCloud等也是在高頻迭代中,以滿足層出不窮的技術(shù)需求。當(dāng)企業(yè)遇到系統(tǒng)性能瓶頸、項(xiàng)目進(jìn)度推進(jìn)乏力、系統(tǒng)運(yùn)維瓶頸的時(shí)候,都會(huì)試圖把微服務(wù)當(dāng)著一根救命稻草,認(rèn)為只要實(shí)施微服務(wù)架構(gòu)了,所有的問題都迎刃而解。然而,在實(shí)施微服務(wù)過程中出現(xiàn)的各種各樣問題如何優(yōu)雅的去解決呢?本文接下來將介紹如何以“硬核”的方式去解決微服務(wù)改造過程中遇到的難點(diǎn)問題。
一、服務(wù)拆分粒度問題
服務(wù)到底怎么拆分合適
在微服務(wù)架構(gòu)中“服務(wù)”的定義是指分布式架構(gòu)下的基礎(chǔ)單元,包含了一組特定的功能。服務(wù)拆分是單體應(yīng)用轉(zhuǎn)化成微服務(wù)架構(gòu)的第一步,服務(wù)拆分是否合理直接影響到微服務(wù)架構(gòu)的復(fù)雜性、穩(wěn)定性以及可擴(kuò)展性。服務(wù)拆分過小,會(huì)導(dǎo)致不必要的分布式事務(wù)產(chǎn)生,而且整個(gè)調(diào)用鏈過程也會(huì)變長,反之,如果服務(wù)拆分過大,會(huì)逐步演變?yōu)閱误w應(yīng)用,不能發(fā)揮微服務(wù)的優(yōu)勢。判斷一個(gè)服務(wù)拆分的好壞,就看微服務(wù)拆分完成后是否具備服務(wù)的自治原則,如果把復(fù)雜單體應(yīng)用改造成一個(gè)一個(gè)松耦合式微服務(wù),那么按照業(yè)務(wù)功能分解模式進(jìn)行分解是最簡單的,只需把業(yè)務(wù)功能相似的模塊聚集在一起。比如:
- 用戶管理:管理用戶相關(guān)的信息,例如注冊(cè)、修改、注銷或查詢、統(tǒng)計(jì)等。
- 商品管理:管理商品的相關(guān)信息。
業(yè)務(wù)功能分解模式另外的優(yōu)勢在于在初級(jí)階段服務(wù)拆分不會(huì)太小,等到業(yè)務(wù)發(fā)展起來后可以再根據(jù)子域方式來拆分,把獨(dú)立的服務(wù)再拆分成更小的服務(wù),最后到接口級(jí)別服務(wù)。
以用戶管理舉例,在初始階段的做服務(wù)拆分的時(shí)候,把用戶管理拆分為用戶服務(wù),且具備了用戶的增刪改查功能,在互聯(lián)網(wǎng)中流量獲客是最貴的,運(yùn)營團(tuán)隊(duì)通過互聯(lián)網(wǎng)投放廣告獲客,用戶在廣告頁上填寫手機(jī)號(hào)碼執(zhí)行注冊(cè)過程,如果此時(shí)注冊(cè)失敗或者注冊(cè)過程響應(yīng)時(shí)間過長,那么這個(gè)客戶就可能流失了,但是廣告的點(diǎn)擊費(fèi)用產(chǎn)生了,無形中形成了資源的浪費(fèi)。當(dāng)用戶規(guī)模上升之后需要對(duì)增刪改查功能做優(yōu)先級(jí)劃分,所以此時(shí)需要按方法維度來拆分服務(wù),把用戶服務(wù)拆分為用戶注冊(cè)服務(wù)(只有注冊(cè)功能),用戶基礎(chǔ)服務(wù)(修改、查詢用戶信息)。
哪些功能需要被拆分成服務(wù)
無論是單體應(yīng)用重構(gòu)為微服務(wù)架構(gòu),還是在微服務(wù)架構(gòu)體系下有新增需求,都會(huì)面臨這些功能或者新增需求是否需要被拆分為服務(wù)。雖然沒有相關(guān)規(guī)定,但是可以遵循服務(wù)拆分的方法論:當(dāng)一塊業(yè)務(wù)不依賴或極少依賴其它服務(wù),有獨(dú)立的業(yè)務(wù)語義,為超過 2 個(gè)或以上的其他服務(wù)或客戶端提供數(shù)據(jù),應(yīng)該被拆分成一個(gè)獨(dú)立的服務(wù)模,而且拆分的服務(wù)要具備高內(nèi)聚低耦合。所謂的高內(nèi)聚是指一個(gè)組件中各個(gè)元素互相依賴的程度,是衡量某個(gè)模塊或者類中各個(gè)代碼片段之間關(guān)聯(lián)強(qiáng)度的標(biāo)準(zhǔn),比如用戶服務(wù),只會(huì)提供用戶相關(guān)的增刪改查信息,假如還關(guān)聯(lián)了用戶訂單相關(guān)的信息,那就說明這個(gè)功能不是高內(nèi)聚的功能,拆分的不好。
低耦合是指系統(tǒng)中每個(gè)組件很少知道或者不知道其他獨(dú)立組件的定義,其中的組件可以被其他提供相同功能的組件替代。
二、緩存到底怎么用才更有效
緩存需要在哪層增加
微服務(wù)架構(gòu)下,原本單體應(yīng)用被劃分為聚合層和原子服務(wù)層,每一層所負(fù)責(zé)的功能各不相同。
1、聚合層:收到終端請(qǐng)求后,聚合多個(gè)原子服務(wù)數(shù)據(jù),按接口要求把聚合后的數(shù)據(jù)返回給終端,需要注意點(diǎn)是聚合層不會(huì)和數(shù)據(jù)庫交互;
2、原子服層:數(shù)據(jù)庫交互層,實(shí)現(xiàn)數(shù)據(jù)的增刪改查,結(jié)合緩存和工具保障服務(wù)的高響應(yīng);要遵循單表原則,禁止2張以上的表做join查詢,如有分庫分表,那么對(duì)外要屏蔽具體規(guī)則,提供服務(wù)接口供外部調(diào)用。
如果使用到緩存,那么到底在聚合層加還是原子層加還是其他呢?應(yīng)該遵循“誰構(gòu)建,誰運(yùn)維”這一理念,是否使用緩存應(yīng)該由對(duì)應(yīng)的開發(fā)人員自行維護(hù),也就是說聚合層和原子層都需要增加緩存。一般來說聚合層和原子層由不同的團(tuán)隊(duì)開發(fā),聚合層和業(yè)務(wù)端比較貼近,需要了解業(yè)務(wù)流程更好的服務(wù)業(yè)務(wù),和App端交互非常多,重點(diǎn)是合理設(shè)計(jì)的前后端接口,減少App和后端交互次數(shù)。原子服務(wù)則是關(guān)注性能,屏蔽數(shù)據(jù)庫操作,屏蔽分庫分表等操作。在聚合層推薦使用多級(jí)緩存,即本地緩存+分布式緩存,本地緩存不做緩存數(shù)據(jù)的變更,使用TTL自動(dòng)過期時(shí)間來自動(dòng)更新緩存內(nèi)的數(shù)據(jù)。
緩存使用過程中不可避免的問題
在使用緩存的時(shí)候不可避免的會(huì)遇到緩存穿透、緩存擊穿、緩存雪崩等場景,針對(duì)每種場景的時(shí)候需要使用不同的應(yīng)對(duì)策略,從而保障系統(tǒng)的高可用性。
1、緩存穿透:是指查詢一個(gè)一定不存在緩存key,由于緩存是未命中的時(shí)候需要從數(shù)據(jù)庫查詢,正常情況下查不到數(shù)據(jù)則不寫入緩存,就會(huì)導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到數(shù)據(jù)庫去查詢,造成緩存穿透,有2個(gè)方案可以解決緩存穿透:
1) 可以使用布隆過濾器方案,系統(tǒng)啟動(dòng)的時(shí)候?qū)⑺汛嬖诘臄?shù)據(jù)哈希到一個(gè)足夠大的bitmap中,當(dāng)一個(gè)一定不存在的數(shù)據(jù)請(qǐng)求的時(shí)候,會(huì)被這個(gè)bitmap攔截掉,從而避免了對(duì)底層數(shù)據(jù)庫的查詢壓力。
- @Component
- public class BloomFilterCache {
- public static BloomFilter
bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 10000); - @PostConstruct
- public void init(){
- List
list=Lists.newArrayList(); //初始化加載所有的需要被緩存的數(shù)據(jù)ID - list.forEach(id ->bloomFilter.put(id));
- }
- public boolean addKey(Integer key){
- return bloomFilter.put(key);
- }
- public boolean isCached(Integer key){
- return bloomFilter.mightContain(key);
- }
- }
這里的BloomFilter選用guava提供的第三方包,服務(wù)啟動(dòng)的時(shí)候,init方法會(huì)加載所有可以被緩存的數(shù)據(jù),把id都放入boolmFilter中,當(dāng)有新增數(shù)據(jù)的時(shí)候,執(zhí)行addKey把新增的數(shù)據(jù)放入BoolmFilter過濾器中。
當(dāng)在需要使用緩存的地方先調(diào)用isCached方法,如果返回true表示正常請(qǐng)求,否則拒絕。
2) 返回空值:如果一個(gè)查詢請(qǐng)求查詢數(shù)據(jù)庫后返回的數(shù)據(jù)為空(不管是數(shù)據(jù)不存在,還是系統(tǒng)故障),仍然把這個(gè)空結(jié)果進(jìn)行緩存,但它的過期時(shí)間會(huì)很短,比如1分鐘,但是這種方法解決不夠徹底。
2、緩存擊穿:緩存key在某個(gè)時(shí)間點(diǎn)過期的時(shí)候,剛好在這個(gè)時(shí)間點(diǎn)對(duì)這個(gè)Key有大量的并發(fā)請(qǐng)求過來,請(qǐng)求命中緩存失敗后會(huì)通過DB加載數(shù)據(jù)并回寫到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端DB壓垮,解決方案也很簡單通過加鎖的方式讀取數(shù)據(jù),同時(shí)寫入緩存。
- Object [] objects={0,1,2,3,4,5,6,7,8,9};
- public List
getData(Integer id) throws InterruptedException { - List
result = new ArrayList (); - result = getDataFromCache(id);
- if (result.isEmpty()) {
- int objLength= objects.length;
- synchronized (objects[id% objects.length]) {
- result = getDataFromDB(id);
- setDataToCache(result);
- }
- }
- return result;
- }
這里加鎖的方法使用的是Object數(shù)組,是希望因不同的id不會(huì)因?yàn)閺臄?shù)據(jù)庫加載數(shù)據(jù)被阻塞,例如id=1、id=2、id=3的key同時(shí)在緩存中消失,微服務(wù)路由策略剛好都把這些請(qǐng)求都路由到同一臺(tái)機(jī)器上,假設(shè)查詢DB需要50毫秒,如果僅使用synchronized(object){……}則id=3的請(qǐng)求會(huì)被阻塞,需要等等150毫秒才能返回結(jié)果,但是使用上述方法則只需要50毫秒出結(jié)果。其中objects數(shù)據(jù)的大小可以根據(jù)DB能承載的并發(fā)量以及原子服務(wù)數(shù)量綜合考慮。
3、緩存雪崩:是指在設(shè)置緩存時(shí)使用了相同的過期時(shí)間,導(dǎo)致緩存在某一時(shí)刻同時(shí)失效,所有的查詢都請(qǐng)求到數(shù)據(jù)庫上,導(dǎo)致應(yīng)用系統(tǒng)產(chǎn)生各種故障,這樣情況稱之為緩存雪崩,可以通過限流的方式來限制請(qǐng)求數(shù)據(jù)庫的次數(shù)。
三、串行化并行解決效率問題
一個(gè)應(yīng)用功能被拆分成多個(gè)服務(wù)之后,原本調(diào)用一個(gè)接口就能完成的功能如今變成需要調(diào)用多個(gè)服務(wù),如果按順序逐個(gè)調(diào)用的話,使用微服務(wù)改造后的接口會(huì)比原始接口響應(yīng)時(shí)間更長,因此要把原本串行調(diào)用的服務(wù)修改為并行調(diào)用,同時(shí)原本通過SQL的join多表聯(lián)合查詢操作變成單表操作,然后在聚合層的內(nèi)存中做拼接。
例如接口A,需要調(diào)用S1(耗時(shí)200毫秒),S2(耗時(shí)180毫秒),S3(耗時(shí)320毫秒)這3個(gè)接口,使用串行調(diào)用方式,那么接口A累計(jì)耗時(shí)=SUM(S1+S2+S3)=700毫秒。為了讓響應(yīng)時(shí)間更短,就需要把這些串行調(diào)用的方式更改為并行調(diào)用的方式,并行調(diào)用方式調(diào)用接口A累計(jì)耗時(shí)為MAX(S1,S2,S3)=320毫秒??梢允褂胘dk8提供的CompletableFuture方法,偽代碼如下:
- CompletableFuture
futureS1 = CompletableFuture.supplyAsync(() -> { - S1接口 },executor);
- CompletableFuture
futureS2 = CompletableFuture.supplyAsync(() -> { - S2接口 },executor);
- CompletableFuture
futureS3 = CompletableFuture.supplyAsync(() -> { - S3接口 },executor);
- CompletableFuture.allOf(futureS1, futureS2, futureS3).get(500, TimeUnit.MILLISECONDS);
此時(shí)就把原本串聯(lián)調(diào)用的服務(wù)變成并行調(diào)用,節(jié)約了接口請(qǐng)求時(shí)間,但卻引發(fā)一個(gè)新的問題,內(nèi)部接口調(diào)用換成網(wǎng)絡(luò)RPC調(diào)用,會(huì)導(dǎo)致服務(wù)調(diào)用的不確定性,引起接口不穩(wěn)定。
四、服務(wù)的熔斷降級(jí)處理
把內(nèi)部接口調(diào)用替換為RPC調(diào)用,在調(diào)用過程中可能會(huì)出現(xiàn)網(wǎng)絡(luò)抖動(dòng)、網(wǎng)絡(luò)異常,當(dāng)服務(wù)提供方(Provide)變得不可用或者響應(yīng)慢時(shí),也會(huì)影響到服務(wù)調(diào)用方的服務(wù)性能,甚至可能會(huì)使得服務(wù)調(diào)用方占滿整個(gè)線程池,導(dǎo)致這個(gè)應(yīng)用上其它的服務(wù)也受影響,從而引發(fā)更嚴(yán)重的雪崩效應(yīng)。因此需要梳理所有服務(wù)提供者并把服務(wù)分級(jí),同時(shí)引入了Hystrix或則Sentinel做服務(wù)熔斷和降級(jí)處理,目的如下:
降級(jí)目的:業(yè)務(wù)高峰期的生活,去掉非核心鏈路,保障主流程正常運(yùn)行;
熔斷目的:防止應(yīng)用程序不斷地嘗試可能超時(shí)或者失敗的服務(wù),能達(dá)到應(yīng)用程序正常執(zhí)行而不需要等待下游修正服務(wù)。
熔斷器需要做以下設(shè)置:
設(shè)置錯(cuò)誤率:可以設(shè)置每個(gè)服務(wù)錯(cuò)誤率到達(dá)制定范圍后開始熔斷或降級(jí);
具備人工干預(yù):可以人工手動(dòng)干預(yù),主動(dòng)觸發(fā)降級(jí)服務(wù);
設(shè)置時(shí)間窗口:可配置化來設(shè)置熔斷或者降級(jí)觸發(fā)的統(tǒng)計(jì)時(shí)間窗口;
具備主動(dòng)告警:當(dāng)接口熔斷之后,需要主動(dòng)觸發(fā)短信告知當(dāng)前熔斷的接口信息;
以Sentinel為例,它提供了很多微服務(wù)框架的適配器,如果是Dubbo應(yīng)用,提供了SentinelDubboConsumerFilter和SentinelDubboProviderFilter等Filter,企業(yè)零開發(fā)即可快速接入Sentinel完成對(duì)服務(wù)的保護(hù),只需要在工程的pom.xml里面引入
com.alibaba.csp sentinel-apache-dubbo-adapter 1.7.2
如果是Spring Cloud,只需要在pom.xml里面引入以下內(nèi)容即可快速接入
com.alibaba.cloud spring-cloud-starter-alibaba-sentinel
這里需要注意2點(diǎn):
1) 要先梳理服務(wù)做好服務(wù)分級(jí),降級(jí)、熔斷是針對(duì)非核心流程,如核心流程處理能力不滿足業(yè)務(wù)需要,則需要擴(kuò)充或者優(yōu)化核心流程;
2) 降級(jí)是動(dòng)態(tài)配置后立即生效,而非手動(dòng)去修改源代碼后再發(fā)布服務(wù)服務(wù);
五、接口冪等處理
在分布式環(huán)境中,網(wǎng)絡(luò)環(huán)境比較復(fù)雜,如前端操作抖動(dòng)、APP自動(dòng)重試、網(wǎng)絡(luò)故障、消息重復(fù)、響應(yīng)速度慢等原因,對(duì)接口的重復(fù)調(diào)用概率會(huì)比單體應(yīng)用環(huán)境下更大,所以說重復(fù)消息在分布式環(huán)境中很難避免,所以在分布式架構(gòu)中,要求所有的調(diào)用過程必須具備冪等性,即用戶對(duì)于同一操作發(fā)起的一次請(qǐng)求或者多次請(qǐng)求的結(jié)果是一致的,不會(huì)因?yàn)槎啻吸c(diǎn)擊而產(chǎn)生了副作用。接口的冪等性實(shí)際上就是接口可重復(fù)調(diào)用,在調(diào)用方多次調(diào)用的情況下,接口最終得到的結(jié)果是一致的。冪等的處理方案有多種,比如冪等表、樂觀鎖、token令牌,但是在實(shí)際過程中并不是每個(gè)場景都需要做冪等處理。例如有些場景自身具備冪等性
- select * from user_order where order_num=?
無論查詢多次其結(jié)果不會(huì)因?yàn)椴樵兇螖?shù)導(dǎo)致結(jié)果有影響,所以select的操作天然具備冪等性,無需處理。
- update sys_user set user_state=1 where user_id=?
直接賦值型的update語句操作多次不會(huì)影響結(jié)果,所以此類update操作也天然具備冪等性。
但是當(dāng)以下語句多次調(diào)用的時(shí)候會(huì)引起數(shù)據(jù)不一致,因此需要對(duì)冪等處理
- insert into user_order(id,order_num,user_id) values(?,?,?)
- update user_point set point = score +20 where user_id=?
唯一主鍵機(jī)制:這個(gè)機(jī)制是利用了數(shù)據(jù)庫的主鍵唯一約束的特性,解決了在insert場景時(shí)冪等問題。但主鍵的要求不是自增的主鍵,而是需要業(yè)務(wù)生成全局唯一的主鍵,如果有分庫分表了那么唯一主鍵機(jī)制就沒有效果了。
冪等表:利用數(shù)據(jù)庫唯一索引做防重處理,當(dāng)?shù)谝淮尾迦胧菦]有問題的,第二次在進(jìn)行插入會(huì)因?yàn)槲ㄒ凰饕龍?bào)錯(cuò),從而達(dá)到攔截的目的。
樂觀鎖:通過version來判斷當(dāng)前請(qǐng)求的數(shù)據(jù)是否有變動(dòng),例如
- update user_point set point = point + 20, version = version + 1 where user_id=100 and version=20
Token令牌:為防止重復(fù)提交, 為每次請(qǐng)求生成請(qǐng)求唯一鍵,服務(wù)端對(duì)每個(gè)唯一鍵進(jìn)行生命周期管控,規(guī)定時(shí)間內(nèi)只允許一次請(qǐng)求,非第一次請(qǐng)求都屬于重復(fù)提交,后端要給出單獨(dú)生成token令牌接口,前端要在每次調(diào)用時(shí)候先獲取token令牌。
無論是唯一主鍵機(jī)制還是冪等表都存在唯一鍵的要求,以電商下單場景為例,看看如何來做冪等處理。例如用戶在App下單后,下單請(qǐng)求首先通過Nginx反向代理,轉(zhuǎn)發(fā)到聚合層,聚合層再調(diào)用原子服務(wù),其中訂單號(hào)為全局唯一。這種場景訂單號(hào)誰來生成,如何來保障用戶下單的冪等性呢?
- 假設(shè)原子服務(wù)生成訂單號(hào):如果聚合層第一次調(diào)用原子服務(wù)超時(shí)了,此時(shí)原子服務(wù)已經(jīng)生成了訂單號(hào)為A并寫入訂單表。因第一次超時(shí),聚合層會(huì)再次發(fā)送請(qǐng)求調(diào)用原子服務(wù),此時(shí)原子服務(wù)再生成訂單號(hào)B并寫入訂單表,導(dǎo)致一次下單生成2份訂單數(shù)據(jù)。
- 假設(shè)聚合層生成訂單號(hào):如果訂單號(hào)是聚合層生成,理論上多次調(diào)用原子層都是同一個(gè)訂單號(hào),具備冪等性,但是如何Nginx重復(fù)調(diào)用聚合層的話,仍然會(huì)導(dǎo)致一次申請(qǐng)多個(gè)訂單的情況。
- 假設(shè)Nginx生成訂單號(hào):如果Nginx生成訂單號(hào),理論上多次調(diào)用原聚合層都是同一個(gè)訂單號(hào),具備冪等性,但是如何App端重復(fù)調(diào)用Nginx的話,任然會(huì)導(dǎo)致一次申請(qǐng)多個(gè)訂單的情況。
- 假設(shè)App生成訂單號(hào):最后只能是App針對(duì)每一次下單生成一個(gè)訂單號(hào),并和請(qǐng)求報(bào)文一起發(fā)送給后端。因?yàn)槊總€(gè)App根據(jù)規(guī)則生成訂單號(hào)可能會(huì)導(dǎo)致訂單號(hào)重復(fù)。
比較優(yōu)雅的解決方案是App在下單的時(shí)候生成以一串針對(duì)該用戶唯一的序列(sequenceId)和下單請(qǐng)求一起發(fā)送到后端,聚合層首先判斷sequenceId是否存在,如存在則直接返回成功,否則生成訂單號(hào)并把sequenceId寫入緩存,然后調(diào)用原子服務(wù)插入訂單數(shù)據(jù),如果原子服務(wù)寫入訂單成功則刪除緩存中的sequenceId。通過這里例子可以看到,在微服務(wù)中解決任何問題不能僅看一小塊,需要從全局角度來看待問題。
六、如何保障數(shù)據(jù)一致性
因事物所具備的四大特性ACID(原子性、一致性、隔離性、持久性),使用事物是保障數(shù)據(jù)一致性的有效手段。例如用戶在平臺(tái)上下單訂購某種業(yè)務(wù)的時(shí)候,需要涉及到訂單服務(wù),積分服務(wù),在單體模式下這種業(yè)務(wù)非常容易實(shí)現(xiàn),通過事務(wù)即可完成,偽代碼如下:
- @Transaction
- public Boolean createOrder(OrderDTO order){
- 創(chuàng)建訂單
- 增加積分
- }
然而在微服務(wù)的情況下,原本通過簡單事務(wù)處理的卻變得非常復(fù)雜,訂單、積分被拆分為不同的服務(wù)部署在獨(dú)立的服務(wù)器上,并且數(shù)據(jù)存在在不同的數(shù)據(jù)庫中,傳統(tǒng)的事物處理模式已經(jīng)失效,這里又引出了分布式框架下數(shù)據(jù)的一致性要求。在談數(shù)據(jù)一致性要求的時(shí)候有2個(gè)非常重要的理論即CAP定理和Base理論:
1、CAP定理:C表示一致性,也就是所有用戶看到的數(shù)據(jù)是一樣的,A表示可用性,是指總能找到一個(gè)可用的數(shù)據(jù)副本,P表示分區(qū)容錯(cuò)性,能夠容忍網(wǎng)絡(luò)中斷等故障。
2、BASE理論:BA指的是基本業(yè)務(wù)可用性,支持分區(qū)失敗,當(dāng)分布式系統(tǒng)出現(xiàn)故障的時(shí)候,允許損失一部分可用性,例如在電商大促的時(shí)候,對(duì)一些非核心鏈路的功能進(jìn)行降級(jí)處理來提高系統(tǒng)的可用性,S表示柔性狀態(tài),允許系統(tǒng)存在中間狀態(tài),這個(gè)中間狀態(tài)不會(huì)影響系統(tǒng)整體可用性。比如,數(shù)據(jù)庫讀寫分離,寫庫同步到讀庫(主庫同步到從庫)會(huì)有一個(gè)延時(shí),E表示最終一致性,數(shù)據(jù)最終是一致的,例如主從同步雖然有短暫的數(shù)據(jù)不一致情況,但是最終數(shù)據(jù)還是一致的。
分布式系統(tǒng)中最重要的是讓系統(tǒng)穩(wěn)定并滿足業(yè)務(wù)需求,而不是追求高度抽象,絕對(duì)的系統(tǒng)特性。針對(duì)分布式事物目前開源方案有阿里巴巴開源的無侵入分布式解決方案Seata,它為用戶提供了 AT、TCC、SAGA 和 XA 事務(wù)模式,為用戶打造一站式的分布式解決方案,例如最簡單的AT模式,特點(diǎn)就是對(duì)業(yè)務(wù)無入侵式,分二階段提交,通過簡單配置并在接口上增加@GlobalTransactional即可完成分布式事物,但是在性能上有衰減。在實(shí)際中可以通過本地事務(wù)和發(fā)送MQ消息這種柔性事物方式來解決分布式事物所面臨的問題,既能保障服務(wù)的穩(wěn)定性又能保障調(diào)用效率的高效性,在MQ可以使用Apache的RocketMQ所提供的事物消息和本地事物表結(jié)合。其中以下概念需要理解下:
1、半事務(wù)消息:暫不能投遞的消息,發(fā)送方已經(jīng)成功地將消息發(fā)送到了消息隊(duì)列服務(wù)端,但是服務(wù)端未收到生產(chǎn)者對(duì)該消息的二次確認(rèn),此時(shí)該消息被標(biāo)記成“暫不能投遞”狀態(tài),處于該種狀態(tài)下的消息即半事務(wù)消息。
2、消息回查:由于網(wǎng)絡(luò)閃斷、生產(chǎn)者應(yīng)用重啟等原因,導(dǎo)致某條事務(wù)消息的二次確認(rèn)丟失,消息隊(duì)列服務(wù)端通過掃描發(fā)現(xiàn)某條消息長期處于“半事務(wù)消息”時(shí),需要主動(dòng)向消息生產(chǎn)者詢問該消息的最終狀態(tài)(Commit 或是 Rollback),該詢問過程即消息回查。
整個(gè)流程如下:聚合服務(wù)收到創(chuàng)建訂單請(qǐng)求的時(shí)候,會(huì)發(fā)送一個(gè)事務(wù)性的MQ消息,注意這里的消息只是發(fā)送到消息隊(duì)列,并沒有收到生產(chǎn)者的確認(rèn),因此消息處于半事物狀態(tài),消息隊(duì)列收到消息后會(huì)回調(diào)生產(chǎn)者,這個(gè)時(shí)候就可以完成本地事物(寫訂單表,寫日志表),如果事物提交成功,則把發(fā)送確認(rèn)消息給MQ。針對(duì)下單這種情況,必須要考慮以下幾種異常:
1、 App調(diào)用下單接口,此時(shí)發(fā)送MQ消息異常則直接返回下單失敗,App需要重新點(diǎn)擊下單
2、 MQ回調(diào)生產(chǎn)者的時(shí)候,生產(chǎn)者開始寫入訂單數(shù)據(jù),此時(shí)事物發(fā)生異常,則返回UNKNOW狀態(tài),不要返回ROLLBACK_MESSAGE,因?yàn)锳pp已經(jīng)收到下單成功的通知了,不允許再出現(xiàn)下單失敗的情況;
3、 MQ長時(shí)間(默認(rèn)1分鐘,時(shí)間可調(diào)整)沒有收到生產(chǎn)者確認(rèn)提交消息,會(huì)進(jìn)行消息的回查
相關(guān)代碼具體如下:
- public class TransactionOrderProducer {
- public void init(){
- producer = new TransactionMQProducer(group);
- producer.setTransactionListener(orderTransactionListener);
- this.start();
- }
- //事務(wù)消息發(fā)送
- public TransactionSendResult send(String data, String topic) throws MQClientException {
- Message message = new Message(topic,data.getBytes());
- return this.producer.sendMessageInTransaction(message, null);
- }
- }
當(dāng)消息隊(duì)列收到消息后,會(huì)回調(diào)orderTransactionListener的executeLocalTransaction方法,在這個(gè)方法里面createOrder會(huì)執(zhí)行訂單入庫的操作,同時(shí)會(huì)在日志表總記錄一條數(shù)據(jù)。
- public class OrderTransactionListener implements TransactionListener {
- @Override
- public LocalTransactionState executeLocalTransaction(Message message, Object o) {
- LocalTransactionState state;
- try{
- String order = new String(message.getBody());
- orderService.createOrder(order,message.getTransactionId());
- state = LocalTransactionState.COMMIT_MESSAGE;
- }catch (Exception e){
- state = LocalTransactionState.UNKNOW;
- }
- return state;
- }
- @Override
- public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
- LocalTransactionState state;
- String transactionId = messageExt.getTransactionId();
- if (transactionService.check(transactionId)){
- state = LocalTransactionState.COMMIT_MESSAGE;
- }else {
- String body = new String(messageExt.getBody());
- OrderDTO order = JSONObject.parseObject(body, OrderDTO.class);
- try {
- orderService.createOrder(order, messageExt.getTransactionId());
- }catch (Exception e){
- return LocalTransactionState.UNKNOW;
- }
- state = LocalTransactionState.COMMIT_MESSAGE;
- }
- return state;
- }
- }
積分服務(wù)只需要消費(fèi)普通MQ的消息即可完成分布式事物,在這里把原先要求一致性的事物寫入訂單和增加積分轉(zhuǎn)換為先寫入訂單,積分服務(wù)消費(fèi)MQ來增加積分,達(dá)到柔性事物的機(jī)制。
結(jié)語
以上六種常見問題是在實(shí)施微服務(wù)中最容易遇到的問題,當(dāng)然解決辦法也是因人而異,但是遇到問題的時(shí)候不能僅僅去看一個(gè)點(diǎn),比如冪等問題,如果僅看一個(gè)技術(shù)點(diǎn)的話,很難優(yōu)雅的處理冪等問題??偟膩碚f實(shí)施微服務(wù)不難,因?yàn)橐呀?jīng)有很多成功案例可以借鑒,遇到問題的時(shí)候多去想,從多個(gè)角度去考慮,從全局去考慮。
潘志偉,某金融企業(yè),擁有十多年從業(yè)經(jīng)驗(yàn),精通微服務(wù)架構(gòu),精通大數(shù)據(jù),擁有億級(jí)用戶平臺(tái)架構(gòu)經(jīng)驗(yàn),萬級(jí)并發(fā)的API網(wǎng)關(guān)經(jīng)驗(yàn)。
網(wǎng)站欄目:硬核”實(shí)戰(zhàn)分享:企業(yè)微服務(wù)架構(gòu)設(shè)計(jì)及實(shí)施的六大難點(diǎn)剖析
地址分享:http://fisionsoft.com.cn/article/codgjpg.html


咨詢
建站咨詢
