ShardingSphere如何輕鬆駕馭Seata柔性分散式事務?

公众号-JavaEdge發表於2024-11-16

0 前文

上一文解析了 ShardingSphere 強一致性事務支援 XAShardingTransactionManager ,本文繼續:

  • 講解該類
  • 介紹支援柔性事務的 SeataATShardingTransactionManager

sharding-transaction-xa-core中關於 XAShardingTransactionManager,本文研究 XATransactionManager 和 ShardingConnection 類實現。

1 XAShardingTransactionManager

1.1 init

public void init(final DatabaseType databaseType, final Collection<ResourceDataSource> resourceDataSources) {
        for (ResourceDataSource each : resourceDataSources) {
            // 根據傳入的 ResourceDataSource建立XATransactionDataSource並快取
            cachedDataSources.put(each.getOriginalName(), new XATransactionDataSource(databaseType, each.getUniqueResourceName(), each.getDataSource(), xaTransactionManager));
        }
        // 對透過 SPI 建立的 XATransactionManager 也執行其 init 初始化
        xaTransactionManager.init();
}

1.2 其它方法

實現也簡單:

@Override
public TransactionType getTransactionType() {
    return TransactionType.XA;
}

@SneakyThrows
@Override
public boolean isInTransaction() {
    return Status.STATUS_NO_TRANSACTION != xaTransactionManager.getTransactionManager().getStatus();
}

@Override
public Connection getConnection(final String dataSourceName) throws SQLException {
	return cachedDataSources.get(dataSourceName).getConnection();
}

1.3 事務操作相關

begin、commit 和 rollback直接委託儲存在 XATransactionManager#TransactionManager 完成:

@SneakyThrows
@Override
public void begin() {
    xaTransactionManager.getTransactionManager().begin();
}

@SneakyThrows
@Override
public void commit() {
    xaTransactionManager.getTransactionManager().commit();
}

@SneakyThrows
@Override
public void rollback() {
    xaTransactionManager.getTransactionManager().rollback();
}

2 AtomikosTransactionManager

TransactionManager預設實現。

2.1 AtomikosXARecoverableResource

代表資源:

public final class AtomikosXARecoverableResource extends JdbcTransactionalResource {

    private final String resourceName;

    AtomikosXARecoverableResource(final String serverName, final XADataSource xaDataSource) {
        super(serverName, xaDataSource);
        resourceName = serverName;
    }

  	// 比對SingleXAResource#ResourceName,確定是否在使用資源,此即設計包裝 XAResource 的 SingleXAResource 類的原因
    @Override
    public boolean usesXAResource(final XAResource xaResource) {
        return resourceName.equals(((SingleXAResource) xaResource).getResourceName());
    }
}

2.2 AtomikosXARecoverableResource

public final class AtomikosTransactionManager implements XATransactionManager {

    private final UserTransactionManager transactionManager = new UserTransactionManager();

    private final UserTransactionService userTransactionService = new UserTransactionServiceImp();

    @Override
    public void init() {
        userTransactionService.init();
    }

    @Override
    public void registerRecoveryResource(final String dataSourceName, final XADataSource xaDataSource) {
        userTransactionService.registerResource(new AtomikosXARecoverableResource(dataSourceName, xaDataSource));
    }

    @Override
    public void removeRecoveryResource(final String dataSourceName, final XADataSource xaDataSource) {
        userTransactionService.removeResource(new AtomikosXARecoverableResource(dataSourceName, xaDataSource));
    }

    @Override
    @SneakyThrows
    public void enlistResource(final SingleXAResource xaResource) {
        transactionManager.getTransaction().enlistResource(xaResource);
    }

    @Override
    public TransactionManager getTransactionManager() {
        return transactionManager;
    }

    @Override
    public void close() {
        userTransactionService.shutdown(true);
    }
}

對 Atomikos 的 UserTransactionManager、UserTransactionService 簡單呼叫,Atomikos#UserTransactionManager 實現 TransactionManager 介面,封裝所有 TransactionManager 需要完成的工作。

看完 sharding-transaction-xa-atomikos-manager,再看 sharding-transaction-xa-bitronix-manager 工程。基於 bitronix 的 XATransactionManager 實現方案

3 BitronixXATransactionManager

public final class BitronixXATransactionManager implements XATransactionManager {

    private final BitronixTransactionManager bitronixTransactionManager = TransactionManagerServices.getTransactionManager();

    @Override
    public void init() {
    }

    @SneakyThrows
    @Override
    public void registerRecoveryResource(final String dataSourceName, final XADataSource xaDataSource) {
        ResourceRegistrar.register(new BitronixRecoveryResource(dataSourceName, xaDataSource));
    }

