海量交易訂單查詢沒做“重試”,一哥們"喜提"P3故障!

免費視頻福利推薦:

2T免費學習視頻,內含精選高頻面試題、SSM、Dubbo、Spring全家桶、微服務、MySQL、MyCat、集羣、分佈式、高併發、中間件、Linux、網絡、多線程,Jenkins、Nexus、Docker、ELK等等免費學習視頻,持續更新!


往期熱門文章推薦:

1、《2019年精選優秀博文都在這裏了!》

2、格式化時間用了YYYY-MM-dd,元旦當天老闆喊我回去改Bug!

3、39 個奇葩代碼註釋,看完笑哭了。。。

4、牛逼的人,都已經開始用文言文寫代碼了!

5、如何優雅地根治null值引起的Bug!


一、故事情節

以下情節根據真實事件改編!

由於現在PDD模式比較火,某大廠的一哥們,接到老闆的需求,做一個拼團業務,具體的業務需求是這樣的:

1、一個拼團活動有開始時間和結束時間;

2、某一商戶下的某一商品當有第一個用戶購買的時候,創建一個拼團單,後邊用戶在當前商戶購買當前商品的時候,直接加入該拼團單(商戶ID和商品ID決定一個拼團單);

3、拼團活動結束後使用定時任務判斷是否成團,如果某一商品的拼團單購買的商品數量到達拼團的最低門檻,則拼團成功,否則拼團失敗;

4、觸發定時任務的時候,拿着所有的拼團單,查詢每一個拼團單對應的交易單;

5、判斷某一拼團單用戶的下單數量是否達到最低門檻,決定拼團成功失敗;

提示:畫圖工具使用的是OmniGraffle
在這裏插入圖片描述
因爲知道,交易庫數據量巨大,再加上業務剛上線,被交易那邊限流,這哥們還特意進行了分頁查詢,每頁查詢50條數據,定時任務也採用主子任務的方式,拆分子任務處理拼團單,儘量減少每次查詢交易單的數量。

業務剛上線的時候,前幾天跑的好好的,定時任務執行完全OK!但是後面不知道運營咋推的,拼團單數量直接翻倍,每個拼團單交易單數量飆升!定時任務直接跑超時了,問題經查發現是交易單查詢超時!直接導致拼團活動到達結束時間,拼團活動無法在約定的時間結束,無法結算,無法發貨,造成資損XX元。後來通過定時任務手動重試解決查詢超時問題。這哥們理所當然的"喜提"P3故障!

注意:

聽哥們說,拼團活動結束的時候,定時任務處理的拼團單的數據量峯值大概有5W+,每一個拼團活動滿足成團條件的最低成交數量是10個,需要查詢交易單的數據量在50W左右,即使做了任務的拆分,每一個子任務只處理幾百個拼團單,每一個拼團單隻查詢幾十個交易單數據,但是在查詢某一拼團交易成功的數量時,交易接口還是超時了(哥們公司規定,接口RT時間需要保證在50ms以內,查詢交易單,還特意設置了RT時間爲2000ms)

二、問題所在

我們先不去噴爲什麼要有這樣的業務需求,爲什麼要在定時任務裏批量的處理拼團是否成功?爲什麼不加緩存?爲什麼查詢的訂單量這麼少就超時了?爲什麼???

從上述的場景中,得出的結論就是,關鍵的接口超時沒有做好重試處理,數據量上升的時候,查詢超時的問題不能夠自動解決!

注意:

一般接口超時,基本重試1-2次之後,都是可以ok的!除非你調用的服務徹底掛了!這樣可以扔到隊列裏邊去定時執行,上述的業務,就是通過定時任務重試的方式。在服務長時間無法訪問的時候,通過整體重試的方式避免業務無法跑下去!

當然接口超時,除了重試,還有其他的方案,這裏我們只介紹重試方案,因爲最簡單了!

三、超時的幾點問題

3.1、讀超時

讀超時,一個比較好的地方就是,你只需要進行重試就可以簡單的解決問題,因爲你不會牽扯到數據的變更,因此重試的時候,無需保證數據訪問的冪等;

3.2、寫超時

寫超時(增、刪、改)的話就比較麻煩了,因爲你無法知道,寫超時了,數據庫是否發生了變更:

  • 數據寫入成功,返回超時了,數據庫已真實變更了這條數據;
  • 數據未寫入,請求超時了,數據庫未發生變更;

上述兩種情況,返回的錯誤碼都是TimeOut,因此無法區分,這個時候,你就需要做好冪等處理了!

三、冪等處理的幾個關鍵

關於冪等處理的幾種方式,不是本文所要闡述的內容,有需要的可以參考:《高併發下的接口冪等性解決方案!

3.1、半冪等

例如:

插入一條數據,調用服務A,A服務插入數據庫的時候,根據主鍵衝突策略,發現已經已經存在了,直接返回錯誤,報已經存在主鍵了;

