資料一致性(一) - 介面呼叫一致性

flyleft發表於2018-12-04

github

場景

客戶端呼叫A服務的介面,A服務介面中又呼叫了B服務。 如果A服務和B服務都執行成功,則成功,並且二者事務都應提交; 如果A服務或B服務任意一個失敗,則失敗,且二者事務都不執行或回滾。 因為網路請求的不可靠性,如果A呼叫B失敗,可能:1. B沒有接收到網路請求;2. B收到後執行失敗; 3. B執行成功,請求返回時異常。 4. B呼叫超時,B可能執行成功也可能失敗。因此當A呼叫B時,可能出現不一致。

介面呼叫幾種方式

方式一:feign直接呼叫

A服務介面:

try {
    業務程式碼A
    feign呼叫B服務介面
    事務提交
} catch (Exception e) {
    事務回滾
}
複製程式碼

B服務介面:

try {
    業務程式碼B
    事務提交
    //此時介面狀態碼2XX
} catch (Exception e) {
    事務回滾
    //此時介面狀態碼非2XX
}
複製程式碼

一致性分析:

  1. feign呼叫B服務介面成功,狀態碼為2XX。則B服務事務已經提交,A進行事務提交。 若A事務提交成功,則一致; 若A事務提交失敗但此時B中事務已經提交,則不一致。
  2. B沒有接收到網路請求。B未被執行,feign呼叫丟擲異常,A事務不進行提交,進入回滾,資料一致。
  3. B執行成功,請求返回時異常。B事務已經提交,feign呼叫B服務介面異常,A事務回滾,資料不一致。
  4. B呼叫超時。可能為B沒有接收到網路請求,也可能B執行成功,請求返回時異常,也可能B收到請求響應緩慢。一致性狀態不確定,都有可能。

方式二:基於可靠訊息

服務A業務程式碼執行完後傳送訊息到訊息佇列,如rabbitmq,並用ack等方式確保傳送成功; B服務接收消費成功後,手動確認訊息,kafka則用手動提交位移的方式。

A服務生產者:

try {
    業務程式碼A
    ack = rabbitmqProducer.sendAndGetAck
    if !ack {
        //傳送失敗可以設定自動重試,不重試就丟擲異常
        throws new RabbitmqSendException
    }
    事務提交
} catch (Exception e) {
    //事務回滾
}
複製程式碼

B服務訊息佇列消費端一:

//
try {
    業務程式碼B
    channel.basicAck手動確認//kafka則手動提交位移
    事務提交
    //此時介面狀態碼2XX
} catch (Exception e) {
    事務回滾
    //此時介面狀態碼非2XX
}
複製程式碼

B服務訊息佇列消費端二:

//
try {
    業務程式碼B
    事務提交
    //此時介面狀態碼2XX
} catch (Exception e) {
    事務回滾
    //此時介面狀態碼非2XX
}
channel.basicAck手動確認//kafka則手動提交位移
複製程式碼

一致性分析:

  1. A服務傳送訊息到訊息佇列成功,卻提交事務失敗,出現資料不一致。
  2. B消費端一:手動確認後,訊息從訊息佇列刪除,B事務提交失敗,出現資料不一致。
  3. B消費端二:B事務提交成功,手動確認失敗,可能會重複收到該條訊息,出現不一致。 此時可在訊息中新增uuid,服務B收到訊息後根據uuid進行一次去重再處理等方式來實現冪等性。

方式三:預執行+確認+回查,類似TCC

A服務需要插入一個表transaction_record記錄呼叫狀態,提供給B服務回撥。

A服務業務介面:

String uuid = generateUUID()//生成一個uuid
try{
    feign.preCreate(uuid,...)//feign呼叫B預執行,比如B服務為建立訂單服務,預建立一個訂單,但狀態為待確認
    業務程式碼A
    將uuid插入transaction_record表中
    事務提交
}catch (Exception e) {
    事務回滾
    feign.cancel(uuid)//feign呼叫B取消,比如B服務為建立訂單服務,設定該訂單狀態為取消
}

feign.confirm(uuid)//feign呼叫B確認,比如B服務為建立訂單服務,設定該訂單狀態為確認,此時訂單可用 
複製程式碼

A服務服務回查介面,提供給B服務回查狀態:

get /v1/transaction/{uuid}

從transaction_record表中查詢,有則返回確認,沒有則返回取消
複製程式碼

B服務需要提供預執行、確認、取消介面。若預執行後遲遲沒有執行確認或取消,則B向A回查,根據結果確認或取消。

一致性分析:

  1. A中preCreate執行異常。應丟擲異常,不再執行業務程式碼和事件表插入uuid,去執行cancel。若B執行則狀態也為未確認,不影響一致性; 若cancel也執行失敗,比如此時B掛掉,B重啟後應去呼叫服務A的回查介面,確定狀態。狀態一致。
  2. 業務程式碼A執行失敗,事務回滾,feign呼叫B取消。若取消成功,則狀態一致;若取消失敗,應丟擲異常,confirm不再執行。狀態一致。
  3. 業務程式碼A執行成功,事務提交失敗,執行事務回滾。若回滾失敗,丟擲異常,不再執行cancel和confirm,B會執行超時回查確定狀態;若回滾成功,則執行取消。狀態一致。
  4. A事務提交成功,確認失敗(比如A執行確認時,服務A或者B剛好掛掉)。則服務B超時回查,發現uuid存在,修改狀態為確認。狀態一致。
  5. 預處理完成後,去執行業務程式碼,若業務程式碼執行緩慢,B服務認為超時,則服務B超時回查,若A的事務還未提交,A的回查介面返回取消, 則B被取消,A卻提交了事務,此時出現事務狀態的不一致。此時可以通過設定B稍微大的超時時間來調整,可以讓服務A在預處理的feign呼叫時傳入期待的超時時間。

總結

以上是介面間呼叫的幾種方式,這裡只提供一種大概的思路,應用時可以自己優化,同步傳送也可修改為非同步+重試等方式。 若一致性要求可採用方式三,uuid+插入表也可採用其他的方式實現。為了提高一致性,介面呼叫也要儘量是冪等的,可通過業務邏輯的冪等性或 uuid實現。

相關文章