    @SneakyThrows
    @Override
    public void removeRecoveryResource(final String dataSourceName, final XADataSource xaDataSource) {
        ResourceRegistrar.unregister(new BitronixRecoveryResource(dataSourceName, xaDataSource));
    }

    @SneakyThrows
    @Override
    public void enlistResource(final SingleXAResource singleXAResource) {
        bitronixTransactionManager.getTransaction().enlistResource(singleXAResource);
    }

    @Override
    public TransactionManager getTransactionManager() {
        return bitronixTransactionManager;
    }

    @Override
    public void close() {
        bitronixTransactionManager.shutdown();
    }
}

XA兩階段提交核心類:

4 ShardingConnection

上圖的整個流程源頭ShardingConnection類,建構函式發現建立 ShardingTransactionManager 過程:

@Getter
public final class ShardingConnection extends AbstractConnectionAdapter {
      public ShardingConnection(...) {
        ...
        shardingTransactionManager = runtimeContext.getShardingTransactionManagerEngine().getTransactionManager(transactionType);
    }
}

ShardingConnection多處用到上面建立的shardingTransactionManager。如:

createConnection

獲取連線:

@Override
protected Connection createConnection(final String dataSourceName, final DataSource dataSource) throws SQLException {
        return isInShardingTransaction() ? shardingTransactionManager.getConnection(dataSourceName) : dataSource.getConnection();
}

isInShardingTransaction

判斷是否在同一事務:

private boolean isInShardingTransaction() {
        return null != shardingTransactionManager && shardingTransactionManager.isInTransaction();
}

setAutoCommit

@Override
public void setAutoCommit(final boolean autoCommit) throws SQLException {
        if (TransactionType.LOCAL == transactionType) {
            super.setAutoCommit(autoCommit);
            return;
        }
        if (autoCommit && !shardingTransactionManager.isInTransaction() || !autoCommit && shardingTransactionManager.isInTransaction()) {
            return;
        }
        if (autoCommit && shardingTransactionManager.isInTransaction()) {
            shardingTransactionManager.commit();
            return;
        }
        if (!autoCommit && !shardingTransactionManager.isInTransaction()) {
            closeCachedConnections();
            shardingTransactionManager.begin();
        }
}

事務型別為本地事務時,直接呼叫 ShardingConnection 父類 AbstractConnectionAdapter#setAutoCommit 完成本地事務自動提交:

  • autoCommit=true 且執行在事務中,調shardingTransactionManager.commit()完成提交
  • autoCommit=false 且當前不在事務中時,調 shardingTransactionManager.begin() 啟動事務

commit、rollback

類似setAutoCommit ,按事務型別決定是否進行分散式提交和回滾:

@Override
public void commit() throws SQLException {
        if (TransactionType.LOCAL == transactionType) {
            super.commit();
        } else {
            shardingTransactionManager.commit();
        }
}

@Override
public void rollback() throws SQLException {
        if (TransactionType.LOCAL == transactionType) {
            super.rollback();
        } else {
            shardingTransactionManager.rollback();
        }
}

ShardingSphere提供兩階段提交的 XA 協議實現方案的同時,也實現柔性事務。看完 XAShardingTransactionManager,來看基於 Seata 框架的柔性事務 TransactionManager 實現類 SeataATShardingTransactionManager。

5 SeataATShardingTransactionManager

該類完全採用阿里Seata框架提供分散式事務特性,而非遵循類似 XA 這樣的開發規範,所以程式碼實現比 XAShardingTransactionManager 類層結構簡單,複雜性都遮蔽在了框架內部。

整合 Seata,先要初始化 TMClient、RMClient,在 Seata 內部,這兩個客戶端之間會基於RPC通訊。

SeataATShardingTransactionManager#init的initSeataRPCClient初始化這倆客戶端物件:

// 根據 seata.conf 建立配置物件
FileConfiguration configuration = new FileConfiguration("seata.conf");

initSeataRPCClient() {
    String applicationId = configuration.getConfig("client.application.id");
    Preconditions.checkNotNull(applicationId, "please config application id within seata.conf file");
    String transactionServiceGroup = configuration.getConfig("client.transaction.service.group", "default");
    TMClient.init(applicationId, transactionServiceGroup);
    RMClient.init(applicationId, transactionServiceGroup);
}

Seata也提供一套構建在 JDBC 規範之上的實現策略,類似03文介紹的 ShardingSphere 與 JDBC 規範之間相容性。

Seata使用DataSourceProxy、ConnectionProxy代理物件,如DataSourceProxy:

實現了自定義Resource介面,繼承AbstractDataSourceProxy(最終實現JDBC的DataSource介面)。所以,初始化 Seata 框架時,也要根據輸入 DataSource 物件構建 DataSourceProxy,並透過 DataSourceProxy 獲取 ConnectionProxy。

init、getConnection

@Override
public void init(final DatabaseType databaseType, final Collection<ResourceDataSource> resourceDataSources) {
     // 初始化 Seata 客戶端
     initSeataRPCClient();
     // 建立 DataSourceProxy 並放入Map
     for (ResourceDataSource each : resourceDataSources) {
            dataSourceMap.put(each.getOriginalName(), new DataSourceProxy(each.getDataSource()));
     }
}

@Override
public Connection getConnection(final String dataSourceName) {
  	// 根據 DataSourceProxy 獲取 ConnectionProxy
  	return dataSourceMap.get(dataSourceName).getConnection();
}

初始化後,提供了事務開啟和提交相關的入口。Seata的GlobalTransaction是核心介面,封裝了面向使用者操作層的分散式事務訪問入口:

public interface GlobalTransaction {
    void begin() throws TransactionException;
    void begin(int timeout) throws TransactionException;
    void begin(int timeout, String name) throws TransactionException;
    void commit() throws TransactionException;
    void rollback() throws TransactionException;
    GlobalStatus getStatus() throws TransactionException;
    String getXid();
}

ShardingSphere 作 GlobalTransaction 的使用者層,也基於 GlobalTransaction 完成分散式事務操作。但 ShardingSphere 並未直接使用這層,而是設計位於sharding-transaction-base-seata-at的SeataTransactionHolder類,儲存執行緒安全的 GlobalTransaction 物件。

SeataTransactionHolder

final class SeataTransactionHolder {

    private static final ThreadLocal<GlobalTransaction> CONTEXT = new ThreadLocal<>();

    static void set(final GlobalTransaction transaction) {
        CONTEXT.set(transaction);
    } 
    static GlobalTransaction get() {
        return CONTEXT.get();
    }

    static void clear() {
        CONTEXT.remove();
    }
}

使用 ThreadLocal 確保對 GlobalTransaction 訪問的執行緒安全性。

咋判斷當前操作是否處於一個全域性事務?Seata存在一個上下文物件RootContex儲存參與者和發起者之間傳播的 Xid:

  • 當事務發起者開啟全域性事務,將 Xid 填入 RootContext
  • 然後 Xid 沿服務呼叫鏈一直傳播,進而填充到每個事務參與者程序的 RootContext
  • 事務參與者發現 RootContext 存在 Xid,就可知自己處於全域性事務

因此,只需判斷:

@Override
public boolean isInTransaction() {
        return null != RootContext.getXID();
}

Seata 也提供針對全域性事務的上下文類 GlobalTransactionContext,可用:

  • getCurrent 獲取一個 GlobalTransaction物件
  • 或透過 getCurrentOrCreate 在無法獲取 GlobalTransaction 物件時新建一個

就不難理解如下實現了

begin

@Override
@SneakyThrows
public void begin() {
  			 // 建立一個 GlobalTransaction,儲存到 SeataTransactionHolder
        SeataTransactionHolder.set(GlobalTransactionContext.getCurrentOrCreate());
  			 // 從 SeataTransactionHolder 獲取一個 GlobalTransaction,並調 begin 啟動事務
        SeataTransactionHolder.get().begin();
        SeataTransactionBroadcaster.collectGlobalTxId();
}

注意到最後的類:

SeataTransactionBroadcaster

儲存 Seata 全域性 Xid 的一個容器類。事務啟動時收集全域性 Xid 並進行儲存,而在事務提交或回滾時清空這些 Xid。

class SeataTransactionBroadcaster {
    
    String SEATA_TX_XID = "SEATA_TX_XID";
    
    static void collectGlobalTxId() {
        if (RootContext.inGlobalTransaction()) {
            ShardingExecuteDataMap.getDataMap().put(SEATA_TX_XID, RootContext.getXID());
        }
    }
    
    static void broadcastIfNecessary(final Map<String, Object> shardingExecuteDataMap) {
        if (shardingExecuteDataMap.containsKey(SEATA_TX_XID) && !RootContext.inGlobalTransaction()) {
            RootContext.bind((String) shardingExecuteDataMap.get(SEATA_TX_XID));
        }
    }

    static void clear() {
        ShardingExecuteDataMap.getDataMap().remove(SEATA_TX_XID);
    }
}

因此

commit、rollback和close

實現就清楚了:

@Override
public void commit() {
        try {
            SeataTransactionHolder.get().commit();
        } finally {
            SeataTransactionBroadcaster.clear();
            SeataTransactionHolder.clear();
        }
}

