日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢(xún)
選擇下列產(chǎn)品馬上在線(xiàn)溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷(xiāo)解決方案
硬核”實(shí)戰(zhàn)分享:企業(yè)微服務(wù)架構(gòu)設(shè)計(jì)及實(shí)施的六大難點(diǎn)剖析

前言

現(xiàn)如今不管是傳統(tǒng)企業(yè)還是互聯(lián)網(wǎng)公司都在談?wù)撐⒎?wù),微服務(wù)架構(gòu)已經(jīng)成為了互聯(lián)網(wǎng)的熱門(mén)話(huà)題,同時(shí),微服務(wù)的開(kāi)發(fā)框架比如Dubbo、SpringCloud等也是在高頻迭代中,以滿(mǎn)足層出不窮的技術(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)了,所有的問(wèn)題都迎刃而解。然而,在實(shí)施微服務(wù)過(guò)程中出現(xiàn)的各種各樣問(wèn)題如何優(yōu)雅的去解決呢?本文接下來(lái)將介紹如何以“硬核”的方式去解決微服務(wù)改造過(guò)程中遇到的難點(diǎn)問(wèn)題。

一、服務(wù)拆分粒度問(wèn)題

服務(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ù)拆分過(guò)小,會(huì)導(dǎo)致不必要的分布式事務(wù)產(chǎn)生,而且整個(gè)調(diào)用鏈過(guò)程也會(huì)變長(zhǎng),反之,如果服務(wù)拆分過(guò)大,會(huì)逐步演變?yōu)閱误w應(yīng)用,不能發(fā)揮微服務(wù)的優(yōu)勢(shì)。判斷一個(gè)服務(wù)拆分的好壞,就看微服務(wù)拆分完成后是否具備服務(wù)的自治原則,如果把復(fù)雜單體應(yīng)用改造成一個(gè)一個(gè)松耦合式微服務(wù),那么按照業(yè)務(wù)功能分解模式進(jìn)行分解是最簡(jiǎn)單的,只需把業(yè)務(wù)功能相似的模塊聚集在一起。比如:

  1. 用戶(hù)管理:管理用戶(hù)相關(guān)的信息,例如注冊(cè)、修改、注銷(xiāo)或查詢(xún)、統(tǒng)計(jì)等。
  2. 商品管理:管理商品的相關(guān)信息。

業(yè)務(wù)功能分解模式另外的優(yōu)勢(shì)在于在初級(jí)階段服務(wù)拆分不會(huì)太小,等到業(yè)務(wù)發(fā)展起來(lái)后可以再根據(jù)子域方式來(lái)拆分,把獨(dú)立的服務(wù)再拆分成更小的服務(wù),最后到接口級(jí)別服務(wù)。

以用戶(hù)管理舉例,在初始階段的做服務(wù)拆分的時(shí)候,把用戶(hù)管理拆分為用戶(hù)服務(wù),且具備了用戶(hù)的增刪改查功能,在互聯(lián)網(wǎng)中流量獲客是最貴的,運(yùn)營(yíng)團(tuán)隊(duì)通過(guò)互聯(lián)網(wǎng)投放廣告獲客,用戶(hù)在廣告頁(yè)上填寫(xiě)手機(jī)號(hào)碼執(zhí)行注冊(cè)過(guò)程,如果此時(shí)注冊(cè)失敗或者注冊(cè)過(guò)程響應(yīng)時(shí)間過(guò)長(zhǎng),那么這個(gè)客戶(hù)就可能流失了,但是廣告的點(diǎn)擊費(fèi)用產(chǎn)生了,無(wú)形中形成了資源的浪費(fèi)。當(dāng)用戶(hù)規(guī)模上升之后需要對(duì)增刪改查功能做優(yōu)先級(jí)劃分,所以此時(shí)需要按方法維度來(lái)拆分服務(wù),把用戶(hù)服務(wù)拆分為用戶(hù)注冊(cè)服務(wù)(只有注冊(cè)功能),用戶(hù)基礎(chǔ)服務(wù)(修改、查詢(xún)用戶(hù)信息)。

 哪些功能需要被拆分成服務(wù)

