×
Community Blog An Interpretation of PolarDB-X Source Codes (10): Life of Transactions

An Interpretation of PolarDB-X Source Codes (10): Life of Transactions

Part 10 of this 10-part series focuses on the codes related to the transaction of the PolarDB-X.

Overview

This article focuses on the codes related to the transaction of the PolarDB-X, highlighting the key codes of the transaction life in the compute nodes (CN) and the entire lifecycle from the beginning, execution, and final commit.

Transactions and Connections

In the CN layer of the PolarDB-X, the connection is closely related to the transaction. This is because the data node (DN) has the capability of transactions within a single DN, and the CN manages transactions on the DN through the connection with the DN, thus achieving strong and consistent distributed transaction capabilities. The connection involved is roughly shown in the following figure:

1
Transactions and Connections 2.png

Let's talk about some of the connections involved. ServerConnection is similar to a frontend connection. Most SQL statements are executed by ServerConnection#innerExecute. The executeSQL method in the TConnection is responsible for the real execution of the SQL statement and creating a new transaction object. The TConnection will reference this transaction object until the transaction is committed or rolled back. The transaction object has a TransactionConnectionHolder that manages all physical connections used by the transaction (the private protocol connection of the CN connecting with DN). It is worth mentioning that this transaction object is referenced by ExecutionContext , a context of a logical SQL execution. As such, when the subsequent executor needs to use the physical connection to communicate with the DN, it can obtain the transaction object through the ExecutionContext and then obtain the appropriate physical connection through the TransactionConnectionHolder of the transaction object.

The above connections will continue to be discussed below.

Two Examples

Let's take two simple examples to illustrate how the life of a transaction is reflected in the codes of the CN.

Test table:

CREATE TABLE `tb1` (
    `id` int PRIMARY KEY,
    `a` int
) DBPARTITION BY HASH(`id`)

First insert a few pieces of data:

INSERT INTO tb1 VALUES (0, 0), (1, 1), (2, 2), (3, 3);

Test the two examples used here:

-- Transaction 1: 
BEGIN; 
SELECT * FROM tb1 WHERE id = 0; 
UPDATE tb1 SET a = 100 WHERE id = 1;
COMMIT;
-- Transaction 2: 
BEGIN; 
SELECT * FROM tb1 WHERE id = 0; 
UPDATE tb1 SET a = 101 WHERE id = 1;
UPDATE tb1 SET a = 101 WHERE id = 0;
COMMIT;

Note: Transaction 2 only modifies one piece of data with id = 0 more than transaction 1. The test table is split by id, so records with id = 0 and id = 1 will fall on different physical shards (assuming shard 0 and shard 1, respectively). Transaction 1 reads shard 0, writes shard 1, and then commits the transaction. This will trigger our stage-one commitment optimization for single-part writes. Transaction 2 reads shard 0, writes shard 1 and shard 0, and commits the transaction. This will perform the complete distributed transaction commit process. These two transactions can also trigger read-only connection optimization. This means distributed transactions are enabled only when they are written for the first time.

In the following discussion, we use the TSO transaction policy and the isolation level of the RR by default.

Life of Transaction 1

BEGIN

Similar to MySQL, there are two ways to start a transaction. The first way is to explicitly execute the BEGIN or START TRANSACTION [transaction_characteristic]. Executing these two statements will call the begin (boolean, IsolationLevel) method of ServerConnection. The second way is to execute the SET autocommit = 0. The current session implicitly starts the transaction. As such, the setAutocommit(boolean, boolean) method in ServerConnection is called. Both methods call the setAutoCommit method of TConnection. These methods just record some variables (such as transaction-related variables set in the transaction_characteristic) and mark the connection to start the transaction. At this point, the transaction object has not been created or interacted with the backend connection.

Read Shard 0

The transaction object is only created after the transaction is enabled and the SELECT * FROM tb1 WHERE id = 0 is executed. According to the discussion in transactions and connections, the execution entry of this logical SQL in the TConnection is executeSQL, which will create transaction objects. The main execution logic is listed below. (The code comes from the release version of PolarDB-X 5.4.13, and there are deletions and modifications for convenience. The following is the same.):

