mongodb 实现transaction

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

转载于:https://juejin.im/post/5ace2f935188255566700f19

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值