業務冪等性技術架構體系

   現如今很多系統都會基於分佈式或微服務思想完成對系統的架構設計。那麼在這一個系統中,就會存在若干個微服務,而且服務間也會產生相互通信調用。那麼既然產生了服務調用,就必然會存在服務調用延遲或失敗的問題。當出現這種問題,服務端會進行重試等操作或客戶端有可能會進行多次點擊提交。如果這樣請求多次的話,那最終處理的數據結果就一定要保證統一,如支付場景。此時就需要通過保證業務冪等性方案來完成

   冪等性不僅僅只是一次或多次操作對資源沒有產生影響,還包括第一次操作產生影響後,以後多次操作不會再產生影響。並且冪等關注的是是否對資源產生影響,而不關注結果。

以 SQL 爲例:

select * from table where id=1

  此 SQL 無論執行多少次,雖然結果有可能出現不同,都不會對數據產生

改變,具備冪等性。

insert into table(id,name) values(1,'heima')

   此 SQL 如果 id 或 name 有唯一性約束,多次操作只允許插入一條記錄,則具備冪等性。如果不是,則不具備冪等性,多次操作會產生多條數據。

update table set score=100 where id = 1 。

此 SQL 無論執行多少次,對數據產生的影響都是相同的。具備冪等性。

冪等性設計主要從兩個維度進行考慮:空間、時間

  1. 空間:定義了冪等的範圍,如生成訂單的話,不允許出現重複下單。

  2. 時間:定義冪等的有效期。有些業務需要永久性保證冪等,如下單、支付等。而部分業務只要保證一段時間冪等即可。

同時對於冪等的使用一般都會伴隨着出現鎖的概念,用於解決併發安全問題。

業務問題拋出

  在業務開發與分佈式系統設計中,冪等性是一個非常重要的概念,有非常多的場景需要考慮冪等性的問題,尤其對於現在的分佈式系統,經常性的考慮重試、重發等操作,一旦產生這些操作,則必須要考慮冪等性問題。以交易系統、支付系統等尤其明顯,如:

  1. 當用戶購物進行下單操作,用戶操作多次,但訂單系統對於本次操作只能產生一個訂單。

  2. 當用戶對訂單進行付款,支付系統不管出現什麼問題,應該只對用戶扣一次款。

  3. 當支付成功對庫存扣減時,庫存系統對訂單中商品的庫存數量也只能扣減一次。

  4. 當對商品進行發貨時,也需保證物流系統有且只能發一次貨。

   在電商系統中還有非常多的場景需要保證冪等性。但是一旦考慮冪等後,服務邏輯務必會變的更加複雜。因此是否要考慮冪等,需要根據具體業務場景具體分析。而且在實現冪等時,還會把並行執行的功能改爲串行化,降低了執行效率。

    此處以下單減庫存爲例,當用戶生成訂單成功後,會對訂單中商品進行扣減庫存。 訂單服務會調用庫存服務進行庫存扣減。庫存服務會完成具體扣減實現。

    現在對於功能調用的設計,有可能出現調用超時,因爲出現如網絡抖動,雖然庫存服務執行成功了,但結果並沒有在超時時間內返回,則訂單服務也會進行重試。那就會出現問題,stock 對於之前的執行已經成功了,只是結果沒有按時返回。而訂單服務又重新發起請求對商品進行庫存扣減。 此時出現庫存扣減兩次的問題。對於這種問題,就需要通過冪等性進行結果。

HTTP 協議語義冪等性

   HTTP 協議有兩種方式:RESTFUL、SOA。現在對於 WEB API,更多的會使用 RESTFUL 風格定義。爲了更好的完成接口語義定義,HTTP 對於常用的四種請求方式也定義了冪等性的語義。

綜上所述,這些僅僅只是 HTTP 協議建議在基於 RESTFUL 風格定義 WEB API 時的語義,並非強制性。同時對於冪等性的實現,肯定是通過前端或服務端完成。

接口冪等

  對於冪等的考慮,主要解決兩點前後端交互與服務間交互。這兩點有時都要考慮冪等性的實現。從前端的思路解決的話,主要有三種:前端防重、PRG 模式、Token 機制。

token 機制

  通過 token 機制來保證冪等是一種非常常見的解決方案,同時也適合絕大部分場景。該方案需要前後端進行一定程度的交互來完成。

  1. 服務端提供獲取 token 接口,供客戶端進行使用。服務端生成 token 後,如果當前爲分佈式架構,將 token 存放於 redis 中,如果是單體架構,可以保存在 jvm 緩存中。

  2. 當客戶端獲取到 token 後,會攜帶着 token 發起請求。

  3. 服務端接收到客戶端請求後,首先會判斷該 token 在 redis 中是否存在。如果存在,則完成進行業務處理,業務處理完成後,再刪除 token。如果不存在,代表當前請求是重複請求,直接向客戶端返回對應標識。