// TConnection#executeSQL(ByteString, Parameters, TStatement, ExecutionContext)
public ResultSet executeSQL(ByteString sql, Parameters params, TStatement stmt,
                                ExecutionContext executionContext) throws SQLException {
  if (this.trx == null || this.trx.isClosed()) {
    // After the transaction is enabled, the transaction object is not created until the first statement is executed. 
    beginTransaction();
  }
  // Allows the executionContext to reference the trx object, which facilitates subsequent executors to use the trx object to obtain a physical connection. 
  executionContext.setTransaction(this.trx);
  resultCursor = executeQuery(sql, executionContext, trxPolicyModified);
}
// TConnection#beginTransaction(boolean)
private void beginTransaction(boolean autoCommit) {
  // According to some default or user-set transaction variables, select an appropriate transaction policy, such as TSO/XA. 
  trxPolicy = loadTrxPolicy(executionContext);
  TransactionClass trxConfig = trxPolicy.getTransactionType(autoCommit, readOnly);
  // Create a transaction object based on the transaction policy. 
  this.trx = transactionManager.createTransaction(trxConfig, executionContext);
}

In our example, if you make a breakpoint in the code above, you can see TsoTransaction is created. Some of the noteworthy variables are listed below:

trx = {TsoTransaction}
    // No timestamps have been obtained. 
    snapshotTimestamp = -1
    commitTimestamp = -1
    // The first physical shard (hereinafter referred to as the primary shard) to which the transaction log is written. 
    primaryGroup = null
    // Specifies whether the transaction is cross-shard. If the transaction is a single-shard transaction, it will be optimized as one phase and submitted. 
    isCrossGroup = false
    // The transaction log manager is responsible for writing transaction logs. 
    globalTxLogManager = {GlobalTxLogManager}
    // Distributed transaction connectionHolder are TransactionConnectionHolder. 
    // The read and write connections are stored separately. The read and the write connections have different behaviors when they are submitted. 
    connectionHolder = {TransactionConnectionHolder}
        // The mapping between physical shards and the corresponding write connection. 
        groupHeldWriteConn = {HashMap}
        // The physical shard is mapped to the corresponding read connection set. When ShareReadView optimization is enabled, 
        // Multiple read connections and one write connection can exist at the same time. Therefore, read and write connections need to be managed separately. 
        // This optimization is not described in detail in this article. 
        groupHeldReadConns = {HashMap}

In the executor stage, it will select to issue a SELECT statement to shard 0. As such, the physical connection of shard 0 needs to be obtained. The code entry is the getPhyConnection(ITransaction, ITransaction.RW, String, DataSource) method in the MyJdbcHandler. The transaction object Transaction is obtained from the ExecutionContext. This method will finally call the getConnection method in AbstractTransaction, which is the base class for all distributed transaction classes.

The code AbstractTransaction#getConnection for taking a physical connection through a transaction is listed below:

// AbstractTransaction#getConnection(String, String, IDataSource, RW, ExecutionContext)
public IConnection getConnection(String schema, String group, IDataSource ds, RW rw, ExecutionContext ec) {
  if (/* is the first write request */ of the transaction) {
    // Use this shard as the primary shard. Transaction logs are written on this shard. 
    this.primaryGroup = group;
    // This shard is used to generate XA transaction xids. 
    this.primaryGroupUid = IServerConfigManager.getGroupUniqueId(schema, group);
  }
  // Use the connectionHolder to obtain a physical connection. 
  IConnection conn = connectionHolder.getConnection(schema, group, ds, rw);
  if (/* is a write request */ && !isCrossGroup && !this.primaryGroup.equals(group)) {
    // The transaction involves multiple shards. 
    this.isCrossGroup = true;
  }
  return conn;
}

In our example, the preceding parameter group is physical shard 0 and rw is READ, indicating that a read connection on physical shard 0 is required. ds is mainly used to generate a physical connection. Since we only read requests, Shard 0 will not be used as the primary shard and will return the connectionHolder.getConnection(schema, group, ds, rw) result.

The following is the code TransactionConnectionHolder#getConnection for taking a physical connection through the connection manager:

// TransactionConnectionHolder#getConnection(String, String, IDataSource, RW)
public IConnection getConnection(String schema, String group, IDataSource ds, RW rw) {
  // Try to obtain the write connection on the shard. If any, directly return the write connection. 
  HeldConnection groupWriteConn = groupHeldWriteConn.get(group);
  if (groupWriteConn != null) {
    return groupWriteConn.connection;
  }

  HeldConnection freeReadConn = /* 尝试找到读连接 */;
  if (freeReadConn != null) {
    if (/* Currently, the connection needs to be written */) {
      // Set the current connection as a write connection. Partitioned = true means that the connection is a write connection. 
      freeReadConn.participated = true;
      this.groupHeldWriteConn.put(group, freeReadConn);
      // As it was a read connection, here it starts distributed transactions. 
      this.trx.commitNonParticipant(group, freeReadConn.connection);
      this.trx.begin(schema, freeReadConn.group, freeReadConn.connection);
    }
    return freeReadConn.connection;
  }
  // The current shard does not have any connection. Create a new connection. According to the read and write type. 
// Set the write connection groupHeldWriteConn or read connection set groupHeldReadConns. 
  IConnection conn = new DeferredConnection ( /* This will obtain and encapsulate the private protocol connection of the shard */ );
  If (/* You need to write a connection */) {
    // Enable normal distributed transactions. 
    this.trx.begin(schema, group, conn);
  } else {
    // Optimize it as a read-only transaction. 
    this.trx.beginNonParticipant(group, conn);
  }
  return conn;
}

In the example, it is the first statement, there is no connection on the shard yet, and it will form a private protocol connection to the shard and be wrapped as a DeferredConnection. Since it is a read request, it will call the beginNonParticipant.

The beginNonParticipant method of TsoTransaction is listed below:

// TsoTransaction#beginNonParticipant(String, IConnection)
protected void beginNonParticipant(String group, IConnection conn) throws SQLException {
  if (snapshotTimestamp < 0) {
    // If the transaction has never received a timestamp, it is obtained here. 
    snapshotTimestamp = nextTimestamp();
  }
  // Use the private protocol pipeline execution mechanism to execute BEGIN. 
  conn.executeLater("BEGIN");
  // Send a timestamp after BEGIN. 
  sendSnapshotSeq(conn);
}

In our example, the private protocol connection will pipeline the BEGIN statement (non-blocking, unequal result return) and send the timestamp later when the physical SQL is executed. At this point, some initialization interfaces on the connection have been completed, and physical SQL statements that read shard 0 can be returned and executed to the executor.

Write Shard 1

Subsequently, we execute the UPDATE tb1 SET a = 100 WHERE id = 1. We need to issue an UPDATE statement to shard 1 in the executor phase. In this case, we need to obtain the physical connection of shard 1. Therefore, the AbstractTransaction#getConnection method is called to obtain a physical connection through the transaction object. Through the code posted earlier, we found that since it is the first write request of the transaction, shard 1 will be regarded as the main shard for generating xids and recording transaction logs later.

When obtaining a physical connection, the TransactionConnectionHolder#getConnection method is called again to take a physical connection through the connection manager. Through the code posted earlier, we found that since shard 1 does not have any connection, a private protocol connection will be generated and wrapped into a DeferredConnection. Unlike Read Shard 0, due to the write request, the begin method of TsoTransaction is executed.

The begin method of TsoTransaction is listed below:

// TsoTransaction#begin(String, String, IConnection)
protected void begin(String schema, String group, IConnection conn) throws SQLException {
  if (snapshotTimestamp < 0) {
    // If the transaction has never received a timestamp, it is obtained here. 
    snapshotTimestamp = nextTimestamp();
  }
// Obtain the xid. 
  String xid = getXid(group);
  // Trigger the private protocol pipeline to execute XA START. 
  conn.executeLater("XA START " + xid);
  // Send a timestamp after XA START. 
  sendSnapshotSeq(conn);
}

In short, the only difference from the previous beginNonParticipant method is that XA START is used to start transactions. According to MySQL documentation on XA transactions, xid consists of gtrid [, bqual [, formatID ]]. Here, the gtrid is drds-transaction id @ primary shard Uid. This ensures the same transaction will use the same gtrid if it executes XA START on different shards. The bqual sets the currently connected shards to determine the shards in which the branch transaction is located when the transaction is resumed. In our example, the xid is: 'drds-13e101d74e400000@5ae6c3b5be613cd1', 'DB1_000001_GROUP', where the 13e101d74e400000 is the transaction id, the 5ae6c3b5be613cd1 is the Uid of shard 1, and the DB1_000001_GROUP is the specific shard name of shard 1. It is worth noting that the previous timestamp will be sent to shard 1 here. At this point, some initialization interfaces on the connection have been completed, and physical SQL statements written to shard 1 can be returned and executed to the executor.

COMMIT

Finally, execute the COMMIT to commit the transaction. The code entry for processing COMMIT is ServerConnection#commit(), which mainly calls TConnection#commit methods. The codes are listed below:

// TConnection#commit()
public void commit() throws SQLException {
  try {
    // It triggers the transaction commit process. 
    this.trx.commit();
  } finally {
    // In most cases, if the transaction is committed successfully or an exception is handled correctly, 
    // All connections are closed and released. trx.close() is equivalent to doing nothing. 
    // However, if the transaction fails to be committed and is not processed, the transaction is rolled back and all physical connections are released. 
    this.trx.close();
    // Remove the reference to this transaction object, which means the end of the transaction life in the current connection. 
    this.trx = null;
  }
}

Let's focus on the commit process of a transaction. When the this.trx.commit() above is called, it will call the commit method of ShareReadViewTransaction. (This class inherits AbstractTransaction and implements the function of shared readview based on XA transaction. XATransaction and TsoTransaction will inherit this class, which can be simply understood as the base class of XA transaction.) The codes are listed below:

// ShareReadViewTransaction#commit()
public void commit() {
  if (!isCrossGroup) {
    // If only a single shard is written, perform one-stage commit optimization. 
    commitOneShardTrx();
  } else {
    // The normal commit process of a multi-sharded distributed transaction. 
    commitMultiShardTrx();
  }
}

In our example, since only shard 1 is written, we enter the first stage of commit optimization with the following codes:

// ShareReadViewTransaction#commitOneShardTrx()
protected void commitOneShardTrx () {
  // Perform the following procedure on all physical connections to commit transactions that are enabled for each physical connection. 
  forEachHeldConnection((group, conn, participated) -> {
      if (!participated) {
        // The read-only connection is a transaction enabled by BEGIN and has only read interfaces. Executing ROLLBACK is enough. 
        conn.execute("ROLLBACK");
      } else {
        // Obtain the xid. 
        String xid = getXid(group);
        // The transaction is started by XA START. Execute XA END and XA COMMIT ONE PHASE to commit the transaction. 
        conn.execute("XA END " + xid + "; XA COMMIT " + xid + " ONE PHASE");
      }
  });
  // All connections commit branch transactions. Release and clear these physical connections. 
  connectionHolder.closeAllConnections();
}

We hold a total of 2 physical connections. The ROLLBACK is executed for read-only connections of shard 0. The XA END and XA COMMIT ONE PHASE are executed to commit transactions for write connections of shard 1. Note: We did not obtain the commit timestamp since the first phase of commit optimization, the commit timestamp will be generated by InnoDB calculation:

The specific calculation rule is COMMIT_TS=MAX_SEQUENCE +1, where MAX_SEQUENCE is the largest snapshot_ts maintained locally by InnoDB in history.

If the commit fails, the close method of the transaction is called. The codes are listed below:

// AbstractTransaction#close()
public void close() {
  // Roll back transactions on all physical connections. 
  cleanupAllConnections();
  // Release and clear these physical connections. 
  connectionHolder.closeAllConnections();
}

It is worth mentioning that cleanupAllConnections() is the method that ROLLBACK statements mainly call. Therefore, to understand the ROLLBACK statement execution process at the same time, let's look at the codes of the cleanupAllConnections method:

// AbstractTransaction#cleanupAllConnections()
protected final void cleanupAllConnections() {
  // Perform the following process on all physical connections that are held to roll back transactions that are enabled for each physical connection. 
  forEachHeldConnection((group, conn, participated) -> {
    if (conn.isClosed()) { return; }
    if (!participated) {
      // The read-only connection is a transaction enabled by BEGIN and only has read interfaces. Just execute ROLLBACK. 
      conn.execute("ROLLBACK");
    } else {
      // Obtain the xid. 
      String xid = getXid(group);
      // The transaction is started by XA START. Execute XA END and XA ROLLBACK to roll back the transaction. 
      conn.execute("XA END " + xid + "; XA ROLLBACK " + xid);
    }
  });
}

As you can see, the logic of rollback is to roll transactions back and forth by ROLLBACK or XA ROLLBACK depending on the situation.

At this point, we see the life of Transaction 1. Next, look at the transaction process of multi-shard writing.

Life of Transaction 2

Write Shard 0

In transaction 2, the process from starting a transaction to executing the UPDATE tb1 SET a = 101 WHERE id = 1 is the same as transaction 1 but is different until the UPDATE tb1 SET a = 101 WHERE id = 0 is executed. Specifically, the transaction obtains the read-only connection of shard 0 before. When it is executed to the TransactionConnectionHolder#getConnection, the transaction on the read-only connection will be submitted first (the ROLLBACK is executed), and then the XA transaction will be started with a XA START in this connection. After the timestamp is set, the physical connection can be returned to the executor and the physical SQL is finally executed. Since this is the second write connection, transactions are set as cross-shard transactions to trigger the normal two-phase commit protocol process.

COMMIT

The focus of transaction 2 is that two shards are written. Therefore, the distributed commit process of commitMultiShardTrx() multi-shard is called during COMMIT. The codes of method commitMultiShardTrx are listed below:

// TsoTransaction#commitMultiShardTrx()
protected void commitMultiShardTrx() {
  // There are three types of state of transaction logs: FAILURE, UNKNODWN, and SUCCESS. 
  TransactionCommitState commitState = TransactionCommitState.FAILURE;
  try {
    // Execute XA prepare for all connections. 
    prepareConnections();
    // Take the commit timestamp. 
    commitTimestamp = nextTimestamp();
    Connection logConn =/* Obtain connections from the primary shard */;
    // All branch transactions are prepared. Before writing transaction logs, the state is set to UNKNOWN. For more information, see the following code description. 
    commitState = TransactionCommitState. UNKNOWN;
    // Write transaction logs. 
    writeCommitLog(logConn);
    // The transaction log is written, whose state is set to SUCCESS. 
    commitState = TransactionCommitState. SUCCESS;
  } catch (RuntimeException ex) {
    exception = ex;
  }
  if (commitState == TransactionCommitState.FAILURE) {
    // If the transaction log fails to be written, roll back all connections. 
    rollbackConnections();
  } else if (commitState == TransactionCommitState.SUCCESS) {
    // The transaction log is written. All connections are committed. 
    commitConnections();
  } else {
    // All connections are prepared, but you cannot determine whether they have been written in transaction logs. 
// It is the logic of transaction recovery that determines whether to COMMIT or ROLLBACK. All connections are discarded here first. 
    discardConnections();
  }

  // Release and clear these physical connections. 
  connectionHolder.closeAllConnections();

  // The exceptions that are thrown in the prepare or commit stage. The exception is thrown again here. 
  if (exception != null) {
      throw exception;
  }
}

The code above is the process committed in the two stages.

In the prepare stage, the prepareConnections() is called, where ROLLBACK statements are executed for read-only connections, and write connections, and XA END {xid} and XA PREPARE {xid} statements are executed.

If all connections are prepared, call writeCommitLog(logConn) in the commit stage, use the connection of the main shard (in our example, shard 1), and write down the transaction log, which mainly includes id and commit timestamp of the transaction. The transaction log is mainly used for subsequent transaction recovery.

The successful writing of the transaction log means the commit is successful. The commitConnections() is called to commit all connections. In this step, only the commit timestamp SET innodb_commit_seq = {commitTimestamp} and XA COMMIT {xid} statements are set for the write connection.

If the transaction log fails to be written before, the rollbackConnections() is called to roll back all connections, especially performing XA ROLLBACK {xid} to rollback on the write connection.

If whether or not the transaction log is written successfully cannot be determined, the transaction state will be UNKNOWN. At this point, we can determine that all branch transactions are successfully prepared. If the transaction log is successfully written, XA COMMIT will be performed. Otherwise, XA ROLLBACK will be performed. Since we are not sure whether or not to commit or roll back, we will discard all physical connections, so these connections are no longer available in the future. As for whether the transaction will be committed or rolled back, it will be handled by the transaction recovery thread. Next, we will interpret the codes related to transaction recovery.

Transaction Recovery

When a distributed transaction is committed, various situations may fail the commit. For example, all branch transactions are prepared and the transaction log is written successfully, but XA COMMIT fails. As such, all branch transactions need to be committed correctly when the transaction recovers. In another case, some branch transaction preparation is successful, while others fail, or the transaction log is not written successfully. Then, the transaction recovery needs to roll back the branch transaction that has been prepared.

Transaction recovery is primarily the responsibility of the XARecoverTask. Its main code is the recoverInstance method, which checks all prepared transactions under a DN and commits or rolls back these transactions according to the transaction state. The code is listed below:

// XARecoverTask#recoverInstance(IDataSource, Set<String>)
// Users of dataSource obtain the connection to the DN. Groups are all physical shards of the current logical database.
private void recoverInstance (IDataSource dataSource, Set<String> groups) {
  // Execute XA RECOVER to obtain all prepared transactions on the DN. 
  // The codes for obtaining a connection from a dataSource and generating a statement are omitted. 
  ResultSet rs = stmt.executeQuery("XA RECOVER");
  while (rs.next()) {
    // For each record, generate an object PreparedXATrans, 
    // Including xid, transaction id, branch transaction shard, primary shard, and other information. 
    PreparedXATrans trans = /* Obtain a row of data from rs to generate*/;
    if (/* The shard where trans is located is a physical shard of the current logical database&&.
        Trans also appeared during the last recover task, 
        That is, it has not been submitted or rolled back for a long time */) {
      // Intervene to handle this branch transaction: rollback or commit. 
      rollBackOrForward(trans, stmt);
    }
  }
}

The task executes a XA RECOVER on each DN every five seconds to obtain all prepared transactions. If a transaction has appeared in the previous task, this means it has not been committed or rolled back for at least five seconds, so it will choose to roll back or commit this transaction. The relevant logic code is rollBackOrForward. The code is listed below:

// XARecoverTask#rollBackOrForward(PreparedXATrans, Statement)
private boolean rollBackOrForward(PreparedXATrans trans, Statement stmt) throws SQLException {
  String primaryGroup = /* Parses the primary shard from trans. For more information, see the preceding interpretation of the generation of xid*/.
  // Try to find relevant transaction logs from those of the primary shard. 
  GlobalTxLog tx = GlobalTxLogManager.get(primaryGroup, trans.transId);
  if (tx != null) {
    // If a transaction log does exist, check whether the transaction log is rolled back or committed. 
    if (tx.getState() == TransactionState.ABORTED) {
      // A transaction in the ABORTED state needs to be rolled back. 
      return tryRollback(stmt, trans);
    } else {
      // The transactions in the SUCCESS state must be committed. 
      return tryCommitTSO(stmt, trans, tx.getCommitTimestamp());
    }
  } else {
    // If no transaction log is found, try to roll back. Start a transaction first, write down the transaction log, and mark the transaction as ABORTED. 
    try (Connection conn2 = /* 获取主分片上的另一个连接 */;) {
      conn2.setAutocommit(false);
      // Try to roll back if another thread is processing the transaction.
      // (For example, the transaction is submitted slowly, but the connection is still disconnected, and the commit process is ongoing).
      // If an error is reported, the statement that writes the transaction log is rolled back. 
      txLog.append(transInfo.transId, TransactionType.XA, TransactionState.ABORTED, new ConnectionContext(), conn2);
      stmt.execute("XA ROLLBACK " + trans.toXid());
      conn2.commit();
      return true;
    } catch (Exception e) {
      /* Determine whether to roll back interfaces of writing a transaction log based on exceptions. */
    }
  }
}

The method mainly has three parts of logic.

First, if there is a corresponding transaction log and the transaction state is ABORTED, perform the tryRollback method to roll back, which mainly executes XA ROLLBACK {xid}.

Second, if the transaction state is SUCCESS, perform the tryCommitTSO method to roll back, which sets the commit timestamp and executes the XA COMMIT {xid}.

Third, if the transaction log is not found, there are generally two possibilities at this time:

1) The transaction commit fails, and the transaction log is not written, which needs to be rolled back.

2) The transaction is still in the two-phase commit protocol process, but prepare is slow, and the transaction log has not been written yet, so no operation is required at this time.

In MySQL, if the connection that initiated the XA START is not closed, other connections cannot roll back or commit this branch transaction through XA ROLLBACK or XA COMMIT. Making good use of this feature, we inserted an ABORTED record in the transaction log to indicate that the distributed transaction needs to be rolled back and then tried to perform XA ROLLBACK to roll back the branch transaction. In the case of 2), a specific error is reported. As such, the interface to insert the ABORTED transaction log is rolled back. After we insert the ABORTED transaction log, the thread that was originally committing the transaction will be blocked when inserting the SUCCESS transaction log. After we roll back the operation of inserting the ABORTED transaction log, the transaction commit process will continue.

Summary

This article mainly interprets the transaction-related code on the CN side of the PolarDB-X, focusing on TSO transactions, using two examples to show the process of starting, executing, committing, and recovering transactions step by step. I hope you can understand the PolarDB-X transaction system better after reading this article.

0 1 0
Share on

ApsaraDB

377 posts | 57 followers

You may also like

Comments

ApsaraDB

377 posts | 57 followers

Related Products