這種方式,服務A冪等做的不徹底,只是保證數據不會變更,但是通過返回錯誤來實現,這樣的話,就需要調用方先進行一次查詢操作,判斷數據是否存在了,如果存在則不插入,如果不存在再調用A服務插入數據了;

3.2、全冪等

相對的,如果調用的服務A,在插入數據的時候,自己先查詢一下數據是否已經存在,如果存在直接返回成功,如果不存在則執行插入操作,那麼調用方就就直接執行插入操作就可以了,無需自己判斷數據是否已經存在了,那麼接口A就是全冪等的了;

3.3、冪等需要關注的幾個問題

以下幾點並不是需要注意問題的全部,歡迎大家留言補充!

3.3.1、服務的調用方和服務的提供方冪等鍵要保證一致,唯一性,並且不變性;

這個很好理解,例如:

  • 服務的調用方以爲調用方是按照用戶身份證號做冪等的,但其實服務提供方是按照手機號做冪等的,這樣就出現問題了;

  • 服務的提供方前段時間還是用A做冪等鍵的,後邊卻用B了,說變就變的也是不可以的;

因此,服務調用方在調用服務之前一定要確定好服務提供方冪等鍵的設置;

3.3.2、調用方不能單純的依靠查詢來做冪等

例如:用戶咔咔點擊了兩次,兩個線程執行,同時執行插入操作,兩個線程都先查詢,結果某一時間點查詢的數據都不存在,然後就執行插入操作了,就插入了兩條數據;

在這裏插入圖片描述
這個時候,就需要加鎖處理了或者根據主鍵衝突策略等方式判斷冪等了;

3.1、3.2中舉例還是有瑕疵的,大家注意!

3.3.3、調用方冪等鍵唯一了,但是其他數據卻變了,業務做好處理,具體業務具體分析

這種情況很常見,例如:服務提供方約定以手機號作爲冪等鍵,但是服務的調用方第一次插入數據的時候,手機號是A,其他數據是B,第二次調用的時候,手機號是A,其他數據確是C,那服務提供方到底讓不讓你插哪?這個就需要根據具體的業務做分析了,如果業務決定,讓你插,你就插,不讓你插就不能插了!

3.3.4、冪等鍵跟隨數據做好持久化,做到“有據可依”,禁止冪等鍵純內存拼接

這個很好理解,舉個例子吧:

插入一條數據,拼接了一個冪等鍵ABC,你如果不做持久化,數據存儲不包含ABC三個字段,那麼你下次如何判斷數據是否已經存在哪?

3.3.5、消息冪等處理的幾個關鍵

消息冪等是一個比較複雜的場景,因爲消息可能存在的無序性、重複性、延遲,都增加了冪等處理的複雜性,其中重複性則是冪等的時候需要重點考慮的;

1、重複性

例如:交易系統存在下單、支付、發貨行爲,交易系統如果多次消費同一筆定金支付成功消息時,由於冪等問題可能導致很多問題:
在這裏插入圖片描述
一般,我們在發送交易消息的時候,會把 “訂單的狀態和訂單ID” 作爲消息體的一部分,然後在接收到消息的時候,根據消息的類型判斷是不是下單消息,以及判斷當前訂單的狀態是否是”用戶下單“,這樣在消息不重複消費的時候,是沒有問題的。

如果出現上述情況,用戶下單消息重複消費,在接收到用戶支付消息的時候訂單狀態已經被修改爲已支付,但是由於用戶下單消息重複消費,消息體是沒有變化的(狀態沒有發生變化),就又修改訂單狀態爲待支付狀態了,這裏顯然是不對的。

我們應該做:

我們應該在接收到消息的時候,根據訂單ID去數據庫查詢一下訂單此時的狀態,然後根據當前的狀態判斷下一步的操作,並且消息處理的時候還要加鎖哦!加鎖的維度可以是訂單ID!防止併發的時候,出現3.3.2中的情況!

因此,不要把可變值作爲冪等的條件,加鎖查詢訂單最新的狀態!

2、無序性

保證消息的順序消費是比較複雜的,並且成本也很高,一般我們可以根據不同的業務判斷消息消費的順序性的;

例如:用戶下單消息=>用戶支付消息,順序的行爲是這樣消費的。但無序的時候,我們可能先接收到”用戶支付消息“然後纔會接收到”用戶下單消息“。

如果你的業務在接收用戶下單消息做的處理不影響主鏈路的話,則可以直接先處理”用戶支付消息“,當在收到”用戶下單消息“的時候,查詢訂單的狀態已經變爲”已支付“,則直接把消息冪等掉,返回true,結束消息的消費。

