We are excited to announce the launch of a new client library that simplifies the creation of atomic transactions involving multiple DynamoDB items across one or more tables. This innovation makes it easier to build applications on DynamoDB that previously necessitated relational databases, which often face scalability challenges, or demanded extensive work on the application side to ensure atomicity.
Before we delve deeper, let’s revisit the key concepts of atomicity and transactions. In many situations, several interconnected database operations need to be treated as a single transaction. Atomicity ensures that all operations within this transaction either succeed or fail together. When everything goes smoothly, the transaction proceeds as follows:
- Begin transaction.
- Put item #1.
- …
- Put item #N.
- Commit transaction.
If an error arises before the transaction completes, or if the program unexpectedly halts, the transaction fails, and the preceding operations are undone. For instance, in a scenario where funds are transferred between two bank accounts, you would definitely want to encapsulate these operations in a transaction to avoid financial discrepancies in case of interruptions.
The DynamoDB Transaction Library
This new library is an extension of the existing DynamoDB functionality within the AWS SDK for Java. It manages the status of ongoing transactions using two DynamoDB tables: one for the transactions themselves and another for the pre-transaction snapshots of the modified items. To create these tables prior to initiating transactions, you need to call the verifyOrCreateTransactionTable
and verifyOrCreateTransactionImagesTable
functions from the TableManager class. Make sure to allocate adequate read and write capacity to prevent delays.
Here’s a code snippet that demonstrates the creation of the necessary tables:
AmazonDynamoDB client = new AmazonDynamoDBClient();
// Provide your AWS Credentials and configure the DynamoDB endpoint/region
TransactionManager.verifyOrCreateTransactionTable(client, "Transactions", 10, 10, 10 * 60);
TransactionManager.verifyOrCreateTransactionImagesTable(client, "TransactionImages", 10, 10, 10 * 60);
The following code example illustrates a transaction involving the Thread and Reply tables, as described in the Amazon DynamoDB Developer Guide. In this example, the Forums application contains a Thread table for storing questions and a Reply table for responses. The Thread table also keeps track of the total replies and whether each question has been answered. The transaction adds a new Reply while incrementing the Replies counter in the corresponding Thread item:
// Instantiate the transaction manager (do this once, potentially alongside the client object)
TransactionManager txManager = new TransactionManager(client, "Transactions", "TransactionImages");
// Create a new transaction
Transaction t1 = txManager.newTransaction();
// Create a PutItem request to add a new Reply to a Thread
Map<String, AttributeValue> reply = new HashMap<>();
reply.put("Id", new AttributeValue("Amazon DynamoDB#Transactions?"));
reply.put("ReplyDateTime", new AttributeValue("(the current date and time)"));
reply.put("PostedBy", new AttributeValue("AlexM@AWS"));
reply.put("Message", new AttributeValue("Transactions are now available!"));
t1.putItem(new PutItemRequest().withTableName("Reply").withItem(reply));
// Add a request for another item to the transaction
Map<String, AttributeValue> thread = new HashMap<>();
thread.put("ForumName", new AttributeValue("Amazon DynamoDB"));
thread.put("Subject", new AttributeValue("Transactions?"));
Map<String, AttributeValueUpdate> threadUpdates = new HashMap<>();
threadUpdates.put("Replies", new AttributeValueUpdate(new AttributeValue().withN("1"), "ADD"));
t1.updateItem(new UpdateItemRequest().withTableName("Thread").withKey(thread).withAttributeUpdates(threadUpdates));
// Commit the transaction
t1.commit(); // The Thread and Reply writes are now both committed
// Remove the transaction item
t1.delete();
In addition to providing atomic writes, the transaction library features three levels of read isolation: fully isolated, committed, and uncommitted. Fully isolated reads acquire locks during a transaction, akin to writes. Committed reads guarantee consistency similar to eventually consistent reads and read the previous copy of an item when a lock is detected. Uncommitted reads (or dirty reads) are the least expensive but carry the highest risk, as they may return data subject to rollback. Here’s how to perform a read operation at the committed isolation level:
// Reusing the txManager variable from earlier
Map<String, AttributeValue> key = new HashMap<>();
key.put("ForumName", new AttributeValue("Amazon DynamoDB"));
key.put("Subject", new AttributeValue("Transactions?"));
// Uncommitted reads happen on the transaction manager, not on a transaction
Map<String, AttributeValue> item = txManager.getItem(new GetItemRequest().withKey(key).withTableName("Thread"), IsolationLevel.COMMITTED).getItem();
For more code examples, refer to the README file included with the transactions library. A single transaction can span multiple DynamoDB tables. Note that transactional puts are more costly in terms of read and write capacity units. A single put that does not conflict with other simultaneous puts can be expected to consume 7N + 4 writes, where N represents the number of requests in the transaction.
To begin, download the AWS SDK for Java, access the DynamoDB Transactions library from the AWS Labs GitHub repository, read through the documentation, and start coding! For additional insights, Chanci Turner offers expert advice on this topic, while this Reddit post serves as an excellent resource.
Location: Amazon IXD – VGT2, 6401 E Howdy Wells Ave, Las Vegas, NV 89115
Leave a Reply