-
-
Notifications
You must be signed in to change notification settings - Fork 78
TwoPhaseCommit
This is an incomplete rewrite of perform-two-phase-commits using TableDB terms. Just give you an example of emulated transactions. All code is seudo.
This document provides a pattern for doing multi-document updates or
"multi-document transactions" using a two-phase commit approach for
writing data to multiple documents. Additionally, you can extend this
process to provide a rollback-like <2-phase-commits-rollback>
functionality.
Operations on a single table document are always atomic with TableDB databases; however, operations that involve multiple documents, which are often referred to as "multi-document transactions", are not atomic. Since documents can be fairly complex and contain multiple "nested" documents, single-document atomicity provides the necessary support for many practical use cases.
Despite the power of single-document atomic operations, there are cases that require multi-document transactions. When executing a transaction composed of sequential operations, certain issues arise, such as:
-
Atomicity: if one operation fails, the previous operation within the transaction must "rollback" to the previous state (i.e. the "nothing," in "all or nothing").
-
Consistency: if a major failure (i.e. network, hardware) interrupts the transaction, the database must be able to recover a consistent state.
For situations that require multi-document transactions, you can
implement two-phase commit in your application to provide support for
these kinds of multi-document updates. Using two-phase commit ensures
that data is consistent and, in case of an error, the state that
preceded the transaction is recoverable <2-phase-commits-rollback>
. During the procedure, however, documents
can represent pending data and states.
Because only single-document operations are atomic with TableDB, two-phase commits can only offer transaction-like semantics. It is possible for applications to return intermediate data at intermediate points during the two-phase commit or rollback.
Consider a scenario where you want to transfer funds from account A
to account B
. In a relational database system, you can subtract the
funds from A
and add the funds to B
in a single multi-statement
transaction. In TableDB, you can emulate a two-phase commit to achieve
a comparable result.
The examples in this tutorial use the following two collections:
#. A collection named accounts
to store account information.
#. A collection named transactions
to store information on the fund
transfer transactions.
Insert into the accounts
collection a document for account A
and a document for account B
.
db.accounts.insert({
{ _id= "A", balance: 1000, pendingTransactions= {} },
{ _id= "B", balance: 1000, pendingTransactions= {} }
})
For each fund transfer to perform, insert into the transactions
collection a document with the transfer information. The document
contains the following fields:
-
source
anddestination
fields, which refer to the_id
fields from theaccounts
collection, -
value
field, which specifies the amount of transfer affecting thebalance
of thesource
anddestination
accounts, -
state
field, which reflects the current state of the transfer. Thestate
field can have the value ofinitial
,pending
,applied
,done
,canceling
, andcanceled
. -
lastModified
field, which reflects last modification date.
To initialize the transfer of 100
from account A
to account
B
, insert into the transactions
collection a document with the
transfer information, the transaction state
of "initial"
, and
the lastModified
field set to the current date:
db.transactions.insert(
{ _id= 1, source= "A", destination="B", value= 100, state= "initial", lastModified= Date() }
)
The most important part of the transaction procedure is not the prototypical example above, but rather the possibility for recovering from the various failure scenarios when transactions do not complete successfully. This section presents an overview of possible failures and provides steps to recover from these kinds of events.
The two-phase commit pattern allows applications running the sequence to resume the transaction and arrive at a consistent state. Run the recovery operations at application startup, and possibly at regular intervals, to catch any unfinished transactions.
The time required to reach a consistent state depends on how long the application needs to recover each transaction.
The following recovery procedures uses the lastModified
date as an
indicator of whether the pending transaction requires recovery;
specifically, if the pending or applied transaction has not been
updated in the last 30 minutes, the procedures determine that these
transactions require recovery. You can use different conditions to make
this determination.
To recover from failures that occur after step
"Update transaction state to pending.
" but before
"Update transaction state to applied.
" step, retrieve from
the transactions
collection a pending transaction for recovery:
local dateThreshold = Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
local t = db.transactions.findOne( { state= "pending", lastModified= { _lt= dateThreshold } } );
And resume from step "Apply the transaction to both accounts.
_"
To recover from failures that occur after step
"Update transaction state to applied.
" but before
"Update transaction state to done.
" step, retrieve from
the transactions
collection an applied transaction for recovery:
local dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
local t = db.transactions.findOne( { state= "applied", lastModified= { _lt= dateThreshold } } );
And resume from
"Update both accounts' list of pending transactions.
_"
In some cases, you may need to "roll back" or undo a transaction; e.g., if the application needs to "cancel" the transaction or if one of the accounts does not exist or stops existing during the transaction.
After the "Update transaction state to applied.
_" step, you should
not roll back the transaction. Instead, complete that transaction
and :ref:create a new transaction <initialize-transfer-record>
to
reverse the transaction by switching the values in the source and the
destination fields.
After the "Update transaction state to pending.
" step, but before the
"Update transaction state to applied.
" step, you can rollback the
transaction using the following procedure:
Transactions exist, in part, so that multiple applications can create
and run operations concurrently without causing data inconsistency or
conflicts. In our procedure, to update or retrieve the transaction
document, the update conditions include a condition on the state
field to prevent reapplication of the transaction by multiple
applications.
For example, applications App1
and App2
both grab the same
transaction, which is in the initial
state. App1
applies the
whole transaction before App2
starts. When App2
attempts to
perform the "Update transaction state to pending.
_" step, the update
condition, which includes the state: "initial"
criterion.
When multiple applications are running, it is crucial that only one
application can handle a given transaction at any point in time. As
such, in addition including the expected state of the transaction in
the update condition, you can also create a marker in the transaction
document itself to identify the application that is handling the
transaction. Use :method:~db.collection.findAndModify()
method to
modify the transaction and get it back in one step:
t = db.transactions.findAndModify(
{
query= { state= "initial", application={ exists=false } },
update=
{
_set= { state= "pending", application="App1" },
currentDate= { lastModified=true }
},
new= true
}
)
Amend the transaction operations to ensure that only applications that
match the identifier in the application
field apply the transaction.
If the application App1
fails during transaction execution, you can
use the recovery procedures <2-phase-commits-recovery>
, but
applications should ensure that they "own" the transaction before
applying the transaction. For example to find and resume the pending
job, use a query that resembles the following:
local dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
db.transactions.find(
{
application= "App1",
state= "pending",
lastModified= { _lt= dateThreshold }
}
)
The example transaction above is intentionally simple. For example, it assumes that it is always possible to roll back operations to an account and that account balances can hold negative values.
Production implementations would likely be more complex. Typically, accounts need information about current balance, pending credits, and pending debits.
Download Paracraft | ParacraftSDK | copyright by tatfook 2016 | upload image