分布式事務
一、分布式事務基礎
什么是事務?
事務指的就是一個操作單元,在這個操作單元中的所有操作最終要保持一致的行為,要么所有操作都成功,要么所有的操作都被撤銷,簡單地說,事務提供一種“要么什么都不做,要么做全套”機制
本地事物
本地事物其實可以認為是資料庫提供的事務機制,說到資料庫事務就不得不說,資料庫事務中的四
大特性:
-
A:原子性(Atomicity),一個事務中的所有操作,要么全部完成,要么全-部不完成
-
C:一致性(Consistency),在一個事務執行之前和執行之后資料庫都必須處于一致性狀態
-
I:隔離性(Isolation),在并發環境中,當不同的事務同時操作相同的資料時,事務之間互不影響
-
D:持久性(Durability),指的是只要事務成功結束,它對資料庫所做的更新就必須永久的保存下來
資料庫事務在實作時會將一次事務涉及的所有操作全部納入到一個不可分割的執行單元,該執行單元中的所有操作要么都成功,要么都失敗,只要其中任一操作執行失敗,都將導致整個事務的回滾
分布式事務
分布式事務指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位于不同的分布式系統的不同節點之上,
簡單的說,就是一次大的操作由不同的小操作組成,這些小的操作分布在不同的服務器上,且屬于不同的應用,分布式事務需要保證這些小操作要么全部成功,要么全部失敗,
本質上來說,分布式事務就是為了保證不同資料庫的資料一致性,
分布式事務的場景
- 單體系統訪問多個資料庫
一個服務需要呼叫多個資料庫實體完成資料的增刪改操作

- 多個微服務訪問同一個資料庫
多個服務需要呼叫一個資料庫實體完成資料的增刪改操作

- 多個微服務訪問多個資料庫
多個服務需要呼叫一個資料庫實體完成資料的增刪改操作

二、分布式事務解決方案
全域事務
全域事務基于DTP模型實作,DTP是由X/Open組織提出的一種分布式事務模型——X/Open Distributed Transaction Processing Reference Model,它規定了要實作分布式事務,需要三種?色:
-
AP: Application 應用系統 (微服務)
-
TM: Transaction Manager 事務管理器 (全域事務管理)
-
RM: Resource Manager 資源管理器 (資料庫)
整個事務分成兩個階段:
-
階段一: 表決階段,所有參與者都將本事務執行預提交,并將能否成功的資訊反饋發給協調者,
-
階段二: 執行階段,協調者根據所有參與者的反饋,通知所有參與者,步調一致地執行提交或者回滾,

優點
- 提高了資料一致性的概率,實作成本較低
缺點
-
單點問題: 事務協調者宕機
-
同步阻塞: 延遲了提交時間,加?了資源阻塞時間
-
資料不一致: 提交第二階段,依然存在commit結果未知的情況,有可能導致資料不一致
可靠訊息服務
基于可靠訊息服務的方案是通過訊息中間件保證上、下游應用資料操作的一致性,假設有A和B兩個系統,分別可以處理任務A和任務B,此時存在一個業務流程,需要將任務A和任務B在同一個事務中處理,就可以使用訊息中間件來實作這種分布式事務,
RocketMQ事務訊息流程圖

