Getting started with Tablestore local transaction

Keywords: Java

Introduction to local affairs

The local transaction provided by table storage can also be called partition key transaction: you can specify that the operation under a partition key is atomic, either all successful or all failed, and the isolation level provided is serialization. In other words, the local transaction of table storage can prevent the following problems

  1. Dirty reads: uncommitted writes read by operations other than transactions
  2. Dirty write: writes outside the transaction overwrite uncommitted writes of the transaction
  3. Non repeatable read: multiple read operations on the same row of data in a transaction read different values
  4. Update lost: this transaction is overwritten by other transactions executed in parallel after it has been submitted (unlike dirty write, which occurs when both transactions have not been submitted)

The basic usage process of local transaction is shown in the figure below

When a local transaction of a Tablestore starts a transaction or first obtains a lock under the partition key, all subsequent write operations and start transaction operations to the partition key will be blocked to the original transaction submission or timeout to ensure the isolation of the operation. Some features are as follows:

  1. Before a transaction is committed or aborted, no other transaction can be started under the same partition key
  2. Writes that are not part of this transaction will be blocked or timed out before the transaction is committed or aborted
  3. Before the transaction is committed and aborted, uncommitted writes in the transaction cannot be read by read operations other than this transaction, while uncommitted writes in this transaction can be read by read operations of this transaction

Use of local transactions

Transaction start, commit and abort

Start transaction

// Local transaction needs to specify a partition key (the first column primary key)
PrimaryKey transactionPK = new PrimaryKey(Collections.singletonList(
    new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId))
));
StartLocalTransactionRequest startTransactionRequest = new StartLocalTransactionRequest(TABLE_NAME, transactionPK);
StartLocalTransactionResponse startTransactionResponse = syncClient.startLocalTransaction(startTransactionRequest);
// After the transaction is started successfully, a transactionId will be returned. Any request that implements the TxnRequest abstract class can specify the transaction through the setTransactionId method
final String transactionId = startTransactionResponse.getTransactionID();

Submission of affairs

// Commit the transaction, and all writes related to the transactionId will be permanently written to the Tablestore
syncClient.commitTransaction(new CommitTransactionRequest(transactionId));

Suspension of business

// Abort the transaction, and all write operations related to changing the transactionId will be rolled back
syncClient.abortTransaction(new AbortTransactionRequest(transactionId));

Basic Usage

Write the data twice with the same transactionId and submit, either fail all or fail all
Write the first row of data

PutRowRequest typeAPutRequest = new PutRowRequest(new RowPutChange(
    TABLE_NAME,
    new PrimaryKey(Arrays.asList(
        new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
        new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeA))
    ))
).addColumn(COLUMN_CONTENT, ColumnValue.fromString("content_a")));
// Set transaction ID, returned by start transaction
typeAPutRequest.setTransactionId(transactionId);
syncClient.putRow(typeAPutRequest);

Read the data written above through the transaction ID

GetRowRequest getRowRequest = new GetRowRequest();
SingleRowQueryCriteria singleRowQueryCriteria = new SingleRowQueryCriteria(
    TABLE_NAME,
    new PrimaryKey(Arrays.asList(
        new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
        new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeA))
    ))
);
singleRowQueryCriteria.setMaxVersions(1);
getRowRequest.setRowQueryCriteria(singleRowQueryCriteria);
// Transaction ID needs to be set here
getRowRequest.setTransactionId(transactionId);
GetRowResponse getRowResponse = syncClient.getRow(getRowRequest);
// Data can be acquired normally
Assert.assertNotNull(getRowResponse.getRow());

Not read by transaction ID

GetRowRequest getRowRequest = new GetRowRequest();
SingleRowQueryCriteria singleRowQueryCriteria = new SingleRowQueryCriteria(
    TABLE_NAME,
    new PrimaryKey(Arrays.asList(
        new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
        new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeA))
    ))
);
singleRowQueryCriteria.setMaxVersions(1);
getRowRequest.setRowQueryCriteria(singleRowQueryCriteria);
GetRowResponse getRowResponse = syncClient.getRow(getRowRequest);
// The transaction ID acquisition is not set. Because the isolation level provided by the Tablestore is serialization, uncommitted writes cannot be read here
Assert.assertNull(getRowResponse.getRow());

