本地事務
在計算機系統中,更多的是通過關系型數據庫來控制事務,這是利用數據庫本身的事務特性來實現的,因此叫數據庫事務,由于應用主要靠關系數據庫來控制事務,而數據庫通常和應用在同一個服務器,所以基于關系型數據庫的事務又被稱為本地事務。
回顧一下數據庫事務的四大特性 ACID:
A(Atomic):原子性,構成事務的所有操作,要么都執行完成,要么全部不執行,不可能出現部分成功部分失
敗的情況。
C(Consistency):一致性,在事務執行前后,數據庫的一致性約束沒有被破壞。比如:張三向李四轉100元,
轉賬前和轉賬后的數據是正確狀態這叫一致性,如果出現張三轉出100元,李四賬戶沒有增加100元這就出現了數
據錯誤,就沒有達到一致性。
I(Isolation):隔離性,數據庫中的事務一般都是并發的,隔離性是指并發的兩個事務的執行互不干擾,一個事
務不能看到其他事務運行過程的中間狀態。通過配置事務隔離級別可以避臟讀、重復讀等問題。
D(Durability):持久性,事務完成之后,該事務對數據的更改會被持久化到數據庫,且不會被回滾。
數據庫事務在實現時會將一次事務涉及的所有操作全部納入到一個不可分割的執行單元,該執行單元中的所有操作
要么都成功,要么都失敗,只要其中任一操作執行失敗,都將導致整個事務的回滾
分布式事務
要服務與服務之間遠程協作才能完成事務操作,這種分布式系統環境下由不同的服務之間通過網絡遠程協作完成事務稱之為分布式事務
區分
本地事務如下
begin transaction;
//1.本地數據庫操作:張三減少金額
//2.本地數據庫操作:李四增加金額
commit transation;
分布式事務如下
begin transaction;
//1.本地數據庫操作:張三減少金額
//2.遠程調用:讓李四增加金額
commit transation;
設想,當遠程調用讓李四增加金額成功了,由于網絡問題遠程調用并沒有返回,此時本地事務提交失敗就回滾了張三減少金額的操作,此時張三和李四的數據就不一致了。
分布式的基本理論
如上,網絡因素成為了分布式事務的考量標準之一
CAP理論
CAP是 Consistency、Availability、Partition tolerance三個詞語的縮寫,分別表示一致性、可用性、分區容忍性。
示例:
整體執行流程如下:
1、商品服務請求主數據庫寫入商品信息(添加商品、修改商品、刪除商品)
2、主數據庫向商品服務響應寫入成功。
3、商品服務請求從數據庫讀取商品信息。
C - Consistency:
一致性是指寫操作后的讀操作可以讀取到最新的數據狀態,當數據分布在多個節點上,從任意結點讀取到的數據都是最新的狀態。
上圖中,商品信息的讀寫要滿足一致性就是要實現如下目標:
1、商品服務寫入主數據庫成功,則向從數據庫查詢新數據也成功。
2、商品服務寫入主數據庫失敗,則向從數據庫查詢新數據也失敗。
如何實現一致性?
1、寫入主數據庫后要將數據同步到從數據庫。
2、寫入主數據庫后,在向從數據庫同步期間要將從數據庫鎖定,待同步完成后再釋放鎖,以免在新數據寫入成功
后,向從數據庫查詢到舊的數據。
總結分布式系統一致性的特點如下:
1、由于存在數據同步的過程,寫操作的響應會有一定的延遲。
2、為了保證數據一致性會對資源暫時鎖定,待數據同步完成釋放鎖定資源。
3、如果請求數據同步失敗的結點則會返回錯誤信息,一定不會返回舊數據。
A - Availability :
可用性是指任何事務操作都可以得到響應結果,且不會出現響應超時或響應錯誤。
上圖中,商品信息讀取滿足可用性就是要實現如下目標:
1、從數據庫接收到數據查詢的請求則立即能夠響應數據查詢結果。
2、從數據庫不允許出現響應超時或響應錯誤。
如何實現可用性?
1、寫入主數據庫后要將數據同步到從數據庫。
2、由于要保證從數據庫的可用性,不可將從數據庫中的資源進行鎖定。
3、即時數據還沒有同步過來,從數據庫也要返回要查詢的數據,哪怕是舊數據,如果連舊數據也沒有則可以按照約定返回一個默認信息,但不能返回錯誤或響應超時。
總結分布式系統可用性的特點如下:
1、 所有請求都有響應,且不會出現響應超時或響應錯誤。
P - Partition tolerance :
通常分布式系統的各各結點部署在不同的子網,這就是網絡分區,不可避免的會出現由于網絡問題而導致結點之間通信失敗,此時仍可對外提供服務,這叫分區容忍性。
上圖中,商品信息讀寫滿足分區容忍性就是要實現如下目標:
1、主數據庫向從數據庫同步數據失敗不影響讀寫操作。
2、其一個結點掛掉不影響另一個結點對外提供服務。
如何實現分區容忍性?
1、盡量使用異步取代同步操作,例如使用異步方式將數據從主數據庫同步到從數據,這樣結點之間能有效的實現
松耦合。
2、添加從數據庫結點,其中一個從結點掛掉其它從結點提供服務。
總結分布式分區容忍性的特點如下:
1、分區容忍性分是布式系統具備的基本能力。
結論:
在所有分布式事務場景中不會同時具備CAP三個特性,因為在具備了P的前提下C和A是不能共存的。
如果要實現C則必須保證數據一致性,在數據同步的時候為防止向從數據庫查詢不一致的數據則需要將從數據庫數據鎖定,待同步完成后解鎖,如果同步失敗從數據庫要返回錯誤信息或超時信息。
如果要實現A則必須保證數據可用性,不管任何時候都可以向從數據查詢數據,則不會響應超時或返回錯誤信息。
通過分析發現在滿足P的前提下C和A存在矛盾性。
CAP有哪些組合方式?
(1)AP:
放棄一致性,追求分區容忍性和可用性。這是很多分布式系統設計時的選擇。
例如:
上邊的商品管理,完全可以實現AP,前提是只要用戶可以接受所查詢的到數據在一定時間內不是最新的即可。
通常實現AP都會保證最終一致性,后面講的BASE理論就是根據AP來擴展的,一些業務場景 比如:訂單退款,今日退款成功,明日賬戶到賬,只要用戶可以接受在一定時間內到賬即可。
(2)CP:
放棄可用性,追求一致性和分區容錯性
zookeeper其實就是追求的強一致,又比如跨行轉賬,一次轉賬請求要等待雙方銀行系統都完成整個事務才算完成。
(3)CA:
放棄分區容忍性,即不進行分區,不考慮由于網絡不通或結點掛掉的問題,則可以實現一致性和可用性。那么系統將不是一個標準的分布式系統,我們最常用的關系型數據就滿足了CA。
BASE理論
1.強一致性和最終一致性
CAP理論告訴我們一個分布式系統最多只能同時滿足一致性(Consistency)、可用性(Availability)和分區容忍性(Partition tolerance)這三項中的兩項。
其中AP在實際應用中較多,AP即舍棄一致性,保證可用性和分區容忍性,但是在實際生產中很多場景都要實現一致性,比如前邊我們舉的例子主數據庫向從數據庫同步數據,即使不要一致性,但是最終也要將數據同步成功來保證數據一致,這種一致性和CAP中的一致性不同。
CAP中的一致性要求在任何時間查詢每個結點數據都必須一致,它強調的是強一致性,
最終一致性是允許可以在一段時間內每個結點的數據不一致,但是經過一段時間每個結點的數據必須一致,它強調的是最終數據的一致性。
2.BASE 是 Basically Available(基本可用)、Soft state(軟狀態)和 Eventually consistent (最終一致性)三個短語的縮寫
BASE理論是對CAP中AP的一個擴展,通過犧牲強一致性來獲得可用性,當出現故障允許部分不可用但要保證核心功能可用,允許數據在一段時間內是不一致的,但最終達到一致狀態。
說明:
基本可用:分布式系統在出現故障時,允許損失部分可用功能,保證核心功能可用。如,電商網站交易付款出現問題了,商品依然可以正常瀏覽。
軟狀態:由于不要求強一致性,所以BASE允許系統中存在中間狀態(也叫軟狀態),這個狀態不影響系統可用性,如訂單的"支付中"、“數據同步中”等狀態,待數據最終一致后狀態改為“成功”狀態。
最終一致:最終一致是指經過一段時間后,所有節點數據都將會達到一致。如訂單的"支付中"狀態,最終會變為“支付成功”或者"支付失敗",使訂單狀態與實際交易結果達成一致,但需要一定時間的延遲、等待。
以理論為基礎,針對不同的分布式場景業界常見的解決方案有2PC,###TCC、可靠消息最終一致性、最大努力通知這幾種。
分布式事務解決方案之2PC(兩階段提交)
2PC即兩階段提交協議,是將整個事務流程分為兩個階段,準備階段(Prepare phase)、提交階段(commit phase),2是指兩個階段,P是指準備階段,C是指提交階段。
舉例:張三和李四好久不見,老友約起聚餐,飯店老板要求先買單,才能出票。這時張三和李四分別抱怨近況不如意,囊中羞澀,都不愿意請客,這時只能AA。只有張三和李四都付款,老板才能出票安排就餐。但由于張三和李四都是鐵公雞,形成了尷尬的一幕:
準備階段:老板要求張三付款,張三付款。老板要求李四付款,李四付款。
提交階段:老板出票,兩人拿票紛紛落座就餐。
例子中形成了一個事務,若張三或李四其中一人拒絕付款,或錢不夠,店老板都不會給出票,并且會把已收款退回。
整個事務過程由事務管理器和參與者組成,店老板就是事務管理器,張三、李四就是事務參與者,事務管理器負責決策整個分布式事務的提交和回滾,事務參與者負責自己本地事務的提交和回滾。
在計算機中部分關系數據庫如Oracle、MySQL支持兩階段提交協議,如下:
- 準備階段(Prepare phase):事務管理器給每個參與者發送Prepare消息,每個數據庫參與者在本地執行事務,并寫本地的Undo/Redo日志,此時事務沒有提交。
(Undo日志是記錄修改前的數據,用于數據庫回滾,Redo日志是記錄修改后的數據,用于提交事務后寫入數據文件) - 提交階段(commit phase):如果事務管理器收到了參與者的執行失敗或者超時消息時,直接給每個參與者發送回滾(Rollback)消息;否則,發送提交(Commit)消息;參與者根據事務管理器的指令執行提交或者回滾操作,并釋放事務處理過程中使用的鎖資源。
注意:必須在最后階段釋放鎖資源。
整個2PC的事務流程涉及到三個角色AP、RM、TM。AP指的是使用2PC分布式事務的應用程序;RM指的是資源管理器,它控制著分支事務;TM指的是事務管理器,它控制著整個全局事務。
1)在準備階段RM執行實際的業務操作,但不提交事務,資源鎖定;
2)在提交階段TM會接受RM在準備階段的執行回復,只要有任一個RM執行失敗,TM會通知所有RM執行回滾操作,否則,TM將會通知所有RM提交該事務。提交階段結束資源鎖釋放。
2PC方案,資源鎖需要等到兩個階段結束才釋放,性能較差。
Seata方案
Seata是由阿里中間件團隊發起的開源項目 Fescar,后更名為Seata,它是一個是開源的分布式事務框架。
傳統2PC的問題在Seata中得到了解決,它通過對本地關系數據庫的分支事務的協調來驅動完成全局事務,是工作在應用層的中間件。主要優點是性能較好,且不長時間占用連接資源,它以高效并且對業務0侵入的方式解決微服
務場景下面臨的分布式事務問題,它目前提供AT模式(即2PC)及TCC模式的分布式事務解決方案。
Seata的設計思想如下:
Seata的設計目標其一是對業務無侵入,因此從業務無侵入的2PC方案著手,在傳統2PC的基礎上演進,并解決2PC方案面臨的問題。
Seata實現2PC與傳統2PC的差別:
- 架構層次方面,傳統2PC方案的 RM 實際上是在數據庫層,RM 本質上就是數據庫自身,通過 XA 協議實現,而Seata的 RM 是以jar包的形式作為中間件層部署在應用程序這一側的。
- 兩階段提交方面,傳統2PC無論第二階段的決議是commit還是rollback,事務性資源的鎖都要保持到Phase2完成才釋放。而Seata的做法是在Phase1 就將本地事務提交,這樣就可以省去Phase2持鎖的時間,整體提高效率。
示例1:如新用戶注冊送積分
具體的執行流程如下:
- 用戶服務的 TM 向 TC 申請開啟一個全局事務,全局事務創建成功并生成一個全局唯一的XID。
- 用戶服務的 RM 向 TC 注冊 分支事務,該分支事務在用戶服務執行新增用戶邏輯,并將其納入 XID 對應全局事務的管轄。
- 用戶服務執行分支事務,向用戶表插入一條記錄。
- 邏輯執行到遠程調用積分服務時(XID 在微服務調用鏈路的上下文中傳播)。積分服務的RM 向 TC 注冊分支事務,該分支事務執行增加積分的邏輯,并將其納入 XID 對應全局事務的管轄。
- 積分服務執行分支事務,向積分記錄表插入一條記錄,執行完畢后,返回用戶服務。
- 用戶服務分支事務執行完畢。
- TM 向 TC 發起針對 XID 的全局提交或回滾決議。
- TC 調度 XID 下管轄的全部分支事務完成提交或回滾請求。
實現方案細節
1、每個RM使用DataSourceProxy連接數據庫,其目的是使用ConnectionProxy,使用數據源和數據連接代理的目的就是在第一階段將undo_log和業務數據放在一個本地事務提交,這樣就保存了只要有業務操作就一定有undo_log。
2、在第一階段undo_log中存放了數據修改前和修改后的值,為事務回滾作好準備,所以第一階段完成就已經將分支事務提交,也就釋放了鎖資源。
3、TM開啟全局事務開始,將XID全局事務id放在事務上下文中,通過feign調用也將XID傳入下游分支事務,每個分支事務將自己的Branch ID分支事務ID與XID關聯。
4、第二階段全局事務提交,TC會通知各各分支參與者提交分支事務,在第一階段就已經提交了分支事務,這里各各參與者只需要刪除undo_log即可,并且可以異步執行,第二階段很快可以完成。
5、第二階段全局事務回滾,TC會通知各各分支參與者回滾分支事務,通過 XID 和 Branch ID 找到相應的回滾日志,通過回滾日志生成反向的 SQL 并執行,以完成分支事務回滾到之前的狀態,如果回滾失敗則會重試回滾操作。
代碼實現
dtx-seata-demo-bank1
dtx-seata-demo-bank1實現如下功能:
1、張三賬戶減少金額,開啟全局事務。
2、遠程調用bank2向李四轉賬。
(1)DAO
@Mapper
@Component
public interface AccountInfoDao {
//更新賬戶金額
@Update("update account_info set account_balance = account_balance + #{amount} where
account_no = #{accountNo}")
int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double
amount);
}
(2)FeignClient
遠程調用bank2的客戶端
@FeignClient(value = "seata‐demo‐bank2",fallback = Bank2ClientFallback.class)
public interface Bank2Client {
@GetMapping("/bank2/transfer")
String transfer(@RequestParam("amount") Double amount);
}
@Component
public class Bank2ClientFallback implements Bank2Client{
@Override
public String transfer(Double amount) {
return "fallback";
}
}
(3)Service
@Service
public class AccountInfoServiceImpl implements AccountInfoService {
private Logger logger = LoggerFactory.getLogger(AccountInfoServiceImpl.class);
@Autowired
AccountInfoDao accountInfoDao;
@Autowired
Bank2Client bank2Client;
//張三轉賬
@Override
@GlobalTransactional
@Transactional
public void updateAccountBalance(String accountNo, Double amount) {
logger.info("******** Bank1 Service Begin ... xid: {}" , RootContext.getXID());
//張三扣減金額
accountInfoDao.updateAccountBalance(accountNo,amount*‐1);
//向李四轉賬
String remoteRst = bank2Client.transfer(amount);
//遠程調用失敗
if(remoteRst.equals("fallback")){
throw new RuntimeException("bank1 下游服務異常");
}
//人為制造錯誤
if(amount==3){
throw new RuntimeException("bank1 make exception 3");
}
}
}
將@GlobalTransactional注解標注在全局事務發起的Service實現方法上,開啟全局事務:
GlobalTransactionalInterceptor會攔截@GlobalTransactional注解的方法,生成全局事務ID(XID),XID會在整個分布式事務中傳遞。
在遠程調用時,spring-cloud-alibaba-seata會攔截Feign調用將XID傳遞到下游服務。
(6)Controller
@RestController
public class Bank1Controller {
@Autowired
AccountInfoService accountInfoService;
//轉賬
@GetMapping("/transfer")
public String transfer(Double amount){
accountInfoService.updateAccountBalance("1",amount);
return "bank1"+amount;
}
}
dtx-seata-demo-bank2
dtx-seata-demo-bank2實現如下功能:
1、李四賬戶增加金額。
dtx-seata-demo-bank2在本賬號事務中作為分支事務不使用@GlobalTransactional。
(1)DAO
@Mapper
@Component
public interface AccountInfoDao {
//向李四轉賬
@Update("UPDATE account_info SET account_balance = account_balance + #{amount} WHERE
account_no = #{accountNo}")
int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double
amount);
}
(2)Service
@Service
public class AccountInfoServiceImpl implements AccountInfoService {
private Logger logger = LoggerFactory.getLogger(AccountInfoServiceImpl.class);
@Autowired
AccountInfoDao accountInfoDao;
@Override
@Transactional
public void updateAccountBalance(String accountNo, Double amount) {
logger.info("******** Bank2 Service Begin ... xid: {}" , RootContext.getXID());
//李四增加金額
accountInfoDao.updateAccountBalance(accountNo,amount);
//制造異常
if(amount==2){
throw new RuntimeException("bank1 make exception 2");
}
}
}
(3)Controller
@RestController
public class Bank2Controller {
@Autowired
AccountInfoService accountInfoService;
@GetMapping("/transfer")
public String transfer(Double amount){
accountInfoService.updateAccountBalance("2",amount);
return "bank2"+amount;
}
}
小結
可以看出適用seata還是比較簡單的,Seata實現2PC的兩種2PC方案,由于Seata的0侵入性并且解決了傳統2PC長期鎖資源的問題,所以推薦采用Seata實現2PC。
Seata實現2PC要點:
1、全局事務開始使用 @GlobalTransactional標識 。
2、每個本地事務方案仍然使用@Transactional標識。
3、每個數據都需要創建undo_log表,此表是seata保證本地事務一致性的關鍵。但也帶來了性能損耗,需要考慮場景
分布式事務解決方案之TCC
TCC是Try、Confirm、Cancel三個詞語的縮寫,TCC要求每個分支事務實現三個操作:預處理Try、確認Confirm、撤銷Cancel。
Try操作做業務檢查及資源預留,
Confirm做業務確認操作,
Cancel實現一個與Try相反的操作即回滾操作。
詳細說明
- Try 階段是做業務檢查(一致性)及資源預留(隔離),此階段僅是一個初步操作,它和后續的Confirm 一起才能真正構成一個完整的業務邏輯。
- Confirm 階段是做確認提交,Try階段所有分支事務執行成功后開始執行 Confirm。通常情況下,采用TCC則認為 Confirm階段是不會出錯的。
即假設:只要Try成功,Confirm一定成功。若Confirm階段真的出錯了,需引
入重試機制或人工處理。 - Cancel 階段是在業務執行錯誤需要回滾的狀態下執行分支事務的業務取消,預留資源釋放。通常情況下,采用TCC則認為Cancel階段也是一定成功的。若Cancel階段真的出錯了,需引入重試機制或人工處理。
TCC需要注意三種異常處理分別是空回滾、冪等、懸掛:
空回滾:
在沒有調用 TCC 資源 Try 方法的情況下,調用了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然后直接返回成功。
出現原因是當一個分支事務所在服務宕機或網絡異常,分支事務調用記錄為失敗,這個時候其實是沒有執行Try階段,當故障恢復后,分布式事務進行回滾則會調用二階段的Cancel方法,從而形成空回滾。
解決思路是關鍵就是要識別出這個空回滾。思路很簡單就是需要知道一階段是否執行,如果執行了,那就是正常回滾;如果沒執行,那就是空回滾。前面已經說過TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分布式事務調用鏈條。再額外增加一張分支事務記錄表,其中有全局事務 ID 和分支事務 ID,第一階段 Try 方法里會插入一條記錄,表示一階段執行了。Cancel 接口里讀取該記錄,如果該記錄存在,則正?;貪L;如果該記錄不存在,則是空回滾。
冪等:
通過前面介紹已經了解到,為了保證TCC二階段提交重試機制不會引發數據不一致,要求 TCC 的二階段 Try、Confirm 和 Cancel 接口保證冪等,這樣不會重復使用或者釋放資源。如果冪等控制沒有做好,很有可能導致數據不一致等嚴重問題。
解決思路在上述“分支事務記錄”中增加執行狀態,每次執行前都查詢該狀態。
懸掛:
懸掛就是對于一個分布式事務,其二階段 Cancel 接口比 Try 接口先執行。
出現原因是在 RPC 調用分支事務try時,先注冊分支事務,再執行RPC調用,如果此時 RPC 調用的網絡發生擁堵,通常 RPC 調用是有超時時間的,RPC 超時以后,TM就會通知RM回滾該分布式事務,可能回滾完成后,RPC 請求才到達參與者真正執行,而一個 Try 方法預留的業務資源,只有該分布式事務才能使用,該分布式事務第一階段預留的業務資源就再也沒有人能夠處理了,對于這種情況,我們就稱為懸掛,即業務資源預留后沒法繼續處理。
解決思路是如果二階段執行完成,那一階段就不能再繼續執行。在執行一階段事務時判斷在該全局事務下,“分支事務記錄”表中是否已經有二階段事務記錄,如果有則不執行Try。
目前市面上的TCC框架眾多比如下面這幾種
框架名稱 | Gitbub地址 |
---|---|
tcc-transaction | https://github.com/changmingxie/tcc-transaction |
Hmily | https://github.com/yu199195/hmily |
ByteTCC | https://github.com/liuyangming/ByteTCC |
EasyTransaction | https://github.com/QNJR-GROUP/EasyTransaction |
雖然Seata也支持TCC,但Seata的TCC模式對Spring Cloud并沒有提供支持。
小結
如果拿TCC事務的處理流程與2PC兩階段提交做比較,2PC通常都是在跨庫的DB層面,而TCC則在應用層面的處理,需要通過業務邏輯來實現。這種分布式事務的實現方式的優勢在于,可以讓應用自己定義數據操作的粒度,使得降低鎖沖突、提高吞吐量成為可能。
而不足之處則在于對應用的侵入性非常強,業務邏輯的每個分支都需要實現try、confirm、cancel三個操作。此外,其實現難度也比較大,需要按照網絡狀態、系統故障等不同的失敗原因實現不同的回滾策略。
分布式事務解決方案之可靠消息最終一致性
可靠消息最終一致性方案是指當事務發起方執行完成本地事務后并發出一條消息,事務參與方(消息消費者)一定能夠接收消息并處理事務成功,此方案強調的是只要消息發給事務參與方最終事務要達到一致。
可靠消息最終一致性方案要解決以下幾個問題:
1.本地事務與消息發送的原子性問題
本地事務與消息發送的原子性問題即:事務發起方在本地事務執行成功后消息必須發出去,否則就丟棄消息。即實現本地事務和消息發送的原子性,要么都成功,要么都失敗。本地事務與消息發送的原子性問題是實現可靠消息最終一致性方案的關鍵問題。
2、事務參與方接收消息的可靠性
事務參與方必須能夠從消息隊列接收到消息,如果接收消息失敗可以重復接收消息。
3、消息重復消費的問題
由于網絡2的存在,若某一個消費節點超時但是消費成功,此時消息中間件會重復投遞此消息,就導致了消息的重復消費。
要解決消息重復消費的問題就要實現事務參與方的方法冪等性
解決方案
方案一:本地消息表方案
此方案的核心是通過本地事務保證數據業務操作和消息的一致性,然后通過定時任務將消息發送至消息中間件,待確認消息發送給消費方成功再將消息刪除。
示例:
用戶服務和積分服務,用戶服務負責添加用戶,積分服務負責增加積分。
交互流程如下:
1、用戶注冊
用戶服務在本地事務新增用戶和增加 ”積分消息日志“。(用戶表和消息表通過本地事務保證一致)
偽代碼
begin transaction;
//1.新增用戶
//2.存儲積分消息日志
commit transation;
這種情況下,本地數據庫操作與存儲積分消息日志處于同一個事務中,本地數據庫操作與記錄消息日志操作具備原
子性。
2、定時任務掃描日志
如何保證將消息發送給消息隊列呢?
經過第一步消息已經寫到消息日志表中,可以啟動獨立的線程,定時對消息日志表中的消息進行掃描并發送至消息中間件,在消息中間件反饋發送成功后刪除該消息日志,否則等待定時任務下一周期重試。
3、消費消息
如何保證消費者一定能消費到消息呢?
這里可以使用MQ的ack(即消息確認)機制,消費者監聽MQ,如果消費者接收到消息并且業務處理完成后向MQ發送ack(即消息確認),此時說明消費者正常消費消息完成,MQ將不再向消費者推送消息,否則消費者會不斷重
試向消費者來發送消息。
積分服務接收到”增加積分“消息,開始增加積分,積分增加成功后向消息中間件回應ack,否則消息中間件將重復投遞此消息。
由于消息會重復投遞,積分服務的”增加積分“功能需要實現冪等性。
方案二:RocketMQ事務消息方案
執行流程如下:
還以注冊送積分的例子來描述 整個流程。
Producer 即MQ發送方,本例中是用戶服務,負責新增用戶。MQ訂閱方即消息消費方,本例中是積分服務,負責新增積分。
1、Producer 發送事務消息
Producer (MQ發送方)發送事務消息至MQ Server,MQ Server將消息狀態標記為Prepared(預備狀態),注意此時這條消息消費者(MQ訂閱方)是無法消費到的。
本例中,Producer 發送 ”增加積分消息“ 到MQ Server。
2、MQ Server回應消息發送成功MQ Server接收到Producer 發送給的消息則回應發送成功表示MQ已接收到消息。
3、Producer 執行本地事務
Producer 端執行業務代碼邏輯,通過本地數據庫事務控制。
本例中,Producer 執行添加用戶操作。
4、消息投遞
若Producer 本地事務執行成功則自動向MQServer發送commit消息,MQ Server接收到commit消息后將”增加積分消息狀態標記為可消費,此時MQ訂閱方(積分服務)即正常消費消息;
若Producer 本地事務執行失敗則自動向MQServer發送rollback消息,MQ Server接收到rollback消息后 將刪除”增加積分消息“ 。
MQ訂閱方(積分服務)消費消息,消費成功則向MQ回應ack,否則將重復接收消息。這里ack默認自動回應,即程序執行正常則自動回應ack。
5、事務回查
如果執行Producer端本地事務過程中,執行端掛掉,或者超時,MQ Server將會不停的詢問同組的其他 Producer來獲取事務執行狀態,這個過程叫事務回查。MQ Server會根據事務回查結果來決定是否投遞消息。
以上主干流程已由RocketMQ實現,對用戶側來說,用戶需要分別實現本地事務執行以及本地事務回查方法,因此只需關注本地事務的執行狀態即可。
RoacketMQ提供RocketMQLocalTransactionListener接口:
public interface RocketMQLocalTransactionListener {
/**
‐ 發送prepare消息成功此方法被回調,該方法用于執行本地事務
‐ @param msg 回傳的消息,利用transactionId即可獲取到該消息的唯一Id
‐ @param arg 調用send方法時傳遞的參數,當send時候若有額外的參數可以傳遞到send方法中,這里能獲取到
‐ @return 返回事務狀態,COMMIT:提交 ROLLBACK:回滾 UNKNOW:回調
*/
RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);
/**
‐ @param msg 通過獲取transactionId來判斷這條消息的本地事務執行狀態
‐ @return 返回事務狀態,COMMIT:提交 ROLLBACK:回滾 UNKNOW:回調
*/
RocketMQLocalTransactionState checkLocalTransaction(Message msg);
}
發送事務消息:
以下是RocketMQ提供用于發送事務消息的API:
TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
//設置TransactionListener實現
producer.setTransactionListener(transactionListener);
//發送事務消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
小結
可靠消息最終一致性就是保證消息從生產方經過消息中間件傳遞到消費方的一致性,本案例使用了RocketMQ作為消息中間件,RocketMQ主要解決了兩個功能:
1、本地事務與消息發送的原子性問題。
2、事務參與方接收消息的可靠性。
適用場景
可靠消息最終一致性事務適合執行周期長且實時性要求不高的場景。引入消息機制后,同步的事務操作變為基于消息執行的異步操作, 避免了分布式事務中的同步阻塞操作的影響,并實現了兩個服務的解耦。
分布式事務解決方案之最大努力通知
充值示例:
交互流程:
1、賬戶系統調用充值系統接口
2、充值系統完成支付處理向賬戶系統發起充值結果通知
若通知失敗,則充值系統按策略進行重復通知
3、賬戶系統接收到充值結果通知修改充值狀態。
4、賬戶系統未接收到通知會主動調用充值系統的接口查詢充值結果。
通過上邊的例子我們總結最大努力通知方案的目標:
目標:發起通知方通過一定的機制最大努力將業務處理結果通知到接收方。
具體包括:
1、有一定的消息重復通知機制。
因為接收通知方可能沒有接收到通知,此時要有一定的機制對消息重復通知。
2、消息校對機制。
如果盡最大努力也沒有通知到接收方,或者接收方消費消息后要再次消費,此時可由接收方主動向通知方查詢消息信息來滿足需求。
最大努力通知與可靠消息一致性的區別?
1、解決方案思想不同
可靠消息一致性,發起通知方需要保證將消息發出去,并且將消息發到接收通知方,消息的可靠性關鍵由發起通知方來保證。
最大努力通知,發起通知方盡最大的努力將業務處理結果通知為接收通知方,但是可能消息接收不到,此時需要接收通知方主動調用發起通知方的接口查詢業務處理結果,通知的可靠性關鍵在接收通知方。
2、兩者的業務應用場景不同
可靠消息一致性關注的是交易過程的事務一致,以異步的方式完成交易。
最大努力通知關注的是交易后的通知事務,即將交易結果可靠的通知出去。
3、技術解決方向不同
可靠消息一致性要解決消息從發出到接收的一致性,即消息發出并且被接收到。
最大努力通知無法保證消息從發出到接收的一致性,只提供消息接收的可靠性機制。可靠機制是,最大努力的將消息通知給接收方,當消息無法被接收方接收時,由接收方主動查詢消息(業務處理結果)
實現方案一:
采用MQ的ack機制就可以實現最大努力通知。
本方案是利用MQ的ack機制由MQ向接收通知方發送通知,流程如下:
1、發起通知方將通知發給MQ。
使用普通消息機制將通知發給MQ。
注意:如果消息沒有發出去可由接收通知方主動請求發起通知方查詢業務執行結果。
2、接收通知方監聽 MQ。
3、接收通知方接收消息,業務處理完成回應ack。
4、接收通知方若沒有回應ack則MQ會重復通知。
MQ會按照間隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知間隔 (如果MQ采用rocketMq,在broker中可進行配置),直到達到通知要求的時間窗口上限。
5、接收通知方可通過消息校對接口來校對消息的一致性。
實現方案二
案也是利用MQ的ack機制,與方案1不同的是應用程序向接收通知方發送通知,如下圖:
交互流程如下:
1、發起通知方將通知發給MQ。
使用可靠消息一致方案中的事務消息保證本地事務與消息的原子性,最終將通知先發給MQ。
2、通知程序監聽 MQ,接收MQ的消息。
方案1中接收通知方直接監聽MQ,方案2中由通知程序監聽MQ。
通知程序若沒有回應ack則MQ會重復通知。
3、通知程序通過互聯網接口協議(如http、webservice)調用接收通知方案接口,完成通知。
通知程序調用接收通知方案接口成功就表示通知成功,即消費MQ消息成功,MQ將不再向通知程序投遞通知消息。
4、接收通知方可通過消息校對接口來校對消息的一致性。
方案1和方案2的不同點:
1、方案1中接收通知方與MQ接口,即接收通知方案監聽 MQ,此方案主要應用與內部應用之間的通知。
2、方案2中由通知程序與MQ接口,通知程序監聽MQ,收到MQ的消息后由通知程序通過互聯網接口協議調用接收通知方。此方案主要應用于外部應用之間的通知,例如支付寶、微信的支付結果通知。
小結
最大努力通知方案是分布式事務中對一致性要求最低的一種,適用于一些最終一致性時間敏感度低的業務;
最大努力通知方案需要實現如下功能:
1、消息重復通知機制。
2、消息校對機制。
分布式事務綜合分析
以P2P的相關業務流程案例分析如下
一. 用戶注冊為例
針對注冊業務,如果用戶與賬號信息不一致,則會導致嚴重問題,因此該業務對一致性要求較為嚴格,即當用戶服務和賬號服務任意一方出現問題都需要回滾事務。
1、采用可靠消息一致性方案
可靠消息一致性要求只要消息發出,事務參與者接到消息就要將事務執行成功,不存在回滾的要求,所以不適用。
2、采用最大努力通知方案
最大努力通知表示發起通知方執行完本地事務后將結果通知給事務參與者,即使事務參與者執行業務處理失敗發起通知方也不會回滾事務,所以不適用。
3、采用Seata實現2PC
在用戶中心發起全局事務,統一賬戶服務為事務參與者,用戶中心和統一賬戶服務只要有一方出現問題則全局事務回滾,符合要求。
實現方法如下:
1、用戶中心添加用戶信息,開啟全局事務
2、統一賬號服務添加賬號信息,作為事務參與者
3、其中一方執行失敗Seata對SQL進行逆操作刪除用戶信息和賬號信息,實現回滾。
4、采用TCC
TCC也可以實現用戶中心和統一賬戶服務只要有一方出現問題則全局事務回滾,符合要求。
實現方法如下:
1、用戶中心
try:添加用戶,狀態為不可用
confirm:更新用戶狀態為可用
cancel:刪除用戶
2、統一賬號服務
try:添加賬號,狀態為不可用
confirm:更新賬號狀態為可用
cancel:刪除賬號
二. 存管開戶案例
P2P業務必須讓銀行存管資金,用戶的資金在銀行存管系統的賬戶中,而不在P2P平臺中,因此用戶要在銀行存管系統開戶。
用戶向用戶中心提交開戶資料,用戶中心生成開戶請求號并重定向至銀行存管系統開戶頁面。用戶設置存管密碼并確認開戶后,銀行存管立即返回“請求已受理”。在某一時刻,銀行存管系統處理完該開戶請求后,將調用回調地址通知處理結果,若通知失敗,則按一定策略重試通知。同時,銀行存管系統應提供開戶結果查詢的接口,供用戶中
心校對結果。
分析:
P2P平臺的用戶中心與銀行存管系統之間屬于跨系統交互,銀行存管系統屬于外部系統,用戶中心無法干預銀行存管系統,所以用戶中心只能在收到銀行存管系統的業務處理結果通知后積極處理,開戶后的使用情況完全由用戶中心來控制。
1、采用Seata實現2PC
需要侵入銀行存管系統的數據庫,由于它的外部系統,所以不適用。
2、采用Hmily實現TCC
TCC侵入性更強,所以不適用。
3、基于MQ的可靠消息一致性
如果讓銀行存管系統監聽 MQ則不合適 ,因為它的外部系統。
如果銀行存管系統將消息發給MQ用戶中心監聽MQ是可以的,但是由于相對銀行存管系統來說用戶中心屬于外部系統,銀行存管系統是不會讓外部系統直接監聽自己的MQ的,基于MQ的通信協議也不方便外部系統間的交互,所以本方案不合適。
4、最大努力通知方案
銀行存管系統內部使用MQ,銀行存管系統處理完業務后將處理結果發給MQ,由銀行存管的通知程序專門發送通知,并且采用互聯網協議通知給第三方系統(用戶中心)。
下圖中發起通知即銀行存管系統:
三. 滿標審核案例
業務流程
在借款人標的募集夠所有的資金后,P2P運營管理員審批該標的,觸發放款,并開啟還款流程。
管理員對某標的滿標審批通過,交易中心修改標的狀態為“還款中”,同時要通知還款服務生成還款計劃。
解決方案分析
生成還款計劃是一個執行時長較長的業務,不建議阻塞主業務流程,此業務對一致性要求較低。
1、采用Seata實現2PC
Seata在事務執行過程會進行數據庫資源鎖定,由于事務執行時長較長會將資源鎖定較長時間,所以不適用。
2、采用TCC
本需求對業務一致性要求較低,因為生成還款計劃的時長較長,所以不要求交易中心修改標的狀態為“還款中”就立即生成還款計劃 ,所以本方案不適用。
3、基于MQ的可靠消息一致性
滿標審批通過后由交易中心修改標的狀態為“還款中”并且向還款服務發送消息,還款服務接收到消息開始生成還款計劃,基本于MQ的可靠消息一致性方案適用此場景 。
4、最大努力通知方案
滿標審批通過后由交易中心向還款服務發送通知要求生成還款計劃,還款服務并且對外提供還款計劃生成結果校對接口供其它服務查詢,最大努力通知方案也適用本場景 。
各種方案的優缺點:
2PC 最大的詬病是一個阻塞協議。RM在執行分支事務后需要等待TM的決定,此時服務會阻塞并鎖定資源。由于其阻塞機制和最差時間復雜度高, 因此,這種設計不能適應隨著事務涉及的服務數量增加而擴展的需要,很難用于并發較高以及子事務生命周期較長 (long-running transactions) 的分布式服務中。
拿TCC事務的處理流程與2PC兩階段提交做比較,2PC通常都是在跨庫的DB層面,而TCC則在應用層面的處理,需要通過業務邏輯來實現。這種分布式事務的實現方式的優勢在于,可以讓應用自己定義數據操作的粒度,使得降低鎖沖突、提高吞吐量成為可能。而不足之處則在于對應用的侵入性非常強,業務邏輯的每個分支都需要實現try、confirm、cancel三個操作。此外,其實現難度也比較大,需要按照網絡狀態、系統故障等不同的失敗原因實現不同的回滾策略。典型的使用場景:滿,登錄送優惠券等。
可靠消息最終一致性事務適合執行周期長且實時性要求不高的場景。引入消息機制后,同步的事務操作變為基于消息執行的異步操作, 避免了分布式事務中的同步阻塞操作的影響,并實現了兩個服務的解耦。典型的使用場景:注冊送積分,登錄送優惠券等。
最大努力通知是分布式事務中要求最低的一種,適用于一些最終一致性時間敏感度低的業務;允許發起通知方處理業務失敗,在接收通知方收到通知后積極進行失敗處理,無論發起通知方如何處理結果都會不影響到接收通知方的后續處理;發起通知方需提供查詢執行情況接口,用于接收通知方校對結果。典型的使用場景:銀行通知、支付結果通知等。
無論是數據庫層的XA、還是應用層TCC、可靠消息、最大努力通知等方案,都沒有完美解決分布式事務問題,它們不過是各自在性能、一致性、可用性等方面做取舍,尋求某些場景偏好下的權衡。