@Override
public void rollback() {
        try {
            SeataTransactionHolder.get().rollback();
        } finally {
            SeataTransactionBroadcaster.clear();
            SeataTransactionHolder.clear();
        }
}

@Override
public void close() {
        dataSourceMap.clear();
        SeataTransactionHolder.clear();
        TmRpcClient.getInstance().destroy();
        RmRpcClient.getInstance().destroy();
}

sharding-transaction-base-seata-at 工程中的程式碼實際上就只有這些內容,這些內容也構成了在 ShardingSphere中 整合 Seata 框架的實現過程。

6 從原始碼到開發

本文給出應用程式咋整合 Seata 分散式事務框架的詳細過程,ShardingSphere 提供一種模版實現。日常開發,若想在業務程式碼整合 Seata,可參考 SeataTransactionHolder、SeataATShardingTransactionManager 等核心程式碼,而無需太多修改。

7 總結

XAShardingTransactionManager理解難在從 ShardingConnection 到底層 JDBC 規範的整個整合和相容過程。

8 整合Seata框架

參考 ShardingSphere 的實現:


1. 配置 Seata 環境

  • 配置檔案準備: 建立 seata.conf 檔案,定義 applicationIdtransactionServiceGroup 等引數。
  • 啟動 Seata 服務: 啟動 Seata Server 並確保其與資料庫的事務協調機制正常工作。

2. 初始化 Seata 客戶端

專案中初始化 TMClientRMClient,它們分別代表事務管理器和資源管理器:

FileConfiguration configuration = new FileConfiguration("seata.conf");
String applicationId = configuration.getConfig("client.application.id");
String transactionServiceGroup = configuration.getConfig("client.transaction.service.group", "default");
TMClient.init(applicationId, transactionServiceGroup);
RMClient.init(applicationId, transactionServiceGroup);

3. 資料來源代理

構建 DataSourceProxy 使用 Seata 的 DataSourceProxy 對資料來源進行代理。

DataSourceProxy dataSourceProxy = new DataSourceProxy(originalDataSource);

獲取連線代理:從代理資料來源中獲取 ConnectionProxy,使每個資料庫連線支援事務傳播。

Connection connection = dataSourceProxy.getConnection();

4. 全域性事務上下文管理

基於 GlobalTransactionContext 獲取或建立事務物件:

GlobalTransaction transaction = GlobalTransactionContext.getCurrentOrCreate();

繫結全域性事務 XID: 當事務發起時,將全域性事務的 XID 儲存在 RootContext 中:

RootContext.bind(transaction.getXid());

透過 RootContext 判斷事務狀態:

boolean isInTransaction = RootContext.inGlobalTransaction();

5. 事務操作實現

開啟事務:

transaction.begin();

提交事務:

try {
    transaction.commit();
} finally {
    RootContext.unbind();
}

回滾事務:

try {
    transaction.rollback();
} finally {
    RootContext.unbind();
}

6. 整合業務邏輯

將分散式事務的核心邏輯封裝在工具類中,例如 SeataTransactionHolder,以便方便地管理全域性事務上下文:

SeataTransactionHolder.set(GlobalTransactionContext.getCurrentOrCreate());

7. 清理資源

在應用關閉時,清理客戶端資源:

TmRpcClient.getInstance().destroy();
RmRpcClient.getInstance().destroy();

8. 注意事項

  • 確保所有資料來源透過 DataSourceProxy 代理,避免事務管理失效。
  • 配置資料庫支援 Undo Log 表,確保事務回滾記錄正常儲存。
  • 除錯過程中,檢查 Seata Server 日誌和應用日誌,定位事務協調的問題。

透過上述步驟,可以在業務程式碼中順利整合 Seata,實現分散式事務管理,保障資料一致性。

關注我,緊跟本系列專欄文章,咱們下篇再續!

作者簡介:魔都架構師,多家大廠後端一線研發經驗,在分散式系統設計、資料平臺架構和AI應用開發等領域都有豐富實踐經驗。

各大技術社群頭部專家博主。具有豐富的引領團隊經驗,深厚業務架構和解決方案的積累。

負責:

  • 中央/分銷預訂系統效能最佳化
  • 活動&券等營銷中臺建設
  • 交易平臺及資料中臺等架構和開發設計
  • 車聯網核心平臺-物聯網連線平臺、大資料平臺架構設計及最佳化
  • LLM Agent應用開發
  • 區塊鏈應用開發
  • 大資料開發挖掘經驗
  • 推薦系統專案

目前主攻市級軟體專案設計、構建服務全社會的應用系統。

參考:

  • 程式設計嚴選網

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章