Write the second line of data

PutRowRequest typeBPutRequest = new PutRowRequest(new RowPutChange(
    TABLE_NAME,
    new PrimaryKey(Arrays.asList(
        new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
        new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeB))
    ))
).addColumn(COLUMN_CONTENT, ColumnValue.fromString("content_b")));
typeBPutRequest.setTransactionId(transactionId);
syncClient.putRow(typeBPutRequest);

Submission

// Commit the transaction. After the transaction is committed, other read operations can get the previous write
syncClient.commitTransaction(new CommitTransactionRequest(transactionId));

Suspension of business

// Abort the transaction, and all write operations related to changing the transactionId will be rolled back
syncClient.abortTransaction(new AbortTransactionRequest(transactionId));

After the transaction is started, other operations other than this transaction attempt to write

This example shows a scenario in which there is another write with the same partition key during the transaction execution. Since starting a transaction under the partition key will directly lock all the write operations under the partition key, any write operations to the same partition will be blocked until the transaction commits or times out during the transaction execution. The following flow chart shows some scenarios of two threads writing through transactions:

Write first line

// put row A with transactionID
PutRowRequest typeAPutRequest = new PutRowRequest(new RowPutChange(
    TABLE_NAME,
    new PrimaryKey(Arrays.asList(
        new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
        new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeA))
    ))
).addColumn(COLUMN_CONTENT, ColumnValue.fromString("content_a")));
// set transactionId in startTransactionResponse
typeAPutRequest.setTransactionId(transactionId);
syncClient.putRow(typeAPutRequest);

When writing the second line, there is no transaction ID, and an attempt to write is made to simulate an operation other than this transaction. The write fails. The error code returned is OTSRowOperationConflict

// put row B without transactionID
PutRowRequest typeBPutRequest = new PutRowRequest(new RowPutChange(
    TABLE_NAME,
    new PrimaryKey(Arrays.asList(
        new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
        new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeB))
    ))
).addColumn(COLUMN_CONTENT, ColumnValue.fromString("content_b")));
// put without transactionId, due to PK_USE_ID is locked by another transaction, this action will fail
try {
    syncClient.putRow(typeBPutRequest);
    Assert.fail();
} catch (TableStoreException e) {
    // ok
    Assert.assertEquals("OTSRowOperationConflict", e.getErrorCode());
}

Batch writes

Write the first and second lines using batch

BatchWriteRowRequest batchWriteRowRequest = new BatchWriteRowRequest();
batchWriteRowRequest.addRowChange(new RowPutChange(TABLE_NAME, new PrimaryKey(Arrays.asList(
    new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
    new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeA))
))).addColumn(COLUMN_CONTENT, ColumnValue.fromString("content_a")));
batchWriteRowRequest.addRowChange(new RowPutChange(TABLE_NAME, new PrimaryKey(Arrays.asList(
    new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
    new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeB))
))).addColumn(COLUMN_CONTENT, ColumnValue.fromString("content_b")));
batchWriteRowRequest.setTransactionId(transactionId);
syncClient.batchWriteRow(batchWriteRowRequest);

Write the third and fourth lines with batch

BatchWriteRowRequest batchWriteRowRequest = new BatchWriteRowRequest();
batchWriteRowRequest.addRowChange(new RowPutChange(TABLE_NAME, new PrimaryKey(Arrays.asList(
    new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
    new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeC))
))).addColumn(COLUMN_CONTENT, ColumnValue.fromString("content_c")));
batchWriteRowRequest.addRowChange(new RowPutChange(TABLE_NAME, new PrimaryKey(Arrays.asList(
    new PrimaryKeyColumn(PK_USER_ID, PrimaryKeyValue.fromString(userId)),
    new PrimaryKeyColumn(PK_TYPE, PrimaryKeyValue.fromString(typeD))
))).addColumn(COLUMN_CONTENT, ColumnValue.fromString("content_d")));
batchWriteRowRequest.setTransactionId(transactionId);
syncClient.batchWriteRow(batchWriteRowRequest);

Posted by jesushax on Mon, 09 Dec 2019 13:48:23 -0800