無(wú)論是單體應(yīng)用重構(gòu)為微服務(wù)架構(gòu),還是在微服務(wù)架構(gòu)體系下有新增需求,都會(huì)面臨這些功能或者新增需求是否需要被拆分為服務(wù)。雖然沒(méi)有相關(guān)規(guī)定,但是可以遵循服務(wù)拆分的方法論:當(dāng)一塊業(yè)務(wù)不依賴(lài)或極少依賴(lài)其它服務(wù),有獨(dú)立的業(yè)務(wù)語(yǔ)義,為超過(guò) 2 個(gè)或以上的其他服務(wù)或客戶(hù)端提供數(shù)據(jù),應(yīng)該被拆分成一個(gè)獨(dú)立的服務(wù)模,而且拆分的服務(wù)要具備高內(nèi)聚低耦合。所謂的高內(nèi)聚是指一個(gè)組件中各個(gè)元素互相依賴(lài)的程度,是衡量某個(gè)模塊或者類(lèi)中各個(gè)代碼片段之間關(guān)聯(lián)強(qiáng)度的標(biāo)準(zhǔn),比如用戶(hù)服務(wù),只會(huì)提供用戶(hù)相關(guān)的增刪改查信息,假如還關(guān)聯(lián)了用戶(hù)訂單相關(guān)的信息,那就說(shuō)明這個(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ù)庫(kù)交互;

2、原子服層:數(shù)據(jù)庫(kù)交互層,實(shí)現(xiàn)數(shù)據(jù)的增刪改查,結(jié)合緩存和工具保障服務(wù)的高響應(yīng);要遵循單表原則,禁止2張以上的表做join查詢(xún),如有分庫(kù)分表,那么對(duì)外要屏蔽具體規(guī)則,提供服務(wù)接口供外部調(diào)用。

如果使用到緩存,那么到底在聚合層加還是原子層加還是其他呢?應(yīng)該遵循“誰(shuí)構(gòu)建,誰(shuí)運(yùn)維”這一理念,是否使用緩存應(yīng)該由對(duì)應(yīng)的開(kāi)發(fā)人員自行維護(hù),也就是說(shuō)聚合層和原子層都需要增加緩存。一般來(lái)說(shuō)聚合層和原子層由不同的團(tuán)隊(duì)開(kāi)發(fā),聚合層和業(yè)務(wù)端比較貼近,需要了解業(yè)務(wù)流程更好的服務(wù)業(yè)務(wù),和App端交互非常多,重點(diǎn)是合理設(shè)計(jì)的前后端接口,減少App和后端交互次數(shù)。原子服務(wù)則是關(guān)注性能,屏蔽數(shù)據(jù)庫(kù)操作,屏蔽分庫(kù)分表等操作。在聚合層推薦使用多級(jí)緩存,即本地緩存+分布式緩存,本地緩存不做緩存數(shù)據(jù)的變更,使用TTL自動(dòng)過(guò)期時(shí)間來(lái)自動(dòng)更新緩存內(nèi)的數(shù)據(jù)。

緩存使用過(guò)程中不可避免的問(wèn)題

在使用緩存的時(shí)候不可避免的會(huì)遇到緩存穿透、緩存擊穿、緩存雪崩等場(chǎng)景,針對(duì)每種場(chǎng)景的時(shí)候需要使用不同的應(yīng)對(duì)策略,從而保障系統(tǒng)的高可用性。

1、緩存穿透:是指查詢(xún)一個(gè)一定不存在緩存key,由于緩存是未命中的時(shí)候需要從數(shù)據(jù)庫(kù)查詢(xún),正常情況下查不到數(shù)據(jù)則不寫(xiě)入緩存,就會(huì)導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到數(shù)據(jù)庫(kù)去查詢(xún),造成緩存穿透,有2個(gè)方案可以解決緩存穿透:

1) 可以使用布隆過(guò)濾器方案,系統(tǒng)啟動(dòng)的時(shí)候?qū)⑺汛嬖诘臄?shù)據(jù)哈希到一個(gè)足夠大的bitmap中,當(dāng)一個(gè)一定不存在的數(shù)據(jù)請(qǐng)求的時(shí)候,會(huì)被這個(gè)bitmap攔截掉,從而避免了對(duì)底層數(shù)據(jù)庫(kù)的查詢(xún)壓力。

 
 
 
 
  1. @Component 
  2. public class BloomFilterCache { 
  3.     public static BloomFilter bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 10000); 
  4.     @PostConstruct 
  5.     public  void init(){ 
  6. List list=Lists.newArrayList(); //初始化加載所有的需要被緩存的數(shù)據(jù)ID 
  7.         list.forEach(id ->bloomFilter.put(id)); 
  8.     } 
  9. public  boolean addKey(Integer key){ 
  10.      return bloomFilter.put(key); 
  11.     public  boolean isCached(Integer key){ 
  12.         return bloomFilter.mightContain(key); 
  13.     } 

這里的BloomFilter選用guava提供的第三方包,服務(wù)啟動(dòng)的時(shí)候,init方法會(huì)加載所有可以被緩存的數(shù)據(jù),把id都放入boolmFilter中,當(dāng)有新增數(shù)據(jù)的時(shí)候,執(zhí)行addKey把新增的數(shù)據(jù)放入BoolmFilter過(guò)濾器中。

當(dāng)在需要使用緩存的地方先調(diào)用isCached方法,如果返回true表示正常請(qǐng)求,否則拒絕。

2) 返回空值:如果一個(gè)查詢(xún)請(qǐng)求查詢(xún)數(shù)據(jù)庫(kù)后返回的數(shù)據(jù)為空(不管是數(shù)據(jù)不存在,還是系統(tǒng)故障),仍然把這個(gè)空結(jié)果進(jìn)行緩存,但它的過(guò)期時(shí)間會(huì)很短,比如1分鐘,但是這種方法解決不夠徹底。

2、緩存擊穿:緩存key在某個(gè)時(shí)間點(diǎn)過(guò)期的時(shí)候,剛好在這個(gè)時(shí)間點(diǎn)對(duì)這個(gè)Key有大量的并發(fā)請(qǐng)求過(guò)來(lái),請(qǐng)求命中緩存失敗后會(huì)通過(guò)DB加載數(shù)據(jù)并回寫(xiě)到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端DB壓垮,解決方案也很簡(jiǎn)單通過(guò)加鎖的方式讀取數(shù)據(jù),同時(shí)寫(xiě)入緩存。

 
 
 
 
  1. Object [] objects={0,1,2,3,4,5,6,7,8,9}; 
  2.     public List getData(Integer id) throws InterruptedException { 
  3.         List result = new ArrayList(); 
  4.         result = getDataFromCache(id); 
  5.         if (result.isEmpty()) { 
  6.                 int objLength= objects.length; 
  7.                 synchronized (objects[id% objects.length]) { 
  8.                     result = getDataFromDB(id); 
  9.                     setDataToCache(result); 
  10.                } 
  11.               }  
  12.         return result; 

這里加鎖的方法使用的是Object數(shù)組,是希望因不同的id不會(huì)因?yàn)閺臄?shù)據(jù)庫(kù)加載數(shù)據(jù)被阻塞,例如id=1、id=2、id=3的key同時(shí)在緩存中消失,微服務(wù)路由策略剛好都把這些請(qǐng)求都路由到同一臺(tái)機(jī)器上,假設(shè)查詢(xún)DB需要50毫秒,如果僅使用synchronized(object){……}則id=3的請(qǐng)求會(huì)被阻塞,需要等等150毫秒才能返回結(jié)果,但是使用上述方法則只需要50毫秒出結(jié)果。其中objects數(shù)據(jù)的大小可以根據(jù)DB能承載的并發(fā)量以及原子服務(wù)數(shù)量綜合考慮。

3、緩存雪崩:是指在設(shè)置緩存時(shí)使用了相同的過(guò)期時(shí)間,導(dǎo)致緩存在某一時(shí)刻同時(shí)失效,所有的查詢(xún)都請(qǐng)求到數(shù)據(jù)庫(kù)上,導(dǎo)致應(yīng)用系統(tǒng)產(chǎn)生各種故障,這樣情況稱(chēng)之為緩存雪崩,可以通過(guò)限流的方式來(lái)限制請(qǐng)求數(shù)據(jù)庫(kù)的次數(shù)。