1)事務訊息發送及提交
(1) 發送訊息(half訊息)
(2) 服務端回應訊息寫入結果
(3) 根據發送結果執行本地事務(如果寫入失敗,此時half訊息對業務不可?,本地邏輯不執行)
(4) 根據本地事務狀態執行Commit或者Rollback(Commit操作生產訊息索引,訊息對消費者可?)
2) 事務補償
(1) 對沒有Commit/Rollback的事務訊息(pending狀態的訊息),從服務端發起一次“回查”
(2) Producer收到回查訊息,檢查回查訊息對于的本地事務的狀態
(3) 根據本地事務狀態,重新Commit或者Rollback
其中,補償階段用戶解決訊息Commit或者Rollback發生超時或者失效的情況
3) 事務訊息狀態
事務訊息共有三種狀態,提交狀態,回查狀態,中間狀態:
- TransactionStatus.CommitTransaction: 提交事務,它允許消費者消費此訊息
- TransactionStatus.RollbackTransaction: 回滾事務,它代表訊息將被洗掉,不允許被消費
- TransactionStatus.Unknown: 中間狀態,它代表需要訊息佇列來確認狀態
訊息生產者實作
發送代碼如下:
Message<OperateIntergralVo> message =
MessageBuilder.withPayload(vo).setHeader("orderNo",orderNo).build();
TransactionSendResult sendResult =
rocketMQTemplate.sendMessageInTransaction("tx_group", "tx_topic", message,
orderNo);
String sendStatus = sendResult.getSendStatus().name();
String localTXState = sendResult.getLocalTransactionState().name();
og.info(">>>> send status={},localTransactionState={}
<<<<",sendStatus,localTXState);
if(sendResult.getLocalTransactionState().equals(LocalTransactionState.COMMIT_ME
SSAGE)){
return "退款成功";
}else{
return "退款失敗";
}
創建事務訊息生產者端的訊息監聽器,注意是生產者,不是消費者,我們需要實作的是RocketMQLocalTransactionListener介面,代碼如下:
@RocketMQTransactionListener(txProducerGroup = "tx_group")
@Slf4j
public class OrderTXMsgListener implements RocketMQLocalTransactionListener {
@Autowired
private IOrderInfoService orderInfoService;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg,
Object arg) {
log.info("執行本地事務");
RocketMQLocalTransactionState result =
RocketMQLocalTransactionState.COMMIT;
try {
String orderNo = (String) arg;
orderInfoService.changeOrderStatusToRefund(orderNo);
} catch (Exception e) {
result = RocketMQLocalTransactionState.ROLLBACK;
}
return result;
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String orderNo = (String) msg.getHeaders().get("orderNo");
if(!StringUtils.isEmpty(orderNo)){
OrderInfo orderInfo = orderInfoService.getOrderStatus(orderNo);
if(OrderInfo.STATUS_REFUND.equals(orderInfo.getStatus())){
return RocketMQLocalTransactionState.COMMIT;
}
}
TCC事務
TCC即為Try Confifirm Cancel,它屬于補償型分布式事務,TCC實作分布式事務一共有三個步驟:
- Try:嘗試待執行的業務
這個程序并未執行業務,只是完成所有業務的一致性檢查,并預留好執行所需的全部資源
- Confifirm:確認執行業務
確認執行業務操作,不做任何業務檢查, 只使用Try階段預留的業務資源,通常情況下,采用TCC則認為 Confifirm階段是不會出錯的,即:只要Try成功,Confifirm一定成功,若Confifirm階段真的出錯了,需引入重試機制或人工處理,
- Cancel:取消待執行的業務
取消Try階段預留的業務資源,通常情況下,采用TCC則認為Cancel階段也是一定成功的,若Cancel階段真的出錯了,需引入重試機制或人工處理,

return RocketMQLocalTransactionState.ROLLBACK;
}
}
TCC兩階段提交與XA兩階段提交的區別是:
-
XA是資源層面的分布式事務,強一致性,在兩階段提交的整個程序中,一直會持有資源的鎖,
-
TCC是業務層面的分布式事務,最終一致性,不會一直持有資源的鎖,
TCC事務的優缺點:
-
優點 :把資料庫層的二階段提交上提到了應用層來實作,規避了資料庫層的2PC性能低下問題,
-
缺點 :TCC的Try、Confifirm和Cancel操作功能需業務提供,開發成本高,
三、Seata分布式事務解決方案
2019 年 1 月,阿里巴巴中間件團隊發起了開源專案 Fescar (Fast & EaSy Commit And Rollback),其愿景是讓分布式事務的使用像本地事務的使用一樣,簡單和高效,并逐步解決開發者們遇到的分布式事務方面的所有難題,后來更名為 Seata ,意為:Simple Extensible Autonomous Transaction Architecture,是一套分布式事務解決方案,
Seata的設計目標是對業務無侵入,因此從業務無侵入的2PC方案著手,在傳統2PC的基礎上演進,
它把一個分布式事務理解成一個包含了若干分支事務的全域事務,全域事務的職責是協調其下管轄的分
支事務達成一致,要么一起成功提交,要么一起失敗回滾,此外,通常分支事務本身就是一個關系資料
庫的本地事務,
3.1 Seata-At模式
Seata主要由三個重要組件組成:
-
TC:Transaction Coordinator 事務協調器,管理全域的分支事務的狀態,用于全域性事務的提交和回滾,
-
TM:Transaction Manager 事務管理器,用于開啟、提交或者回滾全域事務,
-
RM:Resource Manager 資源管理器,用于分支事務上的資源管理,向TC注冊分支事務,上報分支事務的狀態,接受TC的命令來提交或者回滾分支事務,

