分散式事務介紹

selrain_公眾號也叫selrain發表於2018-08-20

分散式事務

介紹

隨著業務的發展越來越快,資料庫的壓力越來越大,這時候就需要對資料庫拆分(在資料庫設計之初,出於未來業務的考量,也有可能提前拆分),在單庫模式下,資料的一致性依賴JDBC的事務,分庫之後,就需要依靠分散式事務來解決。

實現方式

1、最大努力送達

最大努力送達相信對於資料庫的操作最終一定會成功,其原理就是建立一個事務庫,對資料操作監聽,記錄事務日誌,對失敗的操作進行重試,最後清理事務日誌。

enter image description here

執行過程有 四種 情況:

1、【紅線】執行成功

2、【棕線】執行失敗,同步重試成功

3、【粉線】執行失敗,同步重試失敗,非同步重試成功

4、【綠線】執行失敗,同步重試失敗,非同步重試失敗,事務日誌保留

整個過程如下:

  • SoftTransactionManager:事務管理器 getTransaction:例項化具體的事務實現(目前有2種:最大努力送達BEDSoftTransaction、TCCSoftTransaction) init:利用guava的EventBus註冊監聽資料庫DML操作的監聽器;建立事務日誌表
  • BestEffortsDeliveryListener:事務監聽器 listen:監聽DML操作,維護事務表日誌
  • RdbTransactionLogStorage 事務日誌儲存器 add:日誌表新增一條資料 remove:刪除資料
  • 非同步送達作業

下面單獨看看每個元件:

SoftTransactionManager

負責對柔性事務配置( SoftTransactionConfiguration ) 、柔性事務( AbstractSoftTransaction )的管理。

#呼叫init方法初始化管理器

public void init() throws SQLException {
    //guava註冊監聽器
    EventBusInstance.getInstance().register(new BestEffortsDeliveryListener());
    if (TransactionLogDataSourceType.RDB == transactionConfig.getStorageType()) {
        Preconditions.checkNotNull(transactionConfig.getTransactionLogDataSource());
        //建立事務表
        createTable();
    }
}
private void createTable() throws SQLException {
    String dbSchema = "CREATE TABLE IF NOT EXISTS `transaction_log` ("
            + "`id` VARCHAR(40) NOT NULL, "
            + "`transaction_type` VARCHAR(30) NOT NULL, "
            + "`data_source` VARCHAR(255) NOT NULL, "
            + "`sql` TEXT NOT NULL, "
            + "`parameters` TEXT NOT NULL, "
            + "`creation_time` LONG NOT NULL, "
            + "`async_delivery_try_times` INT NOT NULL DEFAULT 0, "
            + "PRIMARY KEY (`id`));";
    try (
            Connection conn = transactionConfig.getTransactionLogDataSource().getConnection();
            PreparedStatement preparedStatement = conn.prepareStatement(dbSchema)) {
        preparedStatement.executeUpdate();
    }
}
複製程式碼

#呼叫getTransaction方法獲取事務實現

public AbstractSoftTransaction getTransaction(final SoftTransactionType type) {
    AbstractSoftTransaction result;
    switch (type) {
            //最大努力送達
        case BestEffortsDelivery: 
            result = new BEDSoftTransaction();
            break;
            //TCC 目前尚未實現
        case TryConfirmCancel:
            result = new TCCSoftTransaction();
            break;
        default: 
            throw new UnsupportedOperationException(type.toString());
    }
    // TODO don't support nested transaction, should configurable in future
    if (getCurrentTransaction().isPresent()) {
        throw new UnsupportedOperationException("Cannot support nested transaction.");
    }
    ExecutorDataMap.getDataMap().put(TRANSACTION, result);
    ExecutorDataMap.getDataMap().put(TRANSACTION_CONFIG, transactionConfig);
    return result;
}
複製程式碼

BestEffortsDeliveryListener

最大努力事務監聽器,通過監聽資料庫操作,同步重試失敗日誌

