詳細講解一下分布式事務-兩階段提交的實現(xiàn)原理
一、為什么需要分布式事務
隨著微服務架構和分布式系統(tǒng)的普及,一個業(yè)務操作往往需要調用多個服務,修改多個數(shù)據(jù)源的數(shù)據(jù)。例如:
- 電商系統(tǒng)中的下單操作:需要扣減庫存、創(chuàng)建訂單、支付等多個操作
- 銀行轉賬操作:需要從一個賬戶扣款,另一個賬戶加款
這些操作需要作為一個整體要么全部成功,要么全部失敗,這就需要分布式事務來保證。
二、兩階段提交(2PC) 原理
兩階段提交(Two-Phase Commit,簡稱2PC)是分布式系統(tǒng)中實現(xiàn)分布式事務的經(jīng)典算法。它將事務的提交過程分為兩個階段:
- 準備階段(Prepare Phase):協(xié)調者詢問所有參與者是否可以提交
- 提交/回滾階段(Commit/Rollback Phase):根據(jù)準備階段的結果決定提交或回滾
角色劃分:
- 協(xié)調者(Coordinator):事務的發(fā)起者,負責協(xié)調所有參與者
- 參與者(Participant/Cohort):事務的實際執(zhí)行者,負責本地事務的執(zhí)行
兩階段提交流程:
(1)準備階段
- 協(xié)調者向所有參與者發(fā)送prepare請求
- 參與者執(zhí)行本地事務但不提交,記錄undo/redo日志
- 參與者向協(xié)調者反饋響應:
成功:返回"同意"
失敗:返回"中止"
(2)提交/回滾階段
情況1:所有參與者都返回"同意"
- 協(xié)調者向所有參與者發(fā)送commit請求
- 參與者完成本地事務提交
- 參與者向協(xié)調者發(fā)送ack響應
- 協(xié)調者收到所有ack后完成事務
情況2:任一參與者返回"中止"或超時
- 協(xié)調者向所有參與者發(fā)送rollback請求
- 參與者利用undo日志回滾本地事務
- 參與者向協(xié)調者發(fā)送ack響應
- 協(xié)調者收到所有ack后中斷事務
三、基本實現(xiàn)示例
首先定義協(xié)調者和參與者的接口:
public interface Coordinator {
void startTransaction(List<Participant> participants);
boolean prepare();
void commit();
void rollback();
}
public interface Participant {
boolean prepare();
void commit();
void rollback();
}
參與都實現(xiàn)代碼:
public class DatabaseParticipant implements Participant {
private Connection connection;
private String transactionId;
public DatabaseParticipant(Connection connection, String transactionId) {
this.connection = connection;
this.transactionId = transactionId;
}
@Override
public boolean prepare() {
try {
// 設置不自動提交
connection.setAutoCommit(false);
// 執(zhí)行SQL但不提交
// 這里應該有實際的業(yè)務SQL,如:
// PreparedStatement ps = connection.prepareStatement("UPDATE account SET balance = balance - 100 WHERE user_id = 1");
// ps.executeUpdate();
// 記錄redo/undo日志
logRedoUndo();
return true;
} catch (SQLException e) {
return false;
}
}
@Override
public void commit() {
try {
connection.commit();
cleanRedoUndo();
} catch (SQLException e) {
// 處理異常
}
}
@Override
public void rollback() {
try {
connection.rollback();
cleanRedoUndo();
} catch (SQLException e) {
// 處理異常
}
}
private void logRedoUndo() {
// 實現(xiàn)記錄redo/undo日志的邏輯
}
private void cleanRedoUndo() {
// 清理日志
}
}
協(xié)調者實現(xiàn)代碼:
public class TwoPhaseCommitCoordinator implements Coordinator {
private List<Participant> participants;
private String transactionId;
public TwoPhaseCommitCoordinator(String transactionId) {
this.transactionId = transactionId;
}
@Override
public void startTransaction(List<Participant> participants) {
this.participants = participants;
}
@Override
public boolean prepare() {
for (Participant participant : participants) {
if (!participant.prepare()) {
return false;
}
}
return true;
}
@Override
public void commit() {
if (prepare()) {
for (Participant participant : participants) {
participant.commit();
}
} else {
rollback();
}
}
@Override
public void rollback() {
for (Participant participant : participants) {
try {
participant.rollback();
} catch (Exception e) {
// 記錄日志,繼續(xù)回滾其他參與者
}
}
}
}
使用示例:
public class TwoPhaseCommitExample {
public static void main(String[] args) {
// 模擬兩個數(shù)據(jù)庫參與者
Connection conn1 = getConnection(); // 獲取第一個數(shù)據(jù)庫連接
Connection conn2 = getConnection(); // 獲取第二個數(shù)據(jù)庫連接
Participant participant1 = new DatabaseParticipant(conn1, "tx123");
Participant participant2 = new DatabaseParticipant(conn2, "tx123");
Coordinator coordinator = new TwoPhaseCommitCoordinator("tx123");
coordinator.startTransaction(Arrays.asList(participant1, participant2));
try {
coordinator.commit();
System.out.println("事務提交成功");
} catch (Exception e) {
coordinator.rollback();
System.out.println("事務回滾");
}
}
private static Connection getConnection() {
// 實際應用中應該從數(shù)據(jù)源獲取連接
return null;
}
}
四、Seata AT模式
Seata(Simple Extensible Autonomous Transaction Architecture)是一款開源的分布式事務解決方案,提供了AT、TCC、SAGA和XA四種事務模式。其中,AT(Auto Transaction)模式是基于兩階段提交協(xié)議改進而來,通過數(shù)據(jù)源代理和全局鎖機制實現(xiàn)了對業(yè)務代碼幾乎零侵入的分布式事務支持。
包含組件:
Transaction Coordinator (TC,事務協(xié)調器)
- 獨立部署的服務,維護全局事務和分支事務的狀態(tài)
- 負責協(xié)調全局事務的提交或回滾
- 管理全局鎖的獲取與釋放
Transaction Manager (TM,事務管理器)
- 嵌入在應用中,負責定義全局事務邊界
- 通過@GlobalTransactional注解標記分布式事務方法
- 向TC發(fā)起全局事務的開始、提交或回滾指令
Resource Manager (RM,資源管理器)
- 管理分支事務上的資源
- 向TC注冊分支事務并報告狀態(tài)
- 驅動分支事務的提交或回滾
- 負責生成和操作undo log
AT模式的整體流程:
(1)業(yè)務執(zhí)行與本地提交
- 解析SQL:攔截業(yè)務SQL,解析SQL類型(INSERT/UPDATE/DELETE)、表、條件等信息
- 查詢前鏡像:根據(jù)SQL條件查詢修改前的數(shù)據(jù)快照(before image)
- 執(zhí)行業(yè)務SQL:執(zhí)行用戶的實際業(yè)務SQL
- 查詢后鏡像:根據(jù)主鍵查詢修改后的數(shù)據(jù)快照(after image)
- 插入回滾日志:將前后鏡像和業(yè)務SQL信息組成undo log記錄,插入到undo_log表
- 注冊分支事務:向TC注冊分支事務并獲取全局鎖
- 提交本地事務:業(yè)務SQL和undo log在同一個本地事務中提交
- 上報執(zhí)行結果:將本地事務執(zhí)行結果上報給TC
(2)全局提交或回滾
全局提交:
- TC異步通知各分支事務提交
- RM異步刪除對應的undo log記錄
- 釋放全局鎖
全局回滾:
- TC通知各分支事務回滾
- RM根據(jù)undo log中的before image生成補償SQL并執(zhí)行
- 校驗數(shù)據(jù)一致性(對比after image與當前數(shù)據(jù))
- 刪除undo log記錄
- 釋放全局鎖
Seata的詳細信息,請查看官網(wǎng):https://seata.apache.org/zh-cn/docs/dev/mode/at-mode
五、兩階段提交的問題
(1)協(xié)調者單點故障
如果在第二階段協(xié)調者宕機,部分參與者收到commit而部分沒收到,系統(tǒng)將處于不一致狀態(tài)。
解決方法:記錄事務日志,協(xié)調者恢復后能繼續(xù)處理。
(2)網(wǎng)絡分區(qū)
網(wǎng)絡分區(qū)可能導致部分參與者無法收到協(xié)調者的指令。
為了解決2PC的網(wǎng)絡阻塞問題,引入了3PC:
- CanCommit階段:詢問參與者是否可以提交
- PreCommit階段:預提交,執(zhí)行事務但不提交
- DoCommit階段:實際提交
3PC通過引入超時機制減少了阻塞,但增加了復雜度。