三、串行化并行解決效率問(wèn)題

一個(gè)應(yīng)用功能被拆分成多個(gè)服務(wù)之后,原本調(diào)用一個(gè)接口就能完成的功能如今變成需要調(diào)用多個(gè)服務(wù),如果按順序逐個(gè)調(diào)用的話(huà),使用微服務(wù)改造后的接口會(huì)比原始接口響應(yīng)時(shí)間更長(zhǎng),因此要把原本串行調(diào)用的服務(wù)修改為并行調(diào)用,同時(shí)原本通過(guò)SQL的join多表聯(lián)合查詢(xú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方法,偽代碼如下:

 
 
 
 
  1. CompletableFuture futureS1 = CompletableFuture.supplyAsync(() -> { 
  2.         S1接口 },executor); 
  3. CompletableFuture futureS2 = CompletableFuture.supplyAsync(() -> { 
  4.         S2接口  },executor); 
  5. CompletableFuture futureS3 = CompletableFuture.supplyAsync(() -> { 
  6.         S3接口 },executor); 
  7. CompletableFuture.allOf(futureS1, futureS2, futureS3).get(500, TimeUnit.MILLISECONDS); 

此時(shí)就把原本串聯(lián)調(diào)用的服務(wù)變成并行調(diào)用,節(jié)約了接口請(qǐng)求時(shí)間,但卻引發(fā)一個(gè)新的問(wèn)題,內(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)用過(guò)程中可能會(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)用方占滿(mǎn)整個(gè)線(xiàn)程池,導(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á)制定范圍后開(kāi)始熔斷或降級(jí);

具備人工干預(yù):可以人工手動(dòng)干預(yù),主動(dòng)觸發(fā)降級(jí)服務(wù);

設(shè)置時(shí)間窗口:可配置化來(lái)設(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è)零開(kāi)發(fā)即可快速接入Sentinel完成對(duì)服務(wù)的保護(hù),只需要在工程的pom.xml里面引入

 
 
 
 
  1.  
  2.     com.alibaba.csp 
  3.     sentinel-apache-dubbo-adapter 
  4.     1.7.2 
  5.  

如果是Spring Cloud,只需要在pom.xml里面引入以下內(nèi)容即可快速接入

 
 
 
 
  1.  
  2.     com.alibaba.cloud 
  3.     spring-cloud-starter-alibaba-sentinel 
  4.  

這里需要注意2點(diǎn):

1) 要先梳理服務(wù)做好服務(wù)分級(jí),降級(jí)、熔斷是針對(duì)非核心流程,如核心流程處理能力不滿(mǎn)足業(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)境下更大,所以說(shuō)重復(fù)消息在分布式環(huán)境中很難避免,所以在分布式架構(gòu)中,要求所有的調(diào)用過(guò)程必須具備冪等性,即用戶(hù)對(duì)于同一操作發(fā)起的一次請(qǐng)求或者多次請(qǐng)求的結(jié)果是一致的,不會(huì)因?yàn)槎啻吸c(diǎn)擊而產(chǎn)生了副作用。接口的冪等性實(shí)際上就是接口可重復(fù)調(diào)用,在調(diào)用方多次調(diào)用的情況下,接口最終得到的結(jié)果是一致的。冪等的處理方案有多種,比如冪等表、樂(lè)觀鎖、token令牌,但是在實(shí)際過(guò)程中并不是每個(gè)場(chǎng)景都需要做冪等處理。例如有些場(chǎng)景自身具備冪等性

 
 
 
 
  1. select * from user_order where order_num=? 

無(wú)論查詢(xún)多次其結(jié)果不會(huì)因?yàn)椴樵?xún)次數(shù)導(dǎo)致結(jié)果有影響,所以select的操作天然具備冪等性,無(wú)需處理。

 
 
 
 
  1. update sys_user set user_state=1 where user_id=? 

直接賦值型的update語(yǔ)句操作多次不會(huì)影響結(jié)果,所以此類(lèi)update操作也天然具備冪等性。