@Subscribe
@AllowConcurrentEvents
public void listen(final DMLExecutionEvent event) {
    if (!isProcessContinuously()) {
        return;
    }
    SoftTransactionConfiguration transactionConfig = SoftTransactionManager.getCurrentTransactionConfiguration().get();
    TransactionLogStorage transactionLogStorage = TransactionLogStorageFactory.createTransactionLogStorage(transactionConfig.buildTransactionLogDataSource());
    BEDSoftTransaction bedSoftTransaction = (BEDSoftTransaction) SoftTransactionManager.getCurrentTransaction().get();
    switch (event.getEventExecutionType()) {
        case BEFORE_EXECUTE:
            //TODO for batch SQL need split to 2-level records
            transactionLogStorage.add(new TransactionLog(event.getId(), bedSoftTransaction.getTransactionId(), bedSoftTransaction.getTransactionType(), 
                    event.getDataSource(), event.getSqlUnit().getSql(), event.getParameters(), System.currentTimeMillis(), 0));
            return;
        case EXECUTE_SUCCESS: 
            transactionLogStorage.remove(event.getId());
            return;
        case EXECUTE_FAILURE: 
            boolean deliverySuccess = false;
            for (int i = 0; i < transactionConfig.getSyncMaxDeliveryTryTimes(); i++) {
                if (deliverySuccess) {
                    return;
                }
                boolean isNewConnection = false;
                Connection conn = null;
                PreparedStatement preparedStatement = null;
                try {
                    conn = bedSoftTransaction.getConnection().getConnection(event.getDataSource());
                    if (!isValidConnection(conn)) {
                        bedSoftTransaction.getConnection().release(conn);
                        conn = bedSoftTransaction.getConnection().getConnection(event.getDataSource());
                        isNewConnection = true;
                    }
                    preparedStatement = conn.prepareStatement(event.getSqlUnit().getSql());
                    //TODO for batch event need split to 2-level records
                    for (int parameterIndex = 0; parameterIndex < event.getParameters().size(); parameterIndex++) {
                        preparedStatement.setObject(parameterIndex + 1, event.getParameters().get(parameterIndex));
                    }
                    preparedStatement.executeUpdate();
                    deliverySuccess = true;
                    transactionLogStorage.remove(event.getId());
                } catch (final SQLException ex) {
                    log.error(String.format("Delivery times %s error, max try times is %s", i + 1, transactionConfig.getSyncMaxDeliveryTryTimes()), ex);
                } finally {
                    close(isNewConnection, conn, preparedStatement);
                }
            }
            return;
        default: 
            throw new UnsupportedOperationException(event.getEventExecutionType().toString());
    }
}
複製程式碼
  • BestEffortsDeliveryListener 通過 EventBus 實現監聽 SQL 的執行。Sharding-JDBC 如何實現 EventBus 的,請參考芋艿大神的《Sharding-JDBC 原始碼分析 —— SQL 執行》分析
  • 呼叫 #isProcessContinuously() 方法判斷是否處於最大努力送達型事務中,當且僅當處於該狀態才進行監聽事件處理
  • SQL 執行前,插入事務日誌
  • SQL 執行成功,移除事務日誌
  • SQL 執行失敗,根據柔性事務配置( SoftTransactionConfiguration )同步的事務送達的最大嘗試次數( syncMaxDeliveryTryTimes )進行多次重試直到成功。

TransactionLogStorage

柔性事務執行過程中,會通過事務日誌( TransactionLog ) 記錄每條 SQL 執行狀態: 目前有2中實現:

  • MemoryTransactionLogStorage:基於記憶體的事務日誌儲存器。主要用於開發測試,生產環境下不要使用。
  • RdbTransactionLogStorage :基於資料庫的事務日誌儲存器。

這裡主要講講RdbTransactionLogStorage的實現:

#add
public void add(TransactionLog transactionLog) {
    String sql = "INSERT INTO `transaction_log` (`id`, `transaction_type`, `data_source`, `sql`, `parameters`, `creation_time`) VALUES (?, ?, ?, ?, ?, ?);";

    try {
        Connection conn = this.dataSource.getConnection();
        Throwable var4 = null;

        try {
            PreparedStatement preparedStatement = conn.prepareStatement(sql);
            Throwable var6 = null;

            try {
                preparedStatement.setString(1, transactionLog.getId());
                preparedStatement.setString(2, SoftTransactionType.BestEffortsDelivery.name());
                preparedStatement.setString(3, transactionLog.getDataSource());
                preparedStatement.setString(4, transactionLog.getSql());
                preparedStatement.setString(5, (new Gson()).toJson(transactionLog.getParameters()));
                preparedStatement.setLong(6, transactionLog.getCreationTime());
                preparedStatement.executeUpdate();
            } catch (Throwable var31) {
                var6 = var31;
                throw var31;
            } finally {
                if (preparedStatement != null) {
                    if (var6 != null) {
                        try {
                            preparedStatement.close();
                        } catch (Throwable var30) {
                            var6.addSuppressed(var30);
                        }
                    } else {
                        preparedStatement.close();
                    }
                }

            }
        } catch (Throwable var33) {
            var4 = var33;
            throw var33;
        } finally {
            conn->close
        }

    } catch (SQLException var35) {
        throw new TransactionLogStorageException(var35);
    }
}
#remove
public void remove(String id) {
    String sql = "DELETE FROM `transaction_log` WHERE `id`=?;";

    try {
        Connection conn = this.dataSource.getConnection();
        Throwable var4 = null;

        try {
            PreparedStatement preparedStatement = conn.prepareStatement(sql);
            Throwable var6 = null;

            try {
                preparedStatement.setString(1, id);
                preparedStatement.executeUpdate();
            } catch (Throwable var31) {
                var6 = var31;
                throw var31;
            } finally {
                if (preparedStatement != null) {
                    if (var6 != null) {
                        try {
                            preparedStatement.close();
                        } catch (Throwable var30) {
                            var6.addSuppressed(var30);
                        }
                    } else {
                        preparedStatement.close();
                    }
                }

            }
        } catch (Throwable var33) {
            var4 = var33;
            throw var33;
        } finally {
            conn->close
        }

    } catch (SQLException var35) {
        throw new TransactionLogStorageException(var35);
    }
}
複製程式碼

BestEffortsDeliveryJob

BestEffortsDeliveryJob 所在 Maven 專案為 sharding-jdbc-transaction-async-job,基於噹噹開源的 Elastic-Job 實現。如下是官方對該 Maven 專案的簡要說明: 由於柔性事務採用非同步嘗試,需要部署獨立的作業和Zookeeper。sharding-jdbc-transaction採用elastic-job實現的sharding-jdbc-transaction-async-job,通過簡單配置即可啟動高可用作業非同步送達柔性事務,啟動指令碼為start.sh


最後,歡迎follow我的個人訂閱號:

分散式事務介紹

相關文章