Java電商秒殺系統性能優化(七)——交易優化技術之事務型消息-保證最終一致性

概述

本章延續之前緩存庫存所引入的事務不一致的問題,使用了異步化的事務型消息解決了最終一致性的問題,同時引入庫存售罄這樣的方案解決過載擊穿的問題。

本文的目標:

  • 掌握異步化事務型消息模型;
  • 掌握庫存售罄模型;

一、事務型消息原理

參考了這篇博主的文章消息隊列事務型消息原理淺析

消息隊列是普遍被應用的異步通信產品,本小節主要分爲以下幾個小結,循序漸進的對消息隊列產品事務型消息設計原理進行分析和闡述:

  • 1、消息隊列簡介
  • 2、消息隊列應用實例
  • 3、事務型消息設計方案
  • 4、事務型消息總結

1.1 消息隊列簡介

在分佈式系統架構中,消息隊列的核心職責是爲不同的應用系統提供異步通信服務,通常涉及以下三個重要角色:

  • 消息發佈者,發送消息的應用系統,負責創建消息對象並通過網絡發佈到消息 Broker,發佈的過程一般是同步的;
  • 消息 Broker,異步消息的“代理人”,負責接收並持久化消息,保證將消息投遞到指定的消息訂閱者應用系統;
  • 消息訂閱者,訂閱消息的應用系統,負責消費消息 Broker 投遞過來的消息。

在分佈式系統架構中,引入消息隊列帶來的三大核心優勢如下:

  • 1、提高核心鏈路吞吐量;
  • 2、降低應用系統之間的耦合度
  • 3、增強整體服務的高可用能力

1.2 消息隊列應用實例

分析和設計一個典型的支付應用業務邏輯,以 “賬單查詢 Case” 爲例,基本業務邏輯如下:

  • 檢索數據庫,獲取指定賬戶的賬單記錄;
  • 記錄用戶的檢索行爲,爲風險控制提供數據積累;
  • 發送短信到用戶手機,通知用戶其賬單被查詢事件;

依賴 “同步 RPC” 的設計方案 A 如下所示:
同步RPC
依賴 “異步消息隊列” 的設計方案 B 如下所示:
異步消息隊列
對比以上 A, B 兩個設計方案,引入消息隊列的設計方案 B 具有如下優勢:

  • “賬單服務” 處理 “賬單查詢 Case” 的耗時由 60 ms 縮減至 13 ms, 提高了服務的吞吐量;
  • “賬單服務” 和 “風險控制服務”、“短信通知服務” 完全解耦,如果在業務演進過程中,增加了新的下游服務,“賬單服務” 完全無需變更;
  • 當 “風險控制服務” 和 “短信通知服務” 不可用時,不會導致 “賬單服務” 不可用;

通過以上 “賬單查詢 Case” 的設計方案,可以闡明引入消息隊列給分佈式應用架構帶來的三大核心優勢;

下面繼續分析和設計 “賬單變更 Case”,基本業務邏輯如下:

  • 寫入數據庫,變更指定賬戶的賬單記錄;
  • 記錄用戶檢索行爲,爲風險控制提供數據積累;
  • 發送短信到用戶手機,通知用戶其賬戶變更金額。

與 “賬單查詢 Case” 的區別在於數據庫操作是寫入,而不再是檢索。二者的區別是數據庫的檢索不涉及數據庫事務,數據庫寫入一定會涉及到數據庫事務,按照之前的引入消息隊列設計思路,“賬單變更 Case” 的設計方案 C 如下:
賬單變更 Case” 消息隊列設計方案 C
但是這種設計方案存在一個嚴重的問題就是,如果 “發佈賬單變更消息” 發送失敗(例如網絡異常或者消息隊列服務不可用),則會導致 “記錄用戶行爲” 和 “短信通知用戶” 後續動作失敗無法完成風險控制數據積累,用戶也無法及時獲取到賬戶變更信息。