但是,如果你的”用戶下單消息“有重要的邏輯,必須先消費了之後,纔可以消費”用戶支付消息“,那我們就需要特別注意了!根據查詢出來的訂單狀態進行判斷,判斷是否已經消費了”用戶下單消息“,當先接收到”用戶支付消息“的時候,消息直接重發就可以了,等消費了”用戶下單消息“之後,再消費”用戶支付消息“。
在這裏插入圖片描述

3.3.5、定時任務冪等處理的幾個關鍵

定時任務的冪等需要解決的主要問題就是”重複性“,和消息的重複消費問題大致相同,需要根據查詢最新的狀態進行業務的處理,這裏不做過多說明;

四、如何實現優雅的重試

Show代碼不是目的,講到底纔是真諦!

4.1、爲什麼進行重試

我們依賴的外部服務對於調用者來說一般都是不可靠的,尤其是在網絡環境比較差的情況下,網絡抖動很容易導致請求超時等異常情況,這時候我們就需要使用失敗重試策略重新調用服務提供方的接口來獲取數據。

一般網絡抖動引起的接口超時,基本重試1-2次之後,都是可以ok的!

重試常見的一種方式是使用定時任務重試,例如某次操作失敗,記錄下來,當定時任務再次啓動,則將數據放到定時任務的方法中,重新跑一遍,最終直至得到想要的結果爲止。

缺點就是,依賴於定時任務工具的特性,重試機制相對簡單,一般只能實現支持Cron表達式每隔多長時間進行重試;

無論是基於定時任務的重試機制,還是我們自己寫的簡單的重試器,缺點都是重試的機制太單一,而且實現起來不優雅,我們很難解決在什麼條件下需要重試、什麼條件下需要結束重試、重試等待的時間,很難監控整個重試的過程等等問題。

4.2、如何實現優雅的重試?

代碼是很枯燥的,授人以魚不如授人以漁,今天就給大家介紹兩款工具:Guava-retrying和Spring-retry,讓你實現優雅的重試!

4.2.1、Guava-retrying

今天就給大家介紹一款重試利器:Guava-retrying,Guava-retrying是基於谷歌的核心類庫Guava的重試機制實現。

GitHub地址:https://github.com/rholder/guava-retrying

1、引入依賴:

<dependency>
	<groupId>com.github.rholder</groupId>
	<artifactId>guava-retrying</artifactId>
	<version>2.0.0</version>
</dependency>

2、簡單實現代碼

public Boolean test() throws Exception {
    //定義重試機制
    Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
            //retryIf 重試條件
            .retryIfException()
            .retryIfRuntimeException()
            .retryIfExceptionOfType(Exception.class)
            .retryIfException(Predicates.equalTo(new Exception()))
            .retryIfResult(Predicates.equalTo(false))

            //等待策略:每次請求間隔1s
            .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
            
            //停止策略 : 嘗試請求6次
            .withStopStrategy(StopStrategies.stopAfterAttempt(6))

            //時間限制 : 某次請求不得超過2s , 類似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
            .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))

            .build();

    //定義請求實現
    Callable<Boolean> callable = new Callable<Boolean>() {
        int times = 1;

        @Override
        public Boolean call() throws Exception {
        	//你的具體業務是什麼,下邊簡單模擬處理
        	
            log.info("call times={}", times);
            times++;

            if (times == 2) {
                throw new NullPointerException();
            } else if (times == 3) {
                throw new Exception();
            } else if (times == 4) {
                throw new RuntimeException();
            } else if (times == 5) {
                return false;
            } else {
                return true;
            }
        }
    };
    
    //利用重試器調用請求
   return  retryer.call(callable);
}

4.2.2、Spring-retry

Spring Retry則看起來更舒服了!

GitHub地址:https://github.com/spring-projects/spring-retry

有興趣的小夥伴可以具體瞭解一下,這裏不再贅述!

六、歸納總結

經過上述一個案例引出了重試的各種需要考慮的問題,以及重試常見的工具,這裏提醒大家一定要正確的進行重試!重要接口,上線前記得壓測,做好重試處理,做好監控報警配置,不要感覺自己的業務小,沒有流量,一但出現問題,你就只能"喜提"故障了!

往期熱門文章:

1、Stack Overflow上188W+程序員都關注的問題:Java到底是值傳遞還是引用傳遞?

2、Dubbo必會的18個面試題!一網打盡!

3、可以提高千倍效率的Java代碼小技巧

4、後端開發甩鍋指南!

5、答應我,別再if/else走天下了可以嗎?
在這裏插入圖片描述

【視頻福利】2T免費學習視頻,搜索或掃描上述二維碼關注微信公衆號:Java後端技術(ID: JavaITWork),和20萬人一起學Java!回覆:1024,即可免費獲取!內含SSM、Spring全家桶、微服務、MySQL、MyCat、集羣、分佈式、中間件、Linux、網絡、多線程,Jenkins、Nexus、Docker、ELK等等免費學習視頻,持續更新!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章