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