但是當(dāng)以下語(yǔ)句多次調(diào)用的時(shí)候會(huì)引起數(shù)據(jù)不一致,因此需要對(duì)冪等處理

 
 
 
 
  1. insert into user_order(id,order_num,user_id) values(?,?,?) 
  2.  
  3. update user_point set point = score +20 where user_id=? 

唯一主鍵機(jī)制:這個(gè)機(jī)制是利用了數(shù)據(jù)庫(kù)的主鍵唯一約束的特性,解決了在insert場(chǎng)景時(shí)冪等問(wèn)題。但主鍵的要求不是自增的主鍵,而是需要業(yè)務(wù)生成全局唯一的主鍵,如果有分庫(kù)分表了那么唯一主鍵機(jī)制就沒(méi)有效果了。

冪等表:利用數(shù)據(jù)庫(kù)唯一索引做防重處理,當(dāng)?shù)谝淮尾迦胧菦](méi)有問(wèn)題的,第二次在進(jìn)行插入會(huì)因?yàn)槲ㄒ凰饕龍?bào)錯(cuò),從而達(dá)到攔截的目的。

樂(lè)觀鎖:通過(guò)version來(lái)判斷當(dāng)前請(qǐng)求的數(shù)據(jù)是否有變動(dòng),例如

 
 
 
 
  1. 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令牌。

無(wú)論是唯一主鍵機(jī)制還是冪等表都存在唯一鍵的要求,以電商下單場(chǎng)景為例,看看如何來(lái)做冪等處理。例如用戶(hù)在App下單后,下單請(qǐng)求首先通過(guò)Nginx反向代理,轉(zhuǎn)發(fā)到聚合層,聚合層再調(diào)用原子服務(wù),其中訂單號(hào)為全局唯一。這種場(chǎng)景訂單號(hào)誰(shuí)來(lái)生成,如何來(lái)保障用戶(hù)下單的冪等性呢?

  • 假設(shè)原子服務(wù)生成訂單號(hào):如果聚合層第一次調(diào)用原子服務(wù)超時(shí)了,此時(shí)原子服務(wù)已經(jīng)生成了訂單號(hào)為A并寫(xiě)入訂單表。因第一次超時(shí),聚合層會(huì)再次發(fā)送請(qǐng)求調(diào)用原子服務(wù),此時(shí)原子服務(wù)再生成訂單號(hào)B并寫(xiě)入訂單表,導(dǎo)致一次下單生成2份訂單數(shù)據(jù)。
  • 假設(shè)聚合層生成訂單號(hào):如果訂單號(hào)是聚合層生成,理論上多次調(diào)用原子層都是同一個(gè)訂單號(hào),具備冪等性,但是如何Nginx重復(fù)調(diào)用聚合層的話(huà),仍然會(huì)導(dǎo)致一次申請(qǐng)多個(gè)訂單的情況。
  • 假設(shè)Nginx生成訂單號(hào):如果Nginx生成訂單號(hào),理論上多次調(diào)用原聚合層都是同一個(gè)訂單號(hào),具備冪等性,但是如何App端重復(fù)調(diào)用Nginx的話(huà),任然會(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ì)該用戶(hù)唯一的序列(sequenceId)和下單請(qǐng)求一起發(fā)送到后端,聚合層首先判斷sequenceId是否存在,如存在則直接返回成功,否則生成訂單號(hào)并把sequenceId寫(xiě)入緩存,然后調(diào)用原子服務(wù)插入訂單數(shù)據(jù),如果原子服務(wù)寫(xiě)入訂單成功則刪除緩存中的sequenceId。通過(guò)這里例子可以看到,在微服務(wù)中解決任何問(wèn)題不能僅看一小塊,需要從全局角度來(lái)看待問(wèn)題。

六、如何保障數(shù)據(jù)一致性