Seata-AT模式的執行流程如下:
-
A服務的TM向TC申請開啟一個全域事務,TC就會創建一個全域事務并回傳一個唯一的XID
-
A服務的RM向TC注冊分支事務,并及其納入XID對應全域事務的管轄
-
A服務執行分支事務,向資料庫做操作4. A服務開始遠程呼叫B服務,此時XID會在微服務的呼叫鏈上傳播
-
B服務的RM向TC注冊分支事務,并將其納入XID對應的全域事務的管轄
-
B服務執行分支事務,向資料庫做操作
-
全域事務呼叫鏈處理完畢,TM根據有無例外向TC發起全域事務的提交或者回滾
-
TC協調其管轄之下的所有分支事務, 決定是否回滾
Seata-AT模式實作2PC與傳統2PC的差別 :
-
架構層次方面,傳統2PC方案的 RM 實際上是在資料庫層,RM本質上就是資料庫自身,通過XA協議實作,而 Seata的RM是以jar包的形式作為中間件層部署在應用程式這一側的,
-
兩階段提交方面,傳統2PC無論第二階段的決議是commit還是rollback,事務性資源的鎖都要保持到Phase2完成才釋放,而Seata的做法是在Phase1 就將本地事務提交,這樣就可以省去Phase持鎖的時間,整體提高效率,
3.2 秒殺專案集成Seata
啟動Seata-server
1.上傳,將seata-server-1.3.0.zip上傳到/usr/local/software目錄下
2.解壓檔案到指定目錄
unzip /usr/local/software/seata-server-1.3.0.zip -d /usr/local
3.修改日志組態檔,否則啟動控制臺亂碼(如果是window的情況需要修改如下配置)
vi /usr/local/seata/conf/logback.xml
原配置如下:
<property name="CONSOLE_LOG_PATTERN" value="https://www.cnblogs.com/chy07/archive/2023/02/20/%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx"/>
修改成如下格式:
<property name="CONSOLE_LOG_PATTERN" value="https://www.cnblogs.com/chy07/archive/2023/02/20/%d{yyyy-MM-dd HH:mm:ss.SSS} %5p --- %[%15.15t] %-40.40logger{39} : %m%n%wEx"/>
此問題是因為開發者為seata1.3.0添加字體顏色,而在window中的shell腳本內不顯示發生的亂碼錯誤
4.修改registry.config檔案
vi /usr/local/seata/conf/registry.conf
修改內容如下:[注意需要把下面nacos的IP地址修改成實際地址]
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "nacos的IP地址:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "nacos的IP地址:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
}
5.啟動seata-server
nohup /usr/local/seata/bin/seata-server.sh -h 目前所在服務器ip地址 -p 7000 >log.out 2>1 &
專案集成seata配置
1.啟動seata-server,詳情請看部署檔案
2.在專案中添加依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.2.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
在組態檔中添加如下配置
seata:
tx-service-group: seckill-service-group
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.config.server-addr}
group: SEATA_GROUP
application: seata-server
service:
vgroup-mapping:
seckill-service-group: default
AT模式代碼實作
分布式事務發起方只需要貼@GlobalTransactional注解即可
分支分布式事務貼上@Transactional即可
3.3 Seata-TCC深度決議
TCC模型圖

