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