因事物所具備的四大特性ACID(原子性、一致性、隔離性、持久性),使用事物是保障數(shù)據(jù)一致性的有效手段。例如用戶(hù)在平臺(tái)上下單訂購(gòu)某種業(yè)務(wù)的時(shí)候,需要涉及到訂單服務(wù),積分服務(wù),在單體模式下這種業(yè)務(wù)非常容易實(shí)現(xiàn),通過(guò)事務(wù)即可完成,偽代碼如下:

 
 
 
 
  1. @Transaction  
  2. public Boolean createOrder(OrderDTO order){ 
  3. 創(chuàng)建訂單 
  4. 增加積分 

然而在微服務(wù)的情況下,原本通過(guò)簡(jiǎn)單事務(wù)處理的卻變得非常復(fù)雜,訂單、積分被拆分為不同的服務(wù)部署在獨(dú)立的服務(wù)器上,并且數(shù)據(jù)存在在不同的數(shù)據(jù)庫(kù)中,傳統(tǒng)的事物處理模式已經(jīng)失效,這里又引出了分布式框架下數(shù)據(jù)的一致性要求。在談數(shù)據(jù)一致性要求的時(shí)候有2個(gè)非常重要的理論即CAP定理和Base理論:

1、CAP定理:C表示一致性,也就是所有用戶(hù)看到的數(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í)處理來(lái)提高系統(tǒng)的可用性,S表示柔性狀態(tài),允許系統(tǒng)存在中間狀態(tài),這個(gè)中間狀態(tài)不會(huì)影響系統(tǒng)整體可用性。比如,數(shù)據(jù)庫(kù)讀寫(xiě)分離,寫(xiě)庫(kù)同步到讀庫(kù)(主庫(kù)同步到從庫(kù))會(huì)有一個(gè)延時(shí),E表示最終一致性,數(shù)據(jù)最終是一致的,例如主從同步雖然有短暫的數(shù)據(jù)不一致情況,但是最終數(shù)據(jù)還是一致的。

分布式系統(tǒng)中最重要的是讓系統(tǒng)穩(wěn)定并滿(mǎn)足業(yè)務(wù)需求,而不是追求高度抽象,絕對(duì)的系統(tǒng)特性。針對(duì)分布式事物目前開(kāi)源方案有阿里巴巴開(kāi)源的無(wú)侵入分布式解決方案Seata,它為用戶(hù)提供了 AT、TCC、SAGA 和 XA 事務(wù)模式,為用戶(hù)打造一站式的分布式解決方案,例如最簡(jiǎn)單的AT模式,特點(diǎn)就是對(duì)業(yè)務(wù)無(wú)入侵式,分二階段提交,通過(guò)簡(jiǎn)單配置并在接口上增加@GlobalTransactional即可完成分布式事物,但是在性能上有衰減。在實(shí)際中可以通過(guò)本地事務(wù)和發(fā)送MQ消息這種柔性事物方式來(lái)解決分布式事物所面臨的問(wèn)題,既能保障服務(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ù)端通過(guò)掃描發(fā)現(xiàn)某條消息長(zhǎng)期處于“半事務(wù)消息”時(shí),需要主動(dòng)向消息生產(chǎn)者詢(xún)問(wèn)該消息的最終狀態(tài)(Commit 或是 Rollback),該詢(xún)問(wèn)過(guò)程即消息回查。

整個(gè)流程如下:聚合服務(wù)收到創(chuàng)建訂單請(qǐng)求的時(shí)候,會(huì)發(fā)送一個(gè)事務(wù)性的MQ消息,注意這里的消息只是發(fā)送到消息隊(duì)列,并沒(méi)有收到生產(chǎn)者的確認(rèn),因此消息處于半事物狀態(tài),消息隊(duì)列收到消息后會(huì)回調(diào)生產(chǎn)者,這個(gè)時(shí)候就可以完成本地事物(寫(xiě)訂單表,寫(xiě)日志表),如果事物提交成功,則把發(fā)送確認(rèn)消息給MQ。針對(duì)下單這種情況,必須要考慮以下幾種異常:

1、 App調(diào)用下單接口,此時(shí)發(fā)送MQ消息異常則直接返回下單失敗,App需要重新點(diǎn)擊下單

2、 MQ回調(diào)生產(chǎn)者的時(shí)候,生產(chǎn)者開(kāi)始寫(xiě)入訂單數(shù)據(jù),此時(shí)事物發(fā)生異常,則返回UNKNOW狀態(tài),不要返回ROLLBACK_MESSAGE,因?yàn)锳pp已經(jīng)收到下單成功的通知了,不允許再出現(xiàn)下單失敗的情況;

