【SpirngCloud】分布式事務(wù)解決方案
1. 理論基礎(chǔ)
1.1 CAP 理論
1998年,加州大學(xué)的計算機科學(xué)家 Eric Brewer 提出,分布式系統(tǒng)有三個指標:
- Consistency(一致性):用戶訪問分布式系統(tǒng)中的任意節(jié)點,得到的數(shù)據(jù)必須一致
- Availability(可用性):用戶訪問集群中的任意健康節(jié)點,必須能得到響應(yīng),而不是超時或拒絕
- Partition tolerance(分區(qū)容錯性)
- Partition(分區(qū)):因為網(wǎng)絡(luò)故障或其它原因?qū)е路植际较到y(tǒng)中的部分節(jié)點與其它節(jié)點失去連接,形成獨立分區(qū)。
- Tolerance(容錯):在集群出現(xiàn)分區(qū)時,整個系統(tǒng)也要持續(xù)對外提供服務(wù)
分布式無法同時滿足這三個指標,這個結(jié)論就叫做 CAP理論
。凡是分布式系統(tǒng)就一定會出現(xiàn)分區(qū),集群又必須對外提供服務(wù),那么就認為 P
一定要實現(xiàn),那么就 C
和 A
之間就要做出取舍,要嘛 CP
要嘛 AP
。
1.2 BASE 理論
BASE
理論是對 CAP
的一種解決思路,包含三個思想:
- Basically Available(基本可用):分布式系統(tǒng)在出現(xiàn)故障時,允許損失部分可用性,即保證核心可用。
- Soft State(軟狀態(tài)):在一定時間內(nèi),允許出現(xiàn)中間狀態(tài),比如臨時的不一致狀態(tài)。
- Eventually Consistent(最終一致性):雖然無法保證強一致性,但是在軟狀態(tài)結(jié)束后,最終達到數(shù)據(jù)一致。
而分布式事務(wù)最大的問題是各個子事務(wù)的一致性問題,因此可以借鑒 CAP定理
和 BASE理論
:
- AP模式:各子事務(wù)分別執(zhí)行和提交,允許出現(xiàn)結(jié)果不一致,然后采用彌補措施恢復(fù)數(shù)據(jù)即可,實現(xiàn)最終一致。
- CP模式:各個子事務(wù)執(zhí)行后互相等待,同時提交,同時回滾,達成強一致。但事務(wù)等待過程中,處于弱可用狀態(tài)。
1.3 分布式事務(wù)模型
解決分布式事務(wù),各個子系統(tǒng)之間必須能感知到彼此的事務(wù)狀態(tài),才能保證狀態(tài)一致,因此需要一個事務(wù)協(xié)調(diào)者來協(xié)調(diào)每一個事務(wù)的參與者(子系統(tǒng)事務(wù))。
這里的子系統(tǒng)事務(wù),稱為分支事務(wù);有關(guān)聯(lián)的各個分支事務(wù)在一起稱為全局事務(wù)
2. Seata 架構(gòu)
Seata事務(wù)中有三個重要的角色:
- TC (Transaction Coordinator) - **事務(wù)協(xié)調(diào)者:**維護全局和分支事務(wù)的狀態(tài),協(xié)調(diào)全局事務(wù)提交或回滾。
- TM (Transaction Manager) - **事務(wù)管理器:**定義全局事務(wù)的范圍、開始全局事務(wù)、提交或回滾全局事務(wù)。
- RM (Resource Manager) - **資源管理器:**管理分支事務(wù)處理的資源,與TC交談以注冊分支事務(wù)和報告分支事務(wù)的狀態(tài),并驅(qū)動分支事務(wù)提交或回滾。
Seata 提供了四種不同的分布式事務(wù)解決方案:
- XA模式:強一致性分階段事務(wù)模式,犧牲了一定的可用性,無業(yè)務(wù)侵入
- TCC模式:最終一致的分階段事務(wù)模式,有業(yè)務(wù)侵入
- AT模式:最終一致的分階段事務(wù)模式,無業(yè)務(wù)侵入,也是Seata的默認模式
- SAGA模式:長事務(wù)模式,有業(yè)務(wù)侵入
2.1 項目引入 Seata
首先在項目中引入依賴:
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本較低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
項目配置:
seata:
registry:
type: nacos # TC服務(wù)注冊中心的配置,微服務(wù)根據(jù)這些信息去注冊中心獲取tc服務(wù)地址
# 參考tc服務(wù)自己的registry.conf中的配置,
# 包括:地址、namespace、group、application-name 、cluster
nacos:
server-addr: 127.0.0.1:8848
namespace: "" #空就是public
group: DEFAULT_GROUP
application: seata-tc-server
username: nacos
password: nacos
tx-service-group: seata-demo # 事務(wù)組,根據(jù)這個獲取tc服務(wù)的cluster名稱
service:
vgroup-mapping: # 事務(wù)組與TC服務(wù)cluster的映射關(guān)系
seata-demo: SH
3. 強一致性分布式事務(wù)解決方案
3.1 XA 模式
XA模式原理:XA 規(guī)范 是 X/Open 組織定義的分布式事務(wù)處理(DTP,Distributed Transaction Processing)標準,XA 規(guī)范 描述了全局的TM與局部的RM之間的接口,幾乎所有主流的數(shù)據(jù)庫都對 XA 規(guī)范 提供了支持。
如果事務(wù)都成功執(zhí)行,那么如下圖所示:
如果任意一個事務(wù)執(zhí)行失敗,那么如下圖所示:
3.1.1 seata的XA模式
seata的XA模式做了一些調(diào)整,但大體上相似:
RM一階段的工作:
- 注冊分支事務(wù)到TC
- 執(zhí)行分支業(yè)務(wù)sql但不提交
- 報告執(zhí)行狀態(tài)到TC
TC二階段的工作:
- TC檢測各分支事務(wù)執(zhí)行狀態(tài)
- 如果都成功,通知所有
RM
提交事務(wù) - 如果有失敗,通知所有
RM
回滾事務(wù)
- 如果都成功,通知所有
RM二階段的工作:
- 接收TC指令,提交或回滾事務(wù)
3.1.2 XA 模式實踐
Seata的starter已經(jīng)完成了XA模式的自動裝配,實現(xiàn)非常簡單,步驟如下:
- 修改每個微服務(wù)中的
application.yml
配置文件,開啟XA模式:
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: "" #空就是public
group: DEFAULT_GROUP
application: seata-tc-server
username: nacos
password: nacos
tx-service-group: seata-demo #事務(wù)組名稱
service:
vgroup-mapping: #事務(wù)組與cluster的映射關(guān)系
seata-demo: SH
data-source-proxy-mode: XA #開啟數(shù)據(jù)源代理的XA模式
- 給發(fā)起全局事務(wù)的入口方法添加
@GlobalTransactional
注解,本例中是OrderServiceImpl中的create方法:
@Override
@GlobalTransactional
public Long create(Order order) {
// 創(chuàng)建訂單
orderMapper.insert(order);
try {
// 扣用戶余額
accountClient.deduct(order.getUserId(), order.getMoney());
// 扣庫存
storageClient.deduct(order.getCommodityCode(), order.getCount());
} catch (FeignException e) {
log.error("下單失敗,原因:{}", e.contentUTF8(), e);
throw new RuntimeException(e.contentUTF8(), e);
}
return order.getId();
}
3.1.3 總結(jié)
XA 模式的優(yōu)點是什么?
- 事務(wù)的強一致性,滿足
ACID
原則。 - 常用數(shù)據(jù)庫都支持,實現(xiàn)簡單,并且沒有代碼侵入。
XA 模式的缺點是什么?
- 因為一階段需要鎖定數(shù)據(jù)庫資源,等待二階段結(jié)束才釋放,性能較差。
- 依賴關(guān)系型數(shù)據(jù)庫實現(xiàn)事務(wù)
4. 最終一致性分布式事務(wù)解決方案
4.1 AT 模式
AT模式同樣是分階段提交的事務(wù)模型,不過卻彌補了 XA 模型中資源鎖定周期過長的缺陷。
階段一 RM
的工作:
- 注冊分支事務(wù)
- 記錄
undo-log
(數(shù)據(jù)快照) - 執(zhí)行業(yè)務(wù)sql并提交
- 報告事務(wù)狀態(tài)
階段二提交時 RM
的工作:
- 刪除
undo-log
即可
階段二回滾時 RM
的工作:
- 根據(jù)
undo-log
恢復(fù)數(shù)據(jù)到更新之前
AT模式有一個缺陷就是“寫丟失”,如下圖所示:
為了解決“寫丟失”問題,AT模式引入了“全局鎖”概念,由TC記錄當前正在操作某行數(shù)據(jù)的事務(wù),該事務(wù)持有全局鎖,具備執(zhí)行權(quán)。
但是這種方法還是有一種問題,如果一個不是由seata管理的事務(wù)去修改money值,那么還是會導(dǎo)致“寫丟失”,如下圖所示:
那么我們最好杜絕這種情況的發(fā)生,讓包含修改money值的事務(wù)都受到seata的管理;或是像上圖一樣利用樂觀鎖的思想。
4.1.1 AT 模式實踐
AT模式中的快照生成、回滾等動作都是由框架自動完成,沒有任何代碼侵入,因此實現(xiàn)非常簡單。
- 在TC服務(wù)關(guān)聯(lián)的數(shù)據(jù)庫中導(dǎo)入
lock_table
,在微服務(wù)相關(guān)的數(shù)據(jù)庫中導(dǎo)入undo_log
。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
-- ----------------------------
-- Records of undo_log
-- ----------------------------
-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
- 修改配置文件,將事務(wù)模式修改為AT模式:
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: "" #空就是public
group: DEFAULT_GROUP
application: seata-tc-server
username: nacos
password: nacos
tx-service-group: seata-demo #事務(wù)組名稱
service:
vgroup-mapping: #事務(wù)組與cluster的映射關(guān)系
seata-demo: SH
data-source-proxy-mode: AT #開啟數(shù)據(jù)源代理的AT模式
- 給發(fā)起全局事務(wù)的入口方法添加
@GlobalTransactional
注解。
4.1.2 總結(jié)
簡述AT模式和XA模式的最大區(qū)別是什么?
- XA模式一階段不提交事務(wù),鎖定資源;AT模式一階段直接提交,不鎖定資源。
- XA模式依賴數(shù)據(jù)庫機制實現(xiàn)回滾,AT模式利用數(shù)據(jù)快照實現(xiàn)數(shù)據(jù)回滾。
- XA模式強一致,AT模式最終一致
AT模式的優(yōu)點:
- 一階段完成直接提交事務(wù),釋放數(shù)據(jù)庫資源,性能比較好
- 利用全局鎖實現(xiàn)讀寫隔離(XA模式鎖定數(shù)據(jù)庫資源的情況下讀寫都被阻塞)
- 沒有代碼侵入,框架自動完成回滾和提交
AT模式的缺點:
- 兩階段之間屬于軟狀態(tài),屬于最終一致
- 框架的快照功能會影響性能,但是比XA模式要好很多
4.2 TCC 模式
TCC模式與AT模式非常相似,每階段都是獨立事務(wù),不同的是TCC通過人工編碼來實現(xiàn)數(shù)據(jù)恢復(fù)。需要實現(xiàn)三個方法:
- Try:資源的檢測和預(yù)留。
- Confirm:完成資源操作業(yè)務(wù),要求 Try 成功 Confirm 一定要能成功。
- Cancel:預(yù)留資源釋放,可以理解為try的反向操作。
舉例,一個扣減用戶余額的業(yè)務(wù)。假設(shè)賬戶A原來余額是100,需要余額扣減30元。
- 階段一(Try):檢查余額是否充足,如果充足則凍結(jié)金額增加30元,可用余額扣除30
- 階段二:假如要提交(Confirm),則凍結(jié)金額扣減30
- 階段二:如果要回滾(Cancel),則凍結(jié)金額30,可用余額增加30
TCC的工作模型圖如下所示:
4.2.1 TCC 模式實踐
需求如下:
- 修改account-service,編寫try、confirm、cancel邏輯
- try業(yè)務(wù):添加凍結(jié)金額,扣減可用金額
- confirm業(yè)務(wù):刪除凍結(jié)金額
- cancel業(yè)務(wù):刪除凍結(jié)金額,恢復(fù)可用金額
- 保證confirm、cancel接口的冪等性
- 允許空回滾
- 拒絕業(yè)務(wù)懸掛
當某分支事務(wù)的try階段阻塞時,可能導(dǎo)致全局事務(wù)超時而觸發(fā)二階段的cancel操作。在未執(zhí)行try操作時先執(zhí)行了cancel操作,這時cancel不能做回滾,就是空回滾。對于已經(jīng)空回滾的業(yè)務(wù),如果以后繼續(xù)執(zhí)行try,就永遠不可能confirm或cancel,這就是業(yè)務(wù)懸掛。應(yīng)當阻止執(zhí)行空回滾后的try操作,避免懸掛。
為了實現(xiàn)空回滾、防止業(yè)務(wù)懸掛,以及冪等性要求。我們必須在數(shù)據(jù)庫記錄凍結(jié)金額的同時,記錄當前事務(wù)id和執(zhí)行狀態(tài),為此我們設(shè)計了一張表:
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) NOT NULL,
`user_id` varchar(255) DEFAULT NULL COMMENT '用戶id',
`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '凍結(jié)金額',
`state` int(1) DEFAULT NULL COMMENT '事務(wù)狀態(tài),0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
業(yè)務(wù)分析:
聲明TCC接口:
TCC的Try、Confirm、Cancel方法都需要在接口中基于注解來聲明,語法如下:
@LocalTCC
public interface AccountTCCService {
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money") int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
原業(yè)務(wù)方法如下所示:
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
log.info("開始扣款");
try {
accountMapper.deduct(userId, money);
} catch (Exception e) {
throw new RuntimeException("扣款失敗,可能是余額不足!", e);
}
log.info("扣款成功");
}
}
使用 TCC 模式后業(yè)務(wù)方法如下所示:
@Slf4j
@Service
public class AccountTccServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper accountFreezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
//1.獲取事務(wù)id
String xid = RootContext.getXID();
//業(yè)務(wù)懸掛
//判斷 accountFreeze 中是否有凍結(jié)記錄,如果有,一定是 CANCEL 執(zhí)行過,我要拒絕業(yè)務(wù)
AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);
if (accountFreeze != null) {
//CANCEL 執(zhí)行過,拒絕業(yè)務(wù)
return;
}
//2.扣減可用余額
accountMapper.deduct(userId, money);
//3.記錄凍結(jié)金額,事務(wù)狀態(tài)
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
accountFreezeMapper.insert(freeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
//1.獲取事務(wù)id
String xid = ctx.getXid();
//2.根據(jù)id刪除凍結(jié)記錄
int count = accountFreezeMapper.deleteById(xid);
return count == 1;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
//0.查詢凍結(jié)記錄
String xid = ctx.getXid();
String userId = ctx.getActionContext("userId").toString();
AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);
//1.空回滾的判斷,判斷 accountFreeze 是否為null,為null證明try沒執(zhí)行,需要空回滾
if (accountFreeze == null) {
//為null證明try沒執(zhí)行,需要空回滾
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
accountFreezeMapper.insert(freeze);
return true;
}
//2.冪等判斷,cancel方法超時會重試
if (accountFreeze.getState() == AccountFreeze.State.CANCEL) {
//已經(jīng)處理過一次 CANCEL 了,無需重復(fù)處理
return true;
}
//3.恢復(fù)可用余額
accountMapper.refund(accountFreeze.getUserId(), accountFreeze.getFreezeMoney());
//4.將凍結(jié)金額清零,狀態(tài)改為 CANCEL
accountFreeze.setFreezeMoney(0);
accountFreeze.setState(AccountFreeze.State.CANCEL);
int count = accountFreezeMapper.updateById(accountFreeze);
return count == 1;
}
}
AT模式 和 TCC模式可以混用。
4.2.2 總結(jié)
TCC模式的每個階段是做什么的?
- Try:資源檢查和預(yù)留
- Confirm:業(yè)務(wù)執(zhí)行和提交
- Cancel:預(yù)留資源的釋放
TCC的優(yōu)點是什么?
- 一階段完成直接提交事務(wù),釋放數(shù)據(jù)庫資源,性能好
- 相比AT模型,無需生成快照,無需使用全局鎖,性能最強
- 不依賴數(shù)據(jù)庫事務(wù),而是依賴補償操作,可以用于非事務(wù)型數(shù)據(jù)庫
TCC的缺點是什么?
- 有代碼侵入,需要人為編寫try、Confirm和Cancel接口,太麻煩
- 軟狀態(tài),事務(wù)是最終一致
- 需要考慮Confirm和Cancel的失敗情況,做好冪等處理
4.3 Saga 模式
Saga模式是SEATA提供的長事務(wù)解決方案。也分為兩個階段:
- 一階段:直接提交本地事務(wù)
- 二階段:成功則什么都不做;失敗則通過編寫補償業(yè)務(wù)來回滾
Saga模式的優(yōu)點:
- 事務(wù)參與者可以基于事件驅(qū)動實現(xiàn)異步調(diào)用,吞吐高
- 一階段直接提交事務(wù),無鎖,性能好
- 不用編寫TCC中的三個階段,實現(xiàn)簡單
Saga模式的缺點:
- 軟狀態(tài)持續(xù)時間不確定,時效性差
- 沒有鎖,沒有事務(wù)隔離,會有臟寫
文章來源:http://www.zghlxwxcb.cn/news/detail-606885.html
5. 四種模式總結(jié)
文章來源地址http://www.zghlxwxcb.cn/news/detail-606885.html
到了這里,關(guān)于【SpirngCloud】分布式事務(wù)解決方案的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!