9. sharding-jdbc源碼之最大努力型事務

阿飛Javaer,轉載請註明原創出處,謝謝!

BASE Transaction

  • Best efforts delivery transaction (已經實現).
  • Try confirm cancel transaction (待定).

Sharding-JDBC由於性能方面的考量,決定不支持強一致性分佈式事務。

最大努力送達型事務

在分佈式數據庫的場景下,相信對於該數據庫的操作最終一定可以成功,所以通過最大努力反覆嘗試送達操作。

最大努力送達型事務的架構圖

最大努力送達型事務的架構圖

摘自sharding-jdbc使用指南☞事務支持

執行過程有以下幾種情況:
1. 執行成功–如圖所示,執行結果事件->監聽執行事件->執行成功->清理事務日誌
2. 執行失敗,同步重試成功–如圖所示,執行結果事件->監聽執行事件->執行失敗->重試執行->執行成功->清理事務日誌
3. 執行失敗,同步重試失敗,異步重試成功–如圖所示,執行結果事件->監聽執行事件->執行失敗->重試執行->執行失敗->”異步送達作業”重試執行->執行成功->清理事務日誌
4. 執行失敗,同步重試失敗,異步重試失敗,事務日誌保留—-如圖所示,執行結果事件->監聽執行事件->執行失敗->重試執行->執行失敗->”異步送達作業”重試執行->執行失敗->… …

說明:不管執行結果如何,執行前事件都會記錄事務日誌;執行事件類型包括3種:BEFORE_EXECUTEEXECUTE_FAILUREEXECUTE_SUCCESS;另外,這裏的”同步“不是絕對的同步執行,而是通過google-guava的EventBus發佈事件後,在監聽端判斷是EXECUTE_FAILURE事件,最多重試syncMaxDeliveryTryTimes次;後面對BestEffortsDeliveryListener的源碼分析有介紹;這裏的”異步“通過外掛實現,在後面的文章10. sharding-jdbc源碼之異步送達JOB會有分析;

適用場景

  • 根據主鍵刪除數據。
  • 更新記錄永久狀態,如更新通知送達狀態。

使用限制

  • 使用最大努力送達型柔性事務的SQL需要滿足冪等性。
  • INSERT語句要求必須包含主鍵,且不能是自增主鍵。
  • UPDATE語句要求冪等,不能是UPDATE xxx SET x=x+1
  • DELETE語句無要求。

開發示例

// 1. 配置SoftTransactionConfiguration
SoftTransactionConfiguration transactionConfig = new SoftTransactionConfiguration(dataSource);
// 配置相關請看後面的備註
transactionConfig.setXXX();

// 2. 初始化SoftTransactionManager
SoftTransactionManager transactionManager = new SoftTransactionManager(transactionConfig);
transactionManager.init();

// 3. 獲取BEDSoftTransaction
BEDSoftTransaction transaction = (BEDSoftTransaction) transactionManager.getTransaction(SoftTransactionType.BestEffortsDelivery);

// 4. 開啓事務
transaction.begin(connection);

// 5. 執行JDBC
/* 
    code here
*/
* 
// 6.關閉事務
transaction.end();

備註:SoftTransactionConfiguration支持的配置以及含義請參考sharding-jdbc使用指南☞事務支持,這段開發示例的代碼也摘自這裏;也可參考sharding-jdbc-transaction模塊中com.dangdang.ddframe.rdb.transaction.soft.integrate.SoftTransactionTest如何使用柔性事務,但是這裏的代碼需要稍作修改,否則只是普通的執行邏輯,不是sharding-jdbc的執行邏輯

@Test
public void bedSoftTransactionTest() throws SQLException {
    SoftTransactionManager transactionManagerFactory = new SoftTransactionManager(getSoftTransactionConfiguration(getShardingDataSource()));
    // 初始化柔性事務管理器
    transactionManagerFactory.init();
    BEDSoftTransaction transactionManager = (BEDSoftTransaction) transactionManagerFactory.getTransaction(SoftTransactionType.BestEffortsDelivery);
    transactionManager.begin(getShardingDataSource().getConnection());
    // 執行INSERT SQL(DML類型),如果執行過程中異常,會在`BestEffortsDeliveryListener`中重試
    insert();
    transactionManager.end();
}

private void insert() {
    String dbSchema = "insert into transaction_test(id, remark) values (2, ?)";
    try (
            // 將.getConnection("db_trans", SQLType.DML)移除,這樣的話,得到的纔是ShardingConnection 
            Connection conn = getShardingDataSource().getConnection();
            PreparedStatement preparedStatement = conn.prepareStatement(dbSchema)) {
        preparedStatement.setString(1, "JUST TEST IT .");
        preparedStatement.executeUpdate();
    } catch (final SQLException e) {
        e.printStackTrace();
    }
}

核心源碼分析

通過3. sharding-jdbc源碼之路由&執行中對ExecutorEngine的分析可知,sharding-jdbc在執行SQL前後,分別調用EventBusInstance.getInstance().post()提交了事件,那麼調用EventBusInstance.getInstance().register()的地方,就是柔性事務處理的地方,通過查看源碼的調用關係可知,只有SoftTransactionManager.init()調用了EventBusInstance.getInstance().register(),所以柔性事務實現的核心在SoftTransactionManager這裏;

柔性事務管理器

柔性事務實現在SoftTransactionManager中,核心源碼如下:

public final class SoftTransactionManager {

    // 柔性事務配置對象   
    @Getter
    private final SoftTransactionConfiguration transactionConfig;

    /**
     * Initialize B.A.S.E transaction manager.
     * @throws SQLException SQL exception
     */
    public void init() throws SQLException {
        // 初始化註冊最大努力送達型柔性事務監聽器;
        EventBusInstance.getInstance().register(new BestEffortsDeliveryListener());
        if (TransactionLogDataSourceType.RDB == transactionConfig.getStorageType()) {
            // 如果事務日誌數據源類型是關係型數據庫,則創建事務日誌表transaction_log
            createTable();
        }
        // 內嵌的最大努力送達型異步JOB任務,依賴噹噹開源的elastic-job
        if (transactionConfig.getBestEffortsDeliveryJobConfiguration().isPresent()) {
            new NestedBestEffortsDeliveryJobFactory(transactionConfig).init();
        }
    }

    // 從這裏可知創建的事務日誌表表名是transaction_log(所以需要保證每個庫中用戶沒有自定義創建transaction_log表)
    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();
        }
    }

從這段源碼可知,柔性事務的幾個重點如下,接下來一一根據源碼進行分析;
- 事務日誌存儲器;
- 最大努力送達型事務監聽器;
- 異步送達JOB任務;

1.事務日誌存儲器

柔性事務日誌接口類爲TransactionLogStorage.java,有兩個實現類:
1. RdbTransactionLogStorage:關係型數據庫存儲柔性事務日誌;
2. MemoryTransactionLogStorage:內存存儲柔性事務日誌;

1.1.1事務日誌核心接口

TransactionLogStorage中幾個重要接口在兩個實現類中的實現:
* void add(TransactionLog):Rdb實現就是把事務日誌TransactionLog 插入到transaction_log表中,Memory實現就是把事務日誌保存到ConcurrentHashMap中;
* void remove(String id):Rdb實現就是從transaction_log表中刪除事務日誌,Memory實現從ConcurrentHashMap中刪除事務日誌;
* void increaseAsyncDeliveryTryTimes(String id):異步增加送達重試次數,即TransactionLog中的asyncDeliveryTryTimes+1;Rdb實現就是update transaction_log表中async_delivery_try_times字段加1;Memory實現就是TransactionLog中重新給asyncDeliveryTryTimes賦值new AtomicInteger(transactionLog.getAsyncDeliveryTryTimes()).incrementAndGet()
* findEligibleTransactionLogs(): 查詢需要處理的事務日誌,條件是:①異步處理次數async_delivery_try_times小於參數最大處裏次數maxDeliveryTryTimes,②transaction_type是BestEffortsDelivery,③系統當前時間與事務日誌的創建時間差要超過參數maxDeliveryTryDelayMillis,每次最多查詢參數size條;Rdb實現通過sql從transaction_log表中查詢,Memory實現遍歷ConcurrentHashMap匹配符合條件的TransactionLog;
* boolean processData():Rdb實現執行TransactionLog中的sql,如果執行過程中拋出異常,那麼調用increaseAsyncDeliveryTryTimes()增加送達重試次數並拋出異常,如果執行成功,刪除事務日誌,並返回true;Memory實現直接返回false(因爲processData()的目的是執行TransactionLog中的sql,而Memory類型無法觸及數據庫,所以返回false)

1.1.2事務日誌存儲核心源碼

RdbTransactionLogStorage.java實現源碼:

public final class RdbTransactionLogStorage implements TransactionLogStorage {

    private final DataSource dataSource;

    @Override
    public void add(final TransactionLog transactionLog) {
        // 保存事務日誌到rdb中的sql
        String sql = "INSERT INTO `transaction_log` (`id`, `transaction_type`, `data_source`, `sql`, `parameters`, `creation_time`) VALUES (?, ?, ?, ?, ?, ?);";
        try (
            Connection conn = dataSource.getConnection();
            PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
            ... ...
            preparedStatement.executeUpdate();
        } catch (final SQLException ex) {
            throw new TransactionLogStorageException(ex);
        }
    }

    @Override
    public void remove(final String id) {
        // 根據id刪除事務日誌的sql
        String sql = "DELETE FROM `transaction_log` WHERE `id`=?;";
        try (
            Connection conn = dataSource.getConnection();
            PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
            preparedStatement.setString(1, id);
            preparedStatement.executeUpdate();
        } catch (final SQLException ex) {
            throw new TransactionLogStorageException(ex);
        }
    }

    @Override
    public List<TransactionLog> findEligibleTransactionLogs(final int size, final int maxDeliveryTryTimes, final long maxDeliveryTryDelayMillis) {
        List<TransactionLog> result = new ArrayList<>(size);
        // 執行該sql查詢需要處理的事務日誌,最多取size條;
        String sql = "SELECT `id`, `transaction_type`, `data_source`, `sql`, `parameters`, `creation_time`, `async_delivery_try_times` FROM `transaction_log` WHERE `async_delivery_try_times`<? AND `transaction_type`=? AND `creation_time`<? LIMIT ?;";
        try (Connection conn = dataSource.getConnection()) {
            try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
                ... ...
                preparedStatement.setLong(3, System.currentTimeMillis() - maxDeliveryTryDelayMillis);
                ... ...
            }
        } catch (final SQLException ex) {
            throw new TransactionLogStorageException(ex);
        }
        return result;
    }

    @Override
    public void increaseAsyncDeliveryTryTimes(final String id) {
        // 更新處理次數+1
        String sql = "UPDATE `transaction_log` SET `async_delivery_try_times`=`async_delivery_try_times`+1 WHERE `id`=?;";
        try (
            ... ...
        } catch (final SQLException ex) {
            throw new TransactionLogStorageException(ex);
        }
    }

    @Override
    public boolean processData(final Connection connection, final TransactionLog transactionLog, final int maxDeliveryTryTimes) {
        try (
            Connection conn = connection;
            // 執行TransactionLog中的sql
            PreparedStatement preparedStatement = conn.prepareStatement(transactionLog.getSql())) {
            for (int parameterIndex = 0; parameterIndex < transactionLog.getParameters().size(); parameterIndex++) {
                preparedStatement.setObject(parameterIndex + 1, transactionLog.getParameters().get(parameterIndex));
            }
            preparedStatement.executeUpdate();
        } catch (final SQLException ex) {
            如果拋出異常,表示執行sql失敗,那麼把增加處理次數並把異常拋出去;
            increaseAsyncDeliveryTryTimes(transactionLog.getId());
            throw new TransactionCompensationException(ex);
        }
        // 如果沒有拋出異常,表示執行sql成功,那麼刪除該事務日誌;
        remove(transactionLog.getId());
        return true;
    }
}

1.1.3事務日誌存儲樣例

