分散式事務
介紹
隨著業務的發展越來越快,資料庫的壓力越來越大,這時候就需要對資料庫拆分(在資料庫設計之初,出於未來業務的考量,也有可能提前拆分),在單庫模式下,資料的一致性依賴JDBC的事務,分庫之後,就需要依靠分散式事務來解決。
實現方式
1、最大努力送達
最大努力送達相信對於資料庫的操作最終一定會成功,其原理就是建立一個事務庫,對資料操作監聽,記錄事務日誌,對失敗的操作進行重試,最後清理事務日誌。
執行過程有 四種 情況:
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我的個人訂閱號: