perform two phase commits

background

Mongodb is atomic for single documents, but not atomic for “multi-document transactions” involving multiple documents. So mongodb is designed to be a complex embedded format.

However, not all formats are designed as a single document, and in many cases multiple document formats are required. The following problems occur when designing for multiple documents:

  • Atomicity: If an operation fails, all previous operations fall back to before (the “nothing”, in “all nothing”).
  • Consistency: If a transaction is interrupted for some reason, the database must be restored to a consistent state

In response to the above situation, two-phase commits ensure that the data are consistent and recoverable. During recovery, the data are displayed as pending.

review

Consider the following transaction scenario, transferring money from A to B. In relational databases, transaction can be implemented; in mongodb, two-phase commits are required.

Suppose there are two collections

  • A conllection called accounts holds transfer information.
  • A conllection called transactions holds information about the amount of money transferred transactions.

Example Initialize the source account and target account

Add two accounts A and B to the Accounts collection

db.accounts.insert(
  [
    { _id: "A", balance: 1000, pendingTransactions: [] },
    { _id: "B", balance: 1000, pendingTransactions: [] }
  ]
)
Copy the code

Initialize the transaction record

For each money transaction operation, insert a record into the Transactions Collection as the transaction information, which has the following information

  • Source and Destination fields, which are foreign keys inside accounts.
  • Value field, the amount of money transferred between two.
  • State, the current state of the transfer, including “initial “,” pending “, “applied”, “done”, “Canceling “, and” Canceled “.
  • LastM0dified field, last modified data

Initialize the transfer record. A transfers 100 to B. Add an initialization message to transactions

db.transactions.insert(
    { _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() }
)
Copy the code

Use TOW-Phase Commit to implement transfer transactions

1. Start the transactions

Transactions Collection initializes the state of the transactions collection. There is currently only one record and you do not need to specify it. If there are more than one, you need to pass in more files to find the specific record.

var t = db.transactions.findOne( { state: "initial"})Copy the code
2 Change transaction state to Pending

Update lastModified to the current time

db.transactions.update(
    { _id: t._id, state: "initial" },
    {
      $set: { state: "pending" },
      $currentDate: { lastModified: true}})Copy the code

{“nMatched” : 1, “nUpserted” : 0, “nModified” : 1}), if successful, nMatched and nModified show 1, updating this record can ensure that no other process uses this record, if returned is 0, it means that this transaction is being used, re-enter the first step.

3 Apply transaction to accounts

Apply t to both records. If the method is not applied to accounts, use the update() method and include the condition {$ne: t._id} to avoid trading twice.

Update the source Account, subtract the balance value (t.values) and add T.ID to the pendingTransactions array.

db.accounts.update(
   { _id: t.source, pendingTransactions: { $ne: t._id } },
   { $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)
Copy the code

After returning success, update the Destination Account, which is an addition record

db.accounts.update(
   { _id: t.destination, pendingTransactions: { $ne: t._id } },
   { $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)
Copy the code

The sign of success is {“nMatched” : 1, “nUpserted” : 0, “nModified” : 1}

4 Update the transaction state
db.transactions.update(
   { _id: t._id, state: "pending" },
   {
     $set: { state: "applied" },
     $currentDate: { lastModified: true}})Copy the code

{“nMatched” : 1, “nUpserted” : 0, “nModified” : 1}

5 Update the accounts of two Pending Transactions

Remove T.ID for both pendingTransactions

db.accounts.update(
   { _id: t.source, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } }
)
Copy the code
db.accounts.update(
   { _id: t.destination, pendingTransactions: t._id },
   { $pull: { pendingTransactions: t._id } }
)
Copy the code

The same sign of success

6 Change the state of transaction to Down
db.transactions.update(
   { _id: t._id, state: "applied" },
   {
     $set: { state: "done" },
     $currentDate: { lastModified: true}})Copy the code

Same as above

Failed recovery scenario

The above is a typical successful example, but there may be a failure scenario in practice. The following are the methods to recover data in different scenarios

Restore operation

Two-phase Commit assumes that the transaction used is in a consistent state, and if an error occurs, the data is recovered after the program is started. Data consistency depends on how long it takes an application to recover from an error.

The following uses lastMofified data to determine whether a pending transaction needs to be resumed,

transaction in pending state

First, find the pending state from transactions Collection (within 30 minutes).

var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

var t = db.transactions.findOne( { state: "pending", lastModified: { $lt: dateThreshold } } );
Copy the code

Let’s reapply A and B

transaction in applied state

Same as above

rollback operations

At some point, it may be necessary to rollback or undo a transaction, such as a record that is cancel or the accounts did not exist at the time the transaction was executed.

transactions in applied state

If the state of a record is applied, then transaction cannot be rolled back,

transactions in pending state

When the state is changed to Pending but not applied, the data needs to be rolled back.

1. Update transaction’s state to Computational
db.transactions.update(
   { _id: t._id, state: "pending" },
   {
     $set: { state: "canceling" },
     $currentDate: { lastModified: true}})Copy the code
2. Undo the data in accounts

If transaction has performed any operation on the set in the accounts, then rollback will be performed. Otherwise, no transaction will be performed on the set in the accounts

db.accounts.update(
   { _id: t.destination, pendingTransactions: t._id },
   {
     $inc: { balance: -t.value },
     $pull: { pendingTransactions: t._id }
   }
)
Copy the code

B records

db.accounts.update(
   { _id: t.source, pendingTransactions: t._id },
   {
     $inc: { balance: t.value},
     $pull: { pendingTransactions: t._id }
   }
)
Copy the code
Update transaction state to cancel
db.transactions.update(
   { _id: t._id, state: "canceling" },
   {
     $set: { state: "cancelled" },
     $currentDate: { lastModified: true}})Copy the code

From canceling to cancelled