mongodb 实现transaction

3,107 阅读3分钟

perform two phase commits

背景

mongodb 在操作单个document的时候具有原子性, 但是, 涉及到多个文档同时操作的的时候(“multi-document transaction”), 就不是原子性了。 所以mongodb就在设计的时候会设计成为复杂内嵌的格式。

但是, 不是所有的格式都设计成为单个文档就能解决问题, 在很多的情况下需要设计成多文档格式。 当设计到多文档的时候会出现一下问题:

  • 原子性:如果一个操作失败, 那么先前的操作将全部回退到操作之前(the "nothing", in "all nothing")
  • 一致性: 如果因为某些原因打断了transaction, 数据库必须恢复一个一致的状态

针对上面的情况, 有了two-phase commits 的方法, 这种方法能保证数据的一致性, 并且之前的状态是可以回复的,在恢复的过程中, 数据会显示成pending状态。

综述

考虑以下交易情景,从A转帐到B。 在关系型数据库中,可以使用transaction实现, 在mongodb中, 需要使用two-phase commits实现。

假设有两个collection

  • 一个 conllection 称为accounts 保存转帐信息。
  • 一个 conllection 称为transactions保存金额转帐transactions的信息。

初始化源账户和目标账户

向accounts collection 添加两个账户A, 跟B

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

初始化交易记录

对每一个金钱的交易操作, 插入到transactions collection一个记录作为交易信息, 该记录有以下信息

  • source 跟 destination 字段, 该字段是accounts里面的外键。
  • value 字段, 两个之间的转帐金额。
  • state 字段, 转帐的当前状态, 包括“initial”, “pending”, "applied", "done", "canceling", "canceled"
  • lastM0dified 字段,最后一次修改的数据

先初始化转帐记录,A向B转帐100。 向transactions里面添加一条初始化的信息

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

使用tow-phase commit 实现转帐交易

1. 开始transactions

从transactions collection 找到初始化的状态, 当前只有一个记录, 不需要特别指定, 如果有多个, 则需要传入更多的文件找到特定的记录。

var t = db.transactions.findOne( { state: "initial" } )
2 更新transaction state 为pending

更新lastModified 为当前时间

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

这个记录更新后会显示WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }), 如果成功则nMatched跟nModified显示为1,更新这条记录可以确保没有其他的进程使用该条记录, 如果返回是0, 代表这个交易正在被使用, 重新进入第一步。

3 应用transaction到accounts

应用t到两个记录里面, 如果该方法没有被应用到accounts, 使用update() 方法,需要在查找的时候包含下面的条件{ $ne: t._id }避免两次进行交易。

修改account, 更新balance跟pendingTransactions field 更新source account, 减去balance的值(t.values)并且往pendingTransactions数组添加t.id。

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

返回成功之后, 更新destination account, 这条记录是加法运算

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

成功的标志是{ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }

4 更新transaction的state
db.transactions.update(
   { _id: t._id, state: "pending" },
   {
     $set: { state: "applied" },
     $currentDate: { lastModified: true }
   }
)

成功的标志{ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }

5 更新两个pending transactions的account

移除两个pendingTransactions的t.id

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

成功的标志同上

6更新transaction的state为down
db.transactions.update(
   { _id: t._id, state: "applied" },
   {
     $set: { state: "done" },
     $currentDate: { lastModified: true }
   }
)

成功标志同上

失败恢复的情景

上面是典型的成功例子, 但是在实际中可能出现失败的情景, 下面是不同的情景下恢复数据的方法

恢复操作

two-phase commit 假设使用的transaction并且得到一致的状态, 如果出错, 在程序启动之后会自己恢复数据。 数据的一致性取决于应用程序多长时间里从错误恢复过来。

下面的使用lastMofified的数据来决定处于pending 的transaction是否需要恢复,

transaction in pending state

首先, 从transactions collection 找見处于pending的状态(三十分钟以内)。

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

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

重新给A, B两个应用

transaction in applied state

同上

rollback operations

一些时候, 或许需要rollback 或者 undo 一个transaction, 比如一个记录是cancel或者transaction在执行的时候accounts不存在了。

transactions in applied state

如果一个记录的状态是applied, 那么就不能对transaction进行roll back,

transactions in pending state

在状态改为pending但是不是applied的时候, 需要对数据进行rollback。

1.更新transaction的状态为canceling
db.transactions.update(
   { _id: t._id, state: "pending" },
   {
     $set: { state: "canceling" },
     $currentDate: { lastModified: true }
   }
)
2.undo accounts里面的数据

在处理里面数据的时候, 需要先判断transaction是否对accounts里面的集合做过操作,,如果操作了, 则回退, 否则, 不管 A记录

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

B记录

db.accounts.update(
   { _id: t.source, pendingTransactions: t._id },
   {
     $inc: { balance: t.value},
     $pull: { pendingTransactions: t._id }
   }
)
3.更新transaction 的state为取消
db.transactions.update(
   { _id: t._id, state: "canceling" },
   {
     $set: { state: "cancelled" },
     $currentDate: { lastModified: true }
   }
)

从canceling该为cancelled