爲了解決以上設計方案 C 的嚴重問題,初步考慮先發布 “賬單變更” 消息,再做數據庫變更的設計方案,但還是無法解決 “消息發佈” 和 “數據庫事務” 可能不一致性的嚴重問題,如果消息已發佈成功過了,數據庫變更事務回滾了,就會導致用戶的賬單沒有變更,但用戶卻收到了賬戶變更短信,存在一致性漏洞的 “賬單變更 Case” 消息隊列設計方案 D 如下所示:
“賬單變更 Case” 消息隊列設計方案 D
可以梳理一下完美解決 “賬單變更 Case” 需要解決的關鍵點:

  • 必須滿足“一致性”要求,即賬單服務數據庫變更事務提交成功風險控制服務和短信通知服務收到“賬單變更”消息
  • 賬單服務數據庫變更事務回滾,風險控制服務和短信通知服務不會收到“賬單變更”消息;
  • “賬單變更”消息發佈失敗,儘量避免導致數據庫變更事務的回滾;

1.3 事務型消息設計方案

爲了解決以上描述的兩個需求,消息隊列需要提供一種特殊類型的消息**:消息隊列收到消息後不會立刻投遞消息到消息訂閱者,而是根據消息發佈者應用的數據庫事務狀態決定消息是否投遞。如果數據庫事務提交,則消息投遞到訂閱者;反之則不投遞。此類消息被命名爲 “事務型消息”**。具體設計方案如下:

事務型消息設計方案 E
事務型消息設計方案 E按照 “事務型消息設計方案 E” 的時序圖,消息發佈者和消息隊列之間增加了一個 “二階段” 消息,用來標明對應事務型消息的 “事務狀態”,消息隊列根據 “二階段” 消息決定是否投遞消息到下游消息訂閱者。應用 “事務型消息”,“賬單變更 Case” 的可行解決方案如下所示:
“賬單變更 Case” 消息隊列-事務型消息設計方案 F
按照 “賬單變更 Case” 消息隊列-事務型消息設計方案 F,可以滿足“賬單服務數據庫變更”與“異步消息是否投遞到訂閱者應用”的事務一致性需求。

消息隊列 “事務型消息” 的設計方案和實現原理基本闡明清楚了,還遺留兩個可以深究的關鍵點:

  • 爲什麼消息發佈方法需要在本地數據庫事務方法之前?
  • 如果消息隊列收不到事務型消息的二階段“提交 or 回滾” 消息,如何處理?

針對第一個關鍵點,假定方法執行時序是先執行本地數據庫事務方法,之後發佈 “事務型” 消息,那麼消息發佈失敗會導致消息發佈者本地事務回滾,這明顯是不符合預期的,因爲數據庫事務回滾的成本比較消息發佈失敗是高昂的;

針對第二個關鍵點,在分佈式網絡架構中是可能出現的,比如網絡異常、消息隊列服務短時間不可用等。這也是消息隊列提供嚴謹的 “事務型消息” 特性必須要解決的問題,如果消息隊列沒有收到 “提交 or 回滾” 回滾消息,則無法決定是否投遞消息到消息訂閱者,因此,嚴謹的 “事務型消息” 設計方案需要有一個異常場景,命名爲 “事務型消息狀態回查”,具體設計方案如下:
事務型消息回查設計方案G
需要明確的是,“事務型消息狀態回查” 只在 “提交 or 回滾消息” 失敗的場景下被觸發,屬於異常路徑。

1.4 事務型消息總結

在分佈式系統架構中,尤其是金融級業務應用的解決方案設計中,消息隊列提供 “事務型消息” 特性是必不可少的,“數據一致性” 是金融級分佈式架構的基本要求,本節通過實例逐步說明消息隊列產品支持 “事務型消息” 的必要性、設計方案和原理,定義了明確的消息隊列事務型消息的核心原理:

  • 消息隊列事務型消息基於 “二階段” 消息實現;
  • 事務型消息是否投遞與消息發佈者本地事務狀態保持一致;
  • 事務型消息狀態回查是保證了 “事務型消息” 的嚴謹性。

二、操作流水

問題本質:
沒有庫存操作流水:

對於操作型數據log data,意義是庫存扣減的操作記錄下來,便於追蹤庫存操作流水具體的狀態;根據這個狀態去做對應的回滾,或者查詢對應的狀態,使很多異步型的操作可以在操作型數據上,例如編譯人員在後臺創建的一些配置。

主業務數據:master data,ItemModel就是主業務數據,記錄了對應商品的主數據;ItemStock對應的庫存也是主業務數據;