模型設計
業務場景
1.賬戶支付,用戶賬戶金額減少
2.賬戶退款,用戶賬戶金額增加
表設計
CREATE TABLE `user_account` (
`user_id` varchar( 100 ) NOT NULL COMMENT '用戶UID',
`gmt_create` datetime NOT NULL COMMENT '創建時間',
`gmt_modified` datetime NOT NULL COMMENT '修改時間',
`amount` bigint( 20 ) NOT NULL COMMENT '用戶余額',
PRIMARY KEY (`user_id`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
扣錢業務邏輯
場景: 賬戶A上有 100 元,要扣除其中的 30 元
Try: 檢查余額,扣除其中 30 元;

Confirm: 空提交

Cancel: 返還扣除的 30 元

加錢業務邏輯
Try: 空操作;

Confirm: 增加可用金額 30 元

Cancel: 空操作

業務模型總結

并發控制
賬戶A上有 100 元,事務T1要扣除其中 30 元,事務T2也要扣除 30 元,出現并發
Try: 檢查余額,扣除其中 30 元

T2 Confirm: 空提交

T1 Cancel: 釋放T1預留的 30 元

業務模型優化
表增加凍結金額欄位
CREATE TABLE `user_account` (
`user_id` varchar( 100 ) NOT NULL COMMENT '用戶UID',
`gmt_create` datetime NOT NULL COMMENT '創建時間',
`gmt_modified` datetime NOT NULL COMMENT '修改時間',
`amount` bigint( 20 ) NOT NULL COMMENT '用戶余額',
`freezed_amount` bigint( 20 ) unsigned DEFAULT '0',
PRIMARY KEY (`user_id`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
扣錢為例: 賬戶上有 100 元,要扣除其中 30 元(此時里面的可用金額=amount-freezed_amount)
Try: 檢查余額,扣除其中 30 元(freezed_amount=freezed_amount+30)

Confirm: 扣除 30 元( amount=amount-30 freezed_amount=freezed_amount-30)

Cancel: 釋放預留的 30 元(freezed_amount=freezed_amount-30)

加錢為例: 賬戶上有 100 元,要加 30 元(此時里面的可用金額=amount-freezed_amount)
Try: 空操作

Confirm: 增加 30 元( amount=amount+30)

Cancel: 空操作

例外處理
慷訓滾
Try方法為執行,Cancel執行了
出現原因:
- Try超時
- 分布式事務回滾,觸發Cancel
- 未收到Try,收到Cancel
解決方案: Cancel方法需要識別出是否執行Try方法,如果執行了就正常執行Cancel,如果沒有就直接結束增加事務日志表來實作這個功能.
CREATE TABLE `account_transaction` (
`tx_id` varchar( 100 ) NOT NULL COMMENT '事務Txid',
`action_id` varchar( 100 ) NOT NULL COMMENT '分支事務id',
`gmt_create` datetime NOT NULL COMMENT '創建時間',
`gmt_modified` datetime NOT NULL COMMENT '修改時間',
`user_id` varchar( 100 ) NOT NULL COMMENT '賬戶Uid',
`amount` varchar( 100 ) NOT NULL COMMENT '變動金額',
`type` varchar( 100 ) NOT NULL DEFAULT '' COMMENT '變動型別',
PRIMARY KEY (`tx_id`,`action_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
冪等
多次呼叫二階段方法
出現原因:
-
網路例外
-
分支事務所在服務器宕機
解決方案: 做冪等性處理
CREATE TABLE `account_transaction` (
`tx_id` varchar( 100 ) NOT NULL COMMENT '事務Txid',
`action_id` varchar( 100 ) NOT NULL COMMENT '分支事務id',
`gmt_create` datetime NOT NULL COMMENT '創建時間',
`gmt_modified` datetime NOT NULL COMMENT '修改時間',
`user_id` varchar( 100 ) NOT NULL COMMENT '賬戶Uid',
`amount` varchar( 100 ) NOT NULL COMMENT '變動金額',
`type` varchar( 100 ) NOT NULL DEFAULT '' COMMENT '變動型別',
`state` smallint( 4 ) NOT NULL COMMENT '狀態: 1.初始化 2.已提交 3.已回滾',
PRIMARY KEY (`tx_id`,`action_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
防懸掛
Cancel比Try先執行
出現原因:
- Try超時(擁堵)
- 分布式事務回滾觸發Cancel
- 擁堵的Try到達
要允許慷訓滾,但是要拒絕慷訓滾之后的Try方法.
解決方案: 在Try方法中, 如果根據全域事務ID能查詢出資料出來,說明在try方法之前執行了慷訓滾,此時
就不能進行try方法,否則就正常執行try方法.
例外處理流程圖
Try方法

Comfirm方法

Cancel方法

TCC模式代碼實作
分布式事務發起方只需要貼@GlobalTransactional注解即可
分支事務需要完成下面步驟:
1.在介面上貼上@LocalTCC和@TwoPhaseBusinessAction注解,具體配置如下:
@LocalTCC
public interface IUsableIntegralService {
/**
* 增加積分
*/
@TwoPhaseBusinessAction(name = "incrIntergralTry", commitMethod =
"incrIntergralCommit", rollbackMethod = "incrIntergralRollback")
void incrIntergralTry(@BusinessActionContextParameter(paramName =
"operateIntergralVo") OperateIntergralVo operateIntergralVo,
BusinessActionContext context);
void incrIntergralCommit(BusinessActionContext context);
void incrIntergralRollback(BusinessActionContext context);
}
2.添加實作類,實作try,confirm,cancel方法邏輯即可
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/544423.html
標籤:其他
上一篇:Java 集合中的排序演算法淺析
下一篇:C++學習-const