3、 MQ長(zhǎng)時(shí)間(默認(rèn)1分鐘,時(shí)間可調(diào)整)沒(méi)有收到生產(chǎn)者確認(rèn)提交消息,會(huì)進(jìn)行消息的回查

相關(guān)代碼具體如下:

 
 
 
 
  1. public class TransactionOrderProducer { 
  2.     public void init(){ 
  3.         producer = new TransactionMQProducer(group); 
  4.         producer.setTransactionListener(orderTransactionListener); 
  5.         this.start(); 
  6.     } 
  7.     //事務(wù)消息發(fā)送 
  8.     public TransactionSendResult send(String data, String topic) throws MQClientException { 
  9.         Message message = new Message(topic,data.getBytes()); 
  10.         return this.producer.sendMessageInTransaction(message, null); 
  11.     } 

當(dāng)消息隊(duì)列收到消息后,會(huì)回調(diào)orderTransactionListener的executeLocalTransaction方法,在這個(gè)方法里面createOrder會(huì)執(zhí)行訂單入庫(kù)的操作,同時(shí)會(huì)在日志表總記錄一條數(shù)據(jù)。

 
 
 
 
  1. public class OrderTransactionListener implements TransactionListener { 
  2.     @Override 
  3. public LocalTransactionState executeLocalTransaction(Message message, Object o) {   
  4.         LocalTransactionState state; 
  5.         try{ 
  6.             String order = new String(message.getBody());          
  7.             orderService.createOrder(order,message.getTransactionId()); 
  8.             state = LocalTransactionState.COMMIT_MESSAGE; 
  9.         }catch (Exception e){ 
  10.             state = LocalTransactionState.UNKNOW; 
  11.         } 
  12.         return state; 
  13.     } 
  14.     @Override 
  15.     public LocalTransactionState checkLocalTransaction(MessageExt messageExt) { 
  16.         LocalTransactionState state; 
  17.         String transactionId = messageExt.getTransactionId(); 
  18.         if (transactionService.check(transactionId)){ 
  19.             state = LocalTransactionState.COMMIT_MESSAGE; 
  20.         }else { 
  21.             String body = new String(messageExt.getBody()); 
  22.             OrderDTO order = JSONObject.parseObject(body, OrderDTO.class); 
  23.             try { 
  24.                 orderService.createOrder(order, messageExt.getTransactionId()); 
  25.             }catch (Exception e){ 
  26.                 return LocalTransactionState.UNKNOW; 
  27.             } 
  28.             state = LocalTransactionState.COMMIT_MESSAGE; 
  29.         } 
  30.         return state; 
  31.     } 

積分服務(wù)只需要消費(fèi)普通MQ的消息即可完成分布式事物,在這里把原先要求一致性的事物寫(xiě)入訂單和增加積分轉(zhuǎn)換為先寫(xiě)入訂單,積分服務(wù)消費(fèi)MQ來(lái)增加積分,達(dá)到柔性事物的機(jī)制。

結(jié)語(yǔ)

以上六種常見(jiàn)問(wèn)題是在實(shí)施微服務(wù)中最容易遇到的問(wèn)題,當(dāng)然解決辦法也是因人而異,但是遇到問(wèn)題的時(shí)候不能僅僅去看一個(gè)點(diǎn),比如冪等問(wèn)題,如果僅看一個(gè)技術(shù)點(diǎn)的話(huà),很難優(yōu)雅的處理冪等問(wèn)題??偟膩?lái)說(shuō)實(shí)施微服務(wù)不難,因?yàn)橐呀?jīng)有很多成功案例可以借鑒,遇到問(wèn)題的時(shí)候多去想,從多個(gè)角度去考慮,從全局去考慮。

 潘志偉,某金融企業(yè),擁有十多年從業(yè)經(jīng)驗(yàn),精通微服務(wù)架構(gòu),精通大數(shù)據(jù),擁有億級(jí)用戶(hù)平臺(tái)架構(gòu)經(jīng)驗(yàn),萬(wà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)剖析
鏈接URL:http://www.5511xx.com/article/codgjpg.html