transaction_log中存儲的事務日誌樣例:

id transction_type data_source sql parameters creation_time async_delivery_try_times
85c141c4-1b8f-4e54-9010-0cc661bb1864 BestEffortsDelivery db_trans insert into transaction_test(id, remark) values (3, ?) [“TEST BY AFEI.”] 1517899200989 0

1.2最大努力送達型事務監聽器

核心源碼如下:

/**
 * Best efforts delivery B.A.S.E transaction listener.
 * 
 * @author zhangliang
 */
@Slf4j
public final class BestEffortsDeliveryListener {

    @Subscribe
    @AllowConcurrentEvents
    // 從方法可知,只監聽DML執行事件(DML即數據維護語言,包括INSERT, UPDATE, DELETE)
    public void listen(final DMLExecutionEvent event) {
        // 判斷是否需要繼續,判斷邏輯爲:事務存在,並且是BestEffortsDelivery類型事務
        if (!isProcessContinuously()) {
            return;
        }
        // 從柔性事務管理器中得到柔性事務配置
        SoftTransactionConfiguration transactionConfig = SoftTransactionManager.getCurrentTransactionConfiguration().get();
        // 得到配置的柔性事務存儲器
        TransactionLogStorage transactionLogStorage = TransactionLogStorageFactory.createTransactionLogStorage(transactionConfig.buildTransactionLogDataSource());
        // 這裏肯定是最大努力送達型事務(如果不是BEDSoftTransaction,isProcessContinuously()就是false)
        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.getSql(), event.getParameters(), System.currentTimeMillis(), 0));
                return;
            case EXECUTE_SUCCESS: 
                // 如果執行成功事件,那麼刪除事務日誌;
                transactionLogStorage.remove(event.getId());
                return;
            case EXECUTE_FAILURE: 
                boolean deliverySuccess = false;
                // 如果執行成功事件,最大努力送達型最多嘗試3次(可配置,SoftTransactionConfiguration中的參數syncMaxDeliveryTryTimes);
                for (int i = 0; i < transactionConfig.getSyncMaxDeliveryTryTimes(); i++) {
                    // 如果在該Listener中執行成功,那麼返回,不需要再嘗試
                    if (deliverySuccess) {
                        return;
                    }
                    boolean isNewConnection = false;
                    Connection conn = null;
                    PreparedStatement preparedStatement = null;
                    try {
                        conn = bedSoftTransaction.getConnection().getConnection(event.getDataSource(), SQLType.DML);
                        // 通過執行"select 1"判斷conn是否是有效的數據庫連接;如果不是有效的數據庫連接,釋放掉並重新獲取一個數據庫連接;
                        if (!isValidConnection(conn)) {
                            bedSoftTransaction.getConnection().release(conn);
                            conn = bedSoftTransaction.getConnection().getConnection(event.getDataSource(), SQLType.DML);
                            isNewConnection = true;
                        }
                        preparedStatement = conn.prepareStatement(event.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));
                        }
                        // 因爲只監控DML,所以調用executeUpdate()
                        preparedStatement.executeUpdate();
                        // executeUpdate()後能執行到這裏,說明執行成功;根據id刪除事務日誌;
                        deliverySuccess = true;
                        transactionLogStorage.remove(event.getId());
                    } catch (final SQLException ex) {
                        // 如果sql執行有異常,那麼輸出error日誌
                        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源碼總結:
* 執行前,插入事務日誌;
* 執行成功,則刪除事務日誌;
* 執行失敗,則最大努力嘗試syncMaxDeliveryTryTimes次;

1.3 異步送達JOB任務


  • 部署用於存儲事務日誌的數據庫。
  • 部署用於異步作業使用的zookeeper。
  • 配置YAML文件,參照示例文件config.yaml。
  • 下載並解壓文件sharding-jdbc-transaction-async-job-$VERSION.tar,通過start.sh腳本啓動異步作業。

異步送達JOB任務基於elastic-job,所以需要部署zookeeper;
異步送達JOB任務將在下一張詳細講解10. sharding-jdbc源碼之異步送達JOB

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章