前言
隨着現在分布式,微服務的普及,怎樣保證微服務之間的數據一致性就成了一個很大的問題,也就是怎樣解決分布式事務。不像之前系統都是單點的,操作的都是同一個數據庫,這樣系統對數據庫的操作都可以放在一個事務中,並不需要跨系統調用服務。而分布式的出現,一個大型的系統下面可能會有多個子系統模塊,這時候就會出現跨系統調用,這時就會出現一個問題,如果我本地系統事務執行正常,而我去調用系統A的時候系統A出現異常,就會導致我們兩邊的數據出現不一致的情況。下面主要講一下幾種常見的解決分布式事務的方案
XA方案/兩階段提交方案
XA方案也被稱為兩階段提交,基於2PC理論實現的。是有個事務管理器的概念,事務管理器負責協調多個數據庫的事務。在XA方案中分為兩階段:
第一階段:事務管理器首先向各個數據庫發送一個precommit預提交操作,然后由各個數據庫反饋是否可以正式提交事務
第二階段:事務管理器收到各個數據庫的反饋,如果各個數據庫都響應ok,則表明可以提交commit,如果有一個數據庫回答不ok,那么事務管理器就會發送回滾事務請求
XA方案應用場景可以在sharding-jdbc中有體現,sharding-jdbc是用來分庫分表的工具,必然就會存在多個數據庫,多個數據庫源,需要保證多個數據庫中的事務要么都成功,要么都失敗,可以通過看個demo了解一下:
@ShardingTransactionType(TransactionType.XA)
@Transactional(rollbackFor = Exception.class)
public void testTransaction() {
// step1: 先查詢庫1中的user 然后進行更新操作
// step2: 在查詢庫2中的user 然后進行更新操作,更新失敗,看兩個庫的數據會不會進行回滾
try {
// step1
User user1 = userMapper.selectUserById(1);
user1.setUsername("測試事務1");
userMapper.updateUser(user1);
// step2
User user0 = userMapper.selectUserById(2);
user0.setUsername("測試事務2");
userMapper.updateUser(user0);
int result = 1/0;
} catch (Exception e) {
log.error(e.getMessage(), e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.info("報錯,事務回滾");
}
}
我們從代碼中可以看到sharding-jdbc提供了@ShardingTransactionType注解,傳入的事務類型是XA,try{}中分別從兩個數據庫中查出了user1,user0。而更新了user1對象,user0對象。最后result=1/0拋出異常,catch(){}中則采用TransactionAspectSupport回滾操作。此時應該兩個庫中的user對象應該被回滾,網友可以去試下,寫一個sharding-jdbc針對分布式事務的demo
XA方案在企業應用中還是用的比較少,因為XA方案還是只是在單個系統中,並沒有出現跨系統間的接口調用,比較適合單塊應用里,跨多個庫的分布式事務。而且還嚴重依賴於事務管理器,一旦執行到第二個階段,事務管理器宕機了,數據庫就會一直等待commit請求,從而被阻塞住。還會出現一個問題就是:各個數據庫之間數據不一致,加入數據庫1和數據庫2收到了commit請求,而數據庫3因為網絡原因沒有收到commit請求,這時就會出現數據庫3與其他兩個庫之間的數據不一致
TCC 方案
TCC全稱是:Try、Confirm、Cancel
Try階段:是對各個服務的資源做檢查以及對資源進行鎖定和預留,比如我要支付100元的物品,首先需要檢查你的賬戶是否夠100元進行支付,如果夠則進行鎖定
Confirm階段:這個階段說的是各個服務中執行實際的操作,我支付了100元,那么我的銀行賬戶需要扣減100元,商家賬戶就需要增加100元
Cancel階段:如果任何一個服務的業務執行操作失敗,這里就需要將成功的進行回滾,我的賬戶扣減100元成功,商家賬戶增加100元失敗,那么成功扣減100元也需要進行回滾
這種方案用的也比較少,主要是后面如果出現失敗,需要自己手動進行回滾,嚴重依賴於自己寫的回滾代碼,但是一般涉及到錢,支付的場景,TCC方案用的比較多,需要嚴格保證分布式事務要么全部成功,要么全部失敗,嚴格保證錢數據的一致性,還有就是各個業務之間執行的時間比較短,不然會出現資源一直被鎖定狀態
本地消息表
本地消息表大概的意思就是在數據庫中建立一張消息表,這張消息表維護執行的事務狀態信息,大概的步驟:
1. 系統A在執行本地事務的同時,會向消息表中插入一條數據
2. 接着系統A如果需要請求系統B,這時就會發送一條消息到mq
3. 系統B接受到了系統A發送過來的mq消息,首先會在自己的本地消息表中插入一條數據,同時執行其他業務操作,如果執行成功,則會更新自己的本地消息表的狀態和更新系統A的消息表的狀態,表示自己處理成功
4. 如果系統B執行失敗,則不會更新自己的本地消息表和系統A的消息表的狀態,那么系統A會不斷的輪詢掃描自己的消息表,看那些消息狀態沒有被更新過來,消息狀態沒有被更新過來的,會再次發送mq消息給系統B消費,讓系統B再次處理
本地消息表是基於BASE理論實現的,保證了最終一致性,哪怕系統B執行失敗了,也會一直重發消息,直到系統B執行成功。適用於對一致性要求不高的場景,還需要注意的一點就是重試消費mq消息的冪等性
本地消息表主要的問題在於嚴重依賴於數據庫的消息表來管理兩邊的事務,如果碰上高並發場景,數據庫會成為一個瓶頸,擴展性不是很高,本地消息表用到的場景也比較少
可靠消息最終一致性
可靠消息最終一致性方案跟本地消息表不同的是直接把消息表砍掉,直接依賴於MQ來實現事務,像阿里的RocketMQ就支持事務,大致的意思就是:
1. 首先系統A會發送一個prepare消息到mq,如果prepare消息發送失敗,后面的操作也別執行了
2. 系統A發送prepare消息成功,開始執行本地事務,如果本地事務執行成功則告訴mq發送確認消息,如果本地事務執行失敗,則告訴mq執行回滾消息,需要將prepare消息回撤掉
3. 如果是發送確認消息,系統B接受到mq發送過來的消息,執行本地的事務,如果系統B執行本地事務失敗,自動不斷重試直到成功。如果還是一直不成功,則發送報警由人工來手工回滾和補償,只能人工手動的修數據。
4. 假設mq接受到了系統A發送過來的prepare消息,但是一直沒收到確認發送還是回滾操作,那么mq會定時的輪詢所有prepare消息來回調系統A的接口,來檢查系統A執行本地事務的時候是不是失敗了,自己在回調接口中的業務可以自定義如果本地事務執行失敗,那么發送到mq的prepare消息也進行回滾操作
這種方案在企業應用中還是使用的比較多的,一些大型互聯網企業都是依賴於mq來實現最終消息的一致性,像rocketmq本身就支持事務,提供了TransactionListener接口,我們只需要實現接口中的checkLocalTransaction的方法,也就是mq沒有收到確認發送還是回滾的操作需要回調的接口。雖然RabbitMQ、ActiveMQ沒有現成的支持事務,但是也可以基於rocketmq的思路來封裝一套類似的邏輯出來
總結
上面幾種方案需要真正落地的話,實現起來還是比較麻煩的,如果要實現分布式事務,需要考慮到的點還是比較多的,復雜度也比較大。能不用分布式事務的話就不用,如果非得使用的話,看能不能使用一些補償機制去實現,最后再結合自己公司的業務分析,來看看哪一種方案適合自己的業務