2.1 庫存數據庫最終一致性保證

方案:

  • 引入庫存操作流水,能夠做到redis和數據庫之間最終的一致性;
  • 引入事務性消息機制;

帶來的問題是:

  • redis不可用時如何處理;
  • 扣減流水錯誤如何處理;

2.2 業務場景決定高可用技術實現

設計原則
寧可少賣,不可超賣;

方案

  • redis可以比實際數據庫中少;
  • 超時釋放;

2.3 庫存售罄

  • 庫存售罄標識;

  • 售罄後不去操作後續流程;

  • 售罄後通知各系統售罄;

  • 回補上新

2.4 後置流程

  • 銷量邏輯異步化;
  • 交易單邏輯異步化;

2.5 交易單邏輯異步化

  • 生成交易單sequence後直接異步返回;
  • 前端輪詢異步單狀態;

我們必須有交易單才能夠進行支付,因此對於交易單的時候必須要生成交易單sequence,還需要在前端進行輪詢去查詢訂單狀態,生成之後纔可以進行支付操作。

三、異步更新庫存

ItemService.java
新建一個方法
//異步更新庫存
boolean asyncDecreaseStock(Integer itemId,Integer amount);
//庫存回補
boolean increaseStock(Integer itemId,Integer amount)throws BusinessException;

ItemServiceImpl.java
@Override
public boolean asyncDecreaseStock(Integer itemId, Integer amount) {
    boolean mqResult = mqProducer.asyncReduceStock(itemId,amount);
    return mqResult;
}

@Override
public boolean increaseStock(Integer itemId, Integer amount) throws BusinessException {
    redisTemplate.opsForValue().increment("promo_item_stock_"+itemId,amount.intValue());
    return true;
}

等到前面所有的事務更新完畢,最後再發異步操作.我們異步更新了庫存的操作,並且根據對應的mqResult的狀態去回補庫存

四、事務型消息應用

什麼叫事務性呢?就是保證數據庫的事務提交,只要事務提交了就一定會保證消息發送成功。數據庫內事務回滾了,消息必定不發送,事務提交未知,消息也處於一個等待的狀態;

MqProducer.java
//事務型同步庫存扣減消息
public boolean transactionAsyncReduceStock(Integer userId,Integer itemId,Integer promoId,Integer amount,String stockLogId){
    Map<String,Object> bodyMap = new HashMap<>();
    bodyMap.put("itemId",itemId);
    bodyMap.put("amount",amount);
    bodyMap.put("stockLogId",stockLogId);

    Map<String,Object> argsMap = new HashMap<>();
    argsMap.put("itemId",itemId);
    argsMap.put("amount",amount);
    argsMap.put("userId",userId);
    argsMap.put("promoId",promoId);
    argsMap.put("stockLogId",stockLogId);

    Message message = new Message(topicName,"increase",
            JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
    TransactionSendResult sendResult = null;
    try {
        sendResult = transactionMQProducer.sendMessageInTransaction(message,argsMap);
    } catch (MQClientException e) {
        e.printStackTrace();
        return false;
    }
    if(sendResult.getLocalTransactionState() == LocalTransactionState.ROLLBACK_MESSAGE){
        return false;
    }else if(sendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE){
        return true;
    }else{
        return false;
    }

}

//判斷庫存是否扣減成功,到resi內判斷是否扣減成功
@Override
        public LocalTransactionState checkLocalTransaction(MessageExt msg) {
            //根據是否扣減庫存成功,來判斷要返回COMMIT,ROLLBACK還是繼續UNKNOWN
            String jsonString  = new String(msg.getBody());
            Map<String,Object>map = JSON.parseObject(jsonString, Map.class);
            Integer itemId = (Integer) map.get("itemId");
            Integer amount = (Integer) map.get("amount");
            String stockLogId = (String) map.get("stockLogId");
            StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
            if(stockLogDO == null){
                return LocalTransactionState.UNKNOW;
            }
            if(stockLogDO.getStatus().intValue() == 2){
                return LocalTransactionState.COMMIT_MESSAGE;
            }else if(stockLogDO.getStatus().intValue() == 1){
                return LocalTransactionState.UNKNOW;
            }
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    });
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章