在《微服務架構下的資料一緻性:概念及相關模式》中介紹了在微服務中實作資料一緻性的三種方式,包括可靠事件模式、業務補償模式、TCC模式。本文重點說一下可靠事件投遞。
1. 可靠事件模式
可靠事件模式屬于事件驅動架構,微服務完成操作後向消息代理釋出事件,關聯的微服務從消息代理訂閱到該事件進而完成相應的業務操作,關鍵在于可靠事件投遞和避免事件重複消費。
可靠事件投遞有兩個特性:1)每個服務原子性的完成業務操作和釋出事件;2)消息代理確定事件投遞至少一次(at least once)。
避免重複消費要求消費事件的服務實作幂等性。
現下流行的消息隊列都已經實作了事件的持久化和at least once的投遞模式,是以可靠事件投遞的第二條特性已經滿足,這裡就不展開。需要着重說的就是可靠時間投遞的第一條特性和避免事件重複消費,即服務的原子性和消費者的幂等性。
2. 可靠事件投遞
2.1 潛在風險
先來看一段簡單的代碼:
public void trans() {
try {
// 1. 操作資料庫
bool result = dao.update(mode1);// 操作資料庫失敗,會抛出異常
// 2. 如果第一步成功,則操作消息隊列(投遞消息)
if(result){
mq.append(mode1);// 如果mq.append方法執行失敗,會抛出異常
}
} catch (Exception e) {
roolback();// 如果發生異常,就復原
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
根據上面代碼和時序圖,理想化的情況會出現3中情況:
- 操作資料庫成功,向消息代理投遞事件也成功
- 操作資料庫失敗,不會向消息代理中投遞事件了
- 操作資料庫成功,但是向消息代理中投遞事件時失敗,向外抛出了異常,剛剛執行的更新資料庫的操作将被復原
之是以說是理想化,是因為上面的思路還是傳統的本地事務,但是在微服務架構中,可能出錯的情況更為複雜,其中最容易出現的錯誤就是網絡IO和伺服器當機:
-
微服務A投遞事件時,消息代理已經接收到消息,并進行持久化成功,即消息發送至消息代理,需要向微服務A傳回響應的時候,網絡發生異常,即4出現錯誤,代碼中的mq.append()
方法抛出異常,最終結果是事件投遞成功,但是資料庫被復原。
- 微服務A在投遞成功後,向資料庫送出commit請求之前發生當機,資料庫因為連接配接異常關閉而復原。最終結果還是事件被投遞,資料庫卻被復原。
在單伺服器情況下,上面提到的兩種異常發生概覽不大,但是在目前多伺服器、網絡情況複雜的環境中,發生的機率被大大放大,由于是異步處理,一旦問題發生,拍錯将變得更加困難。
2.2. 可靠事件投遞的兩種實作
2.2.1 本地事件表
本地事件表方法将事件和業務資料儲存在同一個資料庫中,使用一個額外的“事件恢複”服務來恢複事件,由本地事務保證更新業務和釋出事件的原子性。考慮到事件恢複可能會有一定的延時,服務在完成本地事務後可立即向消息代理釋出一個事件。
- 微服務在同一個本地事務中記錄業務資料和事件資料
- 微服務實時釋出一個事件關聯業務服務中,如果事件釋出成功立即删除記錄的事件,這樣能夠保證事件投遞的實時性。
- 事件恢複服務定時從事件表中恢複未釋出成功的事件,重新釋出,重新釋出成功後删除記錄的事件,這樣能夠保證事件一定能夠被投遞。
這樣能夠很好的解決上面提到的網絡IO異常和伺服器當機的問題,但是業務系統和事件耦合在一起,額外的事件資料操作給資料庫帶來壓力,也成為異步事件機制的一個瓶頸。
2.2.2 外部事件表
針對本地事件表出現的問題,提出外部事件表方法,将事件持久化到外部的事件系統,事件系統需提供實時事件服務以接收微服務釋出的事件,同時事件系統還需要提供事件恢複服務來确認和恢複事件。
- 業務服務在事務送出前,通過實時事件服務向事件系統請求發送事件,事件系統隻記錄事件并不真正發送
- 業務服務在送出後,通過實時事件服務向事件系統确認發送,事件得到确認後事件系統才真正釋出事件到消息代理
- 業務服務在業務復原時,通過實時事件向事件系統取消事件
- 事件系統的事件恢複服務會定期找到未确認發送的事件向業務服務查詢狀态,根據業務服務傳回的狀态決定事件是要釋出還是取消
該方式将業務系統和事件系統獨立解耦,都可以獨立伸縮。但是這種方式需要一次額外的發送操作,并且需要釋出者提供額外的查詢接口,這樣就增加了系統實作的複雜性。
3. 幂等性
3.1 事件本身具備幂等性
本身具備幂等性的事件,需要考慮執行順序。如果事件本身描述的是某個時間點的狀态,而不是變化,那麼就說這個事件具備幂等性。比如,某個時間點賬戶餘額為100,這個事件就具備幂等性;某個時間點賬戶餘額增加10,這個事件就不具備幂等性。
那麼,這種具備幂等性的事件需要考慮執行順序,比如,事件1是2016-07-16 15:07:31賬戶餘額是100,事件2是2016-07-16 25:07:31賬戶餘額是120。
- 如果事件1執行完成後執行事件2,将獲得我們期望的結果。
- 如果事件2先執行,然後又執行了事件1,那結果就不是我們期望的了。
- 如果事件1執行完成後執行事件2,此時結果是我們需要的,但由于事件重複發送,又執行了一遍事件1,此時結果也不是我們期望的了。
簡單的說,我們需要保證事件2一旦處理,事件1就不能再處理。
為了保證事件的順序,最簡單的做法就是在事件中添加時間戳。微服務記錄每個事件最後處理的時間戳,如果收到的事件的時間戳早于我們記錄的,丢棄該事件。當然,在高并發的情況下,同一時間内可能出現多個事件;事件由不同伺服器發出,時間可能不同步。這兩種情況下,可以選擇使用全局遞增序列号替換時間戳。
3.2 事件本身不具備幂等性
對于本身不具有幂等性的事件,主要思想是存儲每條事件執行結果。當收到一個事件時,我們需要根據事件的辨別ID查詢該事件是否已經執行過,如果執行過直接傳回上一次的執行結果,否則排程執行事件。
- 如果重複執行一次的開銷非常小,或者隻有很少的事件會被重複接收,可以選擇重複執行一次事件,在将事件持久化到的過程中,由于唯一鍵(辨別ID)重複,持久化過程失敗。
- 如果重複執行開銷較大,則直接使用一個過濾服務,過濾重複事件。即使用辨別ID過濾事件是否重複。如果是,直接傳回上一次執行結果。