一、概要
阿里最近開源了分布式事務的解決方案fescar。
Fescar 是 阿里巴巴 開源的 分布式事務中間件,以 高效 并且對業務 0 侵入 的方式,解決 微服務 場景下面臨的分布式事務問題。
具體介紹可以詳見wiki地址
分布式事務一直是微服務的一個痛點。性能問題和業務的接入成本一直是困擾開發者的難題。阿里能夠實現業務0入侵并且保證性能,不得不覺得很驚訝。
二、解決的概要思路
- Transaction Coordinator(TC): 全局和分支事務的狀態的保持,驅動這個全局的提交和回滾.
- Transaction Manager(TM): 明確全局事務的范圍:開始一個全局事務,提交或者回滾一個全局事務.
- Resource Manager(RM): 管理分支事務工作資源,告訴TC,注冊這個分支事務和上報分支事務的狀態,驅動分支事務的的提交和回滾。
三、回滾方案
接下來我們重點看下fescar是如何做到對代碼無入侵的。
Phase1:
Fescar 的 JDBC 數據源代理通過對業務 SQL 的解析,把業務數據在更新前后的數據鏡像組織成回滾日志,利用 本地事務 的 ACID 特性,將業務數據的更新和回滾日志的寫入在同一個 本地事務 中提交。
這樣,可以保證:任何提交的業務數據的更新一定有相應的回滾日志存在。
Phase2:
如果決議是全局提交,此時分支事務此時已經完成提交,不需要同步協調處理(只需要異步清理回滾日志),Phase2 可以非常快速地完成。
如果決議是全局回滾,RM 收到協調器發來的回滾請求,通過 XID 和 Branch ID 找到相應的回滾日志記錄,通過回滾記錄生成反向的更新 SQL 并執行,以完成分支的回滾。
簡單來說,是通過生成自動生成回滾sql來實現自動回滾的。
三、疑問
解決方案好像很美好,但是事實上會不會有問題呢?
1.回滾sql生成的正確性如何保證
fescar的做法是類似mysql binlog的做法,把更新前后的每個字段都保存下來,用于生成回滾的sql。
2.即便回滾的sql能正確生成,如何保證一定能回滾成功?
3.并發的場景數據的一致性如何保證
前面提到的高性能的前提是不會對資源做鎖,也就是在phase1的時候就已經釋放鎖了。那么在并發場景下會不會有數據不一致的情況?
四、驗證
MySQLUndoUpdateExecutor.java
protected String buildUndoSQL() {
TableRecords beforeImage = sqlUndoLog.getBeforeImage();
List<Row> beforeImageRows = beforeImage.getRows();
if (beforeImageRows == null || beforeImageRows.size() == 0) {
throw new ShouldNeverHappenException("Invalid UNDO LOG"); // TODO
}
Row row = beforeImageRows.get(0);
StringBuffer mainSQL = new StringBuffer("UPDATE " + sqlUndoLog.getTableName() + " SET ");
StringBuffer where = new StringBuffer(" WHERE ");
boolean first = true;
for (Field field : row.getFields()) {
if (field.getKeyType() == KeyType.PrimaryKey) {
where.append(field.getName() + " = ?");
} else {
if (first) {
first = false;
} else {
mainSQL.append(", ");
}
mainSQL.append(field.getName() + " = ?");
}
}
return mainSQL.append(where).toString();
}
從代碼可以看得出來,這里采用的是類似binlog的row方式,把更新前后的值都保存下來。然后在更新的時候嘗試還原回去。
下面是我本地拿下來的一條undo_log的記錄
mysql> select * from undo_log\G
*************************** 1. row ***************************
id: 165
branch_id: 201285990
xid: 172.19.5.94:8091:201285989
rollback_info: {"branchId":201285990,"sqlUndoLogs":[{
"afterImage":{"rows":[{"fields":[{"keyType":"PrimaryKey","name":"ID","type":4,"value":3},{"keyType":"NULL","name":"COUNT","type":4,"value":98}]}],"tableName":"storage_tbl"},
"beforeImage":{"rows":[{"fields":[{"keyType":"PrimaryKey","name":"ID","type":4,"value":3},{"keyType":"NULL","name":"COUNT","type":4,"value":100}]}],
"tableName":"storage_tbl"},
"sqlType":"UPDATE",
"tableName":"storage_tbl"}],
"xid":"172.19.5.94:8091:201285989"}
log_status: 0
log_created: 2019-01-15 21:55:37
log_modified: 2019-01-15 21:55:37
ext: NULL
最終回滾的sql會生成
update storage_tbl set count=100 where count=98 and id=3;
回到我們的第二個問題:
2.即便回滾的sql能正確生成,如何保證一定能回滾成功?
如果回滾的時候count字段已經變化了,能保證回滾成功嗎?
我們來考慮一個場景:對于一個減庫存的業務,如果事務回滾,就需要把庫存+1來進行回滾。
- 但是由于我們希望能夠做到阿里所說的高性能,在phase1提交成功后就可以馬上開啟第二個事務。當第二個事務成功完成phase2的提交后,第一個事務被通知需要回滾了。這時候必然會失敗。
- 如果我們對于整個倉庫在整個分布式事務完全提交之前加鎖,性能又會大大降低。
由于這個模式是0入侵的,開發人員可能甚至在使用的過程中根本不會思考并發場景會導致的問題,直接理解成按本地事務的方式去實現,很有可能到線上才發現大問題。對于扣減的業務,回滾失敗可能問題還不大,對于增加的業務,回滾失敗很有可能就意味著公司資產的損失。
五、補充
在測試回滾的過程中發現一個比較詭異的地方,在測試demo的時候起了4個業務進程AccountServiceImpl,OrderServiceImpl,StorageServiceImpl,BusinessServiceImpl。
我在執行回滾日志的地方增加了一條日志。
AbstractUndoExecutor
undoPrepare(undoPST, undoValues, pkValue);
int affectRow = undoPST.executeUpdate();
if(affectRow>0) {
LOGGER.info("執行回滾語句成功...undoSQL:{}",undoSQL);
}else{
LOGGER.error("執行回滾語句失敗...undoSQL:{}",undoSQL);
}
結果發現所有的回滾都在全在AccountServiceImpl里面回滾了。難道還要求分布式事務涉及的所有進程的DB權限都必須共享>-<?
六、總結
總體我認為fescar的想法還是很美好,但是在實際落地過程中能否真正地解決問題我認為還是要再三經過考證才行。本人水平有限,可能對fescar理解有誤的地方,歡迎大家指正。
七、問題
最后提幾個問題,方便我們后面帶著問題往下一步步深入了解
- undo log是如何生成的?如何知道修改前后的值?
- tc和rm的通訊方式是怎樣,txid如何生成?
- rm強依賴tc,tc如何實現高可用?
- undo log這種實現方式如何解決ABA的問題?
- fescar能夠解決哪些業務場景的一致性問題?