但是現在有一個問題,當前是先執行業務再刪除 token。在高併發下,很有可能出現第一次訪問時 token 存在,完成具體業務操作。但在還沒有刪除 token 時,客戶端又攜帶 token 發起請求,此時,因爲 token 還存在,第二次請求也會驗證通過,執行具體業務操作。

對於這個問題的解決方案的思想就是並行變串行。會造成一定性能損耗與吞吐量降低。

第一種方案:對於業務代碼執行和刪除 token 整體加線程鎖。當後續線程再來訪問時,則阻塞排隊。

基於自定義業務流程實現 Token 機制

核心代碼如下

@Component
public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
//傳遞令牌
       RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
       if (requestAttributes != null){
          HttpServletRequest request = ((ServletRequestAttributes)
                requestAttributes).getRequest();
          if (request != null){
             Enumeration<String> headerNames = request.getHeaderNames();
             while (headerNames.hasMoreElements()){
                String headerName = headerNames.nextElement();
                if ("token".equals(headerName)){
                   String headerValue = request.getHeader(headerName);
//傳遞token
                   requestTemplate.header(headerName,headerValue);
                }
             }
          }
       }
    }
}

服務冪等

  防重表

  對於防止數據重複提交,還有一種解決方案就是通過防重表實現。防重表的實現思路也非常簡單。首先創建一張表作爲防重表,同時在該表中建立一個或多個字段的唯一索引作爲防重字段,用於保證併發情況下,數據只有一條。在向業務表中插入數據之前先向防重表插入,如果插入失敗則表示是重複數據。

對於防重表的解決方案,可能有人會說爲什麼不使用悲觀鎖。悲觀鎖在使用的過程中也是會發生死鎖的。悲觀鎖是通過鎖表的方式實現的。 假設現在一個用戶 A 訪問表 A(鎖住了表 A),然後試圖訪問表 B; 另一個用戶 B 訪問表 B(鎖住了表 B),然後試圖訪問表 A。 這時對於用戶 A 來說,由於表 B 已經被用戶 B 鎖住了,所以用戶 A 必須等到用戶 B 釋放表 B 才能訪問。 同時對於用戶 B 來說,由於表 A 已經被用戶 A 鎖住了,所以用戶 B 必須等到用戶 A 釋放表 A 才能訪問。此時死鎖就已經產生了。

MySQL 樂觀鎖

select+insert 防重提交

分佈式鎖

消息冪等

   在系統中當使用消息隊列時,無論做哪種技術選型,有很多問題是無論如何也不能忽視的,如:消息必達、消息冪等等。本章節以典型的 RabbitMQ 爲例,講解如何保證消息冪等的可實施解決方案,其他 MQ 選型均可參考。

消息重試演示

   消息隊列的消息冪等性,主要是由 MQ 重試機制引起的。因爲消息生產者將消息發送到 MQ-Server 後,MQ-Server 會將消息推送到具體的消息消費者。假設由於網絡抖動或出現異常時,MQ-Server 根據重試機制就會將消息重新向消息消費者推送,造成消息消費者多次收到相同消息,造成數據不一致。

在 RabbitMQ 中,消息重試機制是默認開啓的,但只會在 consumer 出現異常時,纔會重複推送。在使用中,異常的出現有可能是由於消費方又去調用第三方接口,由於網絡抖動而造成異常,但是這個異常有可能是暫時的。所以當消費者出現異常,可以讓其重試幾次,如果重試幾次後,仍然有異常,則需要進行數據補償。

數據補償方案:當重試多次後仍然出現異常,則讓此條消息進入死信隊列,最終進入到數據庫中,接着設置定時 job 查詢這些數據,進行手動補償。

  1. 修改 consumer 一方的配置文件

    # 消費者監聽相關配置
    listener:
      simple:
       retry:
        # 開啓消費者(程序出現異常)重試機制,默認開啓並一直重試
        enabled: true
       # 最大重試次數
        max‐attempts: 5
       # 重試間隔時間(毫秒)
       initial‐interval: 3000
  2. 設置消費異常

    當 consumer 消息監聽類中添加異常,最終接受消息時,可以發現,消息在接收五次後,最終出現異常

消息冪等解決

  要保證消息冪等性的話,其實最終要解決的就是保證多次操作,造成的影響是相同的。那麼其解決方案的思路與服務間冪等的思路其實基本都是一致的。

  1. 消息防重表,解決思路與服務間冪等的防重表一致。

  2. redis。利用 redis 防重。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/P1tz63aAYMvvY3Fe_MYi0g