MongoDB官方文档翻译系列之 -- 执行二阶段提交

简介

本篇文档提供了一个使用二阶段提交将数据写入多个文档的方法来处理多文档更新或“多文档事务”。在此基础上,你可以扩展实现类似数据回滚的功能。

背景

在MongoDB数据库中,作用于单个document的操作总是原子性的;但是,涉及到多个document的操作,也就是我们常说的“多文档事务”,是非原子性的。 由于document可以设计的非常复杂并且能包含多个“内嵌”document,因此单文档原子性对很多实际场景提供了必要的支持。(译者注:比如你要批量更新某批商品的出厂日期,可以将这些商品信息放在同一个document中做内嵌。但是我几乎没有使用过这种方法,会有很多额外的问题,比如频繁操作会导致document
move。)

尽管单文档原子操作能满足不少需求,但是在很多场景下仍然需要多文档事务的支持。当执行一个由几个顺序操作组成的事务时,可能会出现某些问题,例如:

  • 原子性: 如果某个操作失败了,同一个事务内发生在它之前的所有操作必须“回滚”到最初的状态(即“要么全OK,要么什么也不做”)。
  • 一致性: 如果发生了严重故障将事务中断(网络、硬件故障),数据库必须恢复到一致的状态。

对于需要多文档事务的场景,你可以在应用中实现二阶段提交来提供支持。二阶段提交可以保证数据的一致性,如果发生错误,事务前的状态是可恢复的。在事务执行过程中,无论发生什么情况都可以还原到数据和状态的准备阶段。

注意

因为在MongoDB中只有单文档操作是原子性的,二阶段提交只能提供类似事务的语义。在二阶段提交或回滚进行中,应用程序可以返回任意步骤点的中间数据。

模式

概述

考虑这样一个场景,你想从账户A转账给账户B。在关系型数据库系统中,你可以在单个多语句事务中先减少账户A的资金然后为账户B增加资金。在MongoDB中,你可以模拟实现一个二阶段提交得到同样的结果。

本节中的所有示例使用下面两个集合:

1.     集合accounts保存账户信息。

2.     集合transactions保存转账事务信息。

初始化源账户和目标账户

将账户A和账户B的信息写入到集合accounts。

db.accounts.insert(

[

{ _id: "A", balance: 1000, pendingTransactions: [] },

{ _id: "B", balance: 1000, pendingTransactions: [] }

]

)

上面的语句返回一个BulkWriteResult()对象,包含了本次操作的状态信息。如果成功写入,BulkWriteResult()对象中的 nInserted的值为2。(译者注:在2.6版本后写操作都会返回WriteResult对象,批量写会返回BulkWriteResult,具体请见相关章节)

初始化转帐数据

将每笔转账信息写入到transactions表,转账数据包含以下字段:

  • source 和 destination字段, 指向accounts集合中的_id值
  • value字段,表示转账金额,影响源账户和目标账户的余额
  • state 字段,表示转账操作当前状态,state字段可选值范围为initial, pending, applied, done, canceling和 canceled
  • lastModified 字段,表示最后更新时间

将账户A向账户B转账100的操作信息初始化到transactions集合, state字段值为"initial", lastModified字段值设为当前时间:

db.transactions.insert(

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

)

上面的语句返回一个WriteResult()对象,包含了本次操作的状态信息,如果写入成功, WriteResult()对象的 nInserted值为1。

使用二阶段提交转账

?  获取transaction集合的数据

从transactions集合查找一条state字段值为initial的数据。当前transactions集合中只有一条数据,也就是说我们在上文 初始化转账数据 这个步骤只写入了一条数据。如果集合中有另外的数据,下面的查询会返回任意state字段为initial的数据,除非你附加一些别的查询条件。

var t = db.transactions.findOne( { state: "initial" } )

在 mongo shell中定义变量t来打印返回的内容。上边的语句会得到如下输出:

{ "_id" : 1, "source" : "A", "destination" : "B", "value" : 100, "state" : "initial", "lastModified" : ISODate("2014-07-11T20:39:26.345Z") }

?  将transaction数据的state字段设为pending

将transaction数据的state字段从initial设为pending,并用 $currentDate 操作将lastModified字段设为当前时间。

db.transactions.update(

{ _id: t._id, state: "initial" },

{

$set: { state: "pending" },

$currentDate: { lastModified: true }

}

)

这个更新操作会返回一个WriteResult()对象,包含本次更新操作的状态信息,如果更新成功,nMatched 和 nModified 显示为1。

在这个更新语句中state: "initial" 条件确保没有其它线程更新过本条数据。如果nMatched和 nModified为0,回到第一步重新获取一条数据然后继续按步骤进行。

?  对账户进行转账

如果账户不包含transaction信息,用 update()方法更新帐户信息,
在更新条件中带有pendingTransactions: {$ne: t._id },这是为了避免重复同一次转账。

同时更新balance字段和pendingTransactions字段来实现转账。

更新源账户信息,为balance字段减去transaction 数据的value 值,并将transaction 的_id写入到pendingTransactions字段的数组中。

db.accounts.update(

{ _id: t.source, pendingTransactions: { $ne: t._id } },

{ $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }

)

操作成功后,方法会返回WriteResult() 对象, nMatched 和nModified值为1。

更新目标账户信息,为balance字段加上transaction 数据的value 值,并将transaction 的_id写入到pendingTransactions字段的数组中。

db.accounts.update(

{ _id: t.destination, pendingTransactions: { $ne: t._id } },

{ $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }

)

操作成功后,方法会返回 WriteResult() 对象, nMatched 和nModified 值为1。

?  将transaction数据的state设为applied

用下面的update()操作将transaction数据的state 值设为applied  operation to set the
transaction’s state to applied,并更新lastModified字段值为当前时间:

db.transactions.update(

{ _id: t._id, state: "pending" },

{

$set: { state: "applied" },

$currentDate: { lastModified: true }

}

)

操作成功后,方法会返回 WriteResult() 对象, nMatched 和nModified 值为1。

?  将transaction 数据的_id值从两个账户的pendingTransactions字段中移除

从两个账户中的pendingTransactions 字段中移除state值为applied的 transaction数据的 _id值。

更新源账户

db.accounts.update(

{ _id: t.source, pendingTransactions: t._id },

{ $pull: { pendingTransactions: t._id } }

)

操作成功后,方法会返回 WriteResult() 对象, nMatched 和nModified 值为1。

更新目标账户

db.accounts.update(

{ _id: t.destination, pendingTransactions: t._id },

{ $pull: { pendingTransactions: t._id } }

)

操作成功后,方法会返回 WriteResult() 对象, nMatched 和nModified 值为1。

?  更新transaction数据的 state值为done.

将transaction 数据的state设为 done ,更新lastModified为当前时间,这也标志着本次事务的结束。

db.transactions.update(

{ _id: t._id, state: "applied" },

{

$set: { state: "done" },

$currentDate: { lastModified: true }

}

)

操作成功后,方法会返回 WriteResult() 对象, nMatched 和nModified 值为1。

从失败场景恢复

其实最重要的部分不是上面示例中比较顺的场景,重要的当事务未成功完成时有没有可能从各种各样失败情况中恢复。这部分会概括各种可能出现的失败场景,并教你一些步骤,如何从这些事件中恢复。

恢复操作

二阶段提交模式允许应用程序有序的运行一些操作来恢复事务并达到一致性状态。在应用启动时运行恢复程序,可能是个定期执行的程序,用来捕获任何未完成的事务。

在一致性问题上对于时间的需求取决于应用间隔多长时间为每个事务进行恢复。

接下来举例的恢复程序根据lastModified字段做为指标来决定pending状态的事务是否需要进行恢复; 再具体点,如果pending 或 applied 状态的事务在30分钟内未更新过,恢复程序会认为这些事务需要进行恢复。你可以用不同的条件来决定事务是否需要恢复。

pending状态的事务

要恢复发生在上文举例的“将transaction数据的state设为pending.
步骤之后,但发生在 “将transaction数据的 state设为applied.“步骤之前的错误,先从transactions集合中获取一条pending状态的数据:

var dateThreshold = new Date();

dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

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

然后从上文的 “对账户进行转账“步骤开始继续执行

applied状态的事务

要恢复发生在上文举例的 “将transaction数据的state设为applied”步骤之后,但发生在 but before “将transaction数据的state设为done.“步骤之前的错误,先从transactions集合中获取一条applied状态的数据:

var dateThreshold = new Date();

dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

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

然后从上文的“U将transactions数据的_id值从两个账户信息的pendingTransactions字段中删除.“步骤开始继续执行

回滚操作

在一些情况下,你可能需要“回滚”或取消事务;举例来说,比如应用程序主观的需要去“取消”事务,或者事务中的某个账户不存在,或者说在事务进行中账户不复存在了。

applied状态的事务

在 “将transaction数据state设为applied.”步骤之后,你最好不要回滚事务了。取而代之的方式应该是,完成这个事务,然后新启一个事务,将上个事务的源账户和目标账户调换一下,再做一次转账。

pending状态的事务

在 “将transaction数据 state设为pending.”步骤之后,在
将 transaction数据 state设为applied.”步骤之前,你可以根据下面的流程来回滚事务:

?  将transaction数据state设为canceling

将transaction的 state 从pending 设为canceling。

db.transactions.update(

{ _id: t._id, state: "pending" },

{

$set: { state: "canceling" },

$currentDate: { lastModified: true }

}

)

操作成功后,方法会返回 WriteResult() 对象, nMatched 和nModified 值为1。

?  在两个账户上撤消事务

对两个账户做反向操作来撤消事务,在update条件的中加上pendingTransactions: t._id来筛选满足条件的数据。

更新目标账户信息,在balance  字段上减去transaction 数据的value  值,并将transaction 数据的 _id 从pendingTransactions 数组中移除。

db.accounts.update(

{ _id: t.destination, pendingTransactions: t._id },

{

$inc: { balance: -t.value },

$pull: { pendingTransactions: t._id }

}

)

操作成功后,方法会返回 WriteResult() 对象, nMatched 和nModified 值为1。
如果转账事务在之前没有发生在该账户上,那么上面的更新操作匹配不到数据, nMatched and nModified 值会是0。

更新源账户信息,在balance字段上加上transaction数据的 value值,并将transaction 数据的 _id 从pendingTransactions 数组中移除。

db.accounts.update(

{ _id: t.source, pendingTransactions: t._id },

{

$inc: { balance: t.value},

$pull: { pendingTransactions: t._id }

}

)

操作成功后,方法会返回 WriteResult() 对象, nMatched 和nModified 值为1。如果转账事务在之前没有发生在该账户上,那么上面的更新操作匹配不到数据, nMatched and nModified 值会是0。

?  将transaction数据state设为canceled

将transaction 数据的state 从canceling 设为cancelled来完成最终的回滚 。

db.transactions.update(

{ _id: t._id, state: "canceling" },

{

$set: { state: "cancelled" },

$currentDate: { lastModified: true }

}

)

操作成功后,方法会返回 WriteResult() 对象, nMatched 和nModified 值为1。

多应用

事务的存在,从某种程度上说,是为了便于多个应用并发的创建和执行操作,而不会引发数据不一定和数据冲突。在我们的程序中,更新或获取transaction集合的数据时,更新条件中都会包含state 字段条件,这能防止多应用冲突的申请transaction 数据。

例如,App1和App2同时获取了某条相同的state为initial的transaction 数据。在App2开工前,App1执行了完整的事务,当App2试图执行步骤 “将transaction数据state设为pending.”时,由于更新条件中包含有state: "initial"语句,更新操作匹配不到数据,nMatched 和nModified值会是0。这会让App2返回到第一步去获取另一条transaction数据重新开始事务流程。

当多个应用运行时,最关键的是在任意时刻只能有唯一一个应用能操作一条给定的transaction 数据。同样的,除了在更新条件中包含预期的事务状态之外,你还可以为transaction 数据创建一个标记来鉴别正在操作该transaction数据的应用。用findAndModify()方法原子性的修改并返回transaction数据:

t = db.transactions.findAndModify(

{

query: { state: "initial", application: { $exists: false } },

update:

{

$set: { state: "pending", application: "App1" },

$currentDate: { lastModified: true }

},

new: true

}

)

修改之前例子中的事务操作,可以确保只有匹配上application字段标识的应用才能操作相应的transaction数据。

如果App1在事务执行过程中失败了,你可以用恢复程序进行恢复,但是在恢复之前,应用程序必须确定它们“拥有”相应的transaction数据。例如要找到并继续执行一个pending状态的事务,
使用类似下面的查询:

var dateThreshold = new Date();

dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);

db.transactions.find(

{

application: "App1",

state: "pending",

lastModified: { $lt: dateThreshold }

}

)

在生产环境下使用二阶段提交

本文中的账户事务例子故意体现的很简单。比如,我们假设总能够对账户做回滚操作,并且账户余额是负数。

生产环境中的实现可能会更复杂一些,例如真实场景下账户需要的信息还包括当前余额、待转出、待转入。

对于所有的事务来说,在你部署时需要设置一个合适的写模式。(译者注:我觉得涉及事务的地方最好还是使用安全写比较靠谱)

时间: 2024-10-16 03:41:12

MongoDB官方文档翻译系列之 -- 执行二阶段提交的相关文章

分布式基础之二阶段提交

分布式基础之二阶段提交 二阶段提交(Two Phase Commit)在分布式事务处理中非常常见.它主要用来保证分布式事务处理的一致性,决定事务的提交或回滚.目前二阶段提交广泛应用于关系型数据库的分布式事务处理中,它是分布式系统中的一个常见协议. 需求 为什么要二阶段提交?因为在分布式系统中,每个节点只知道自己的事务是否执行成功了,而分布式系统要求一致性,也就是所有的节点的状态都应该一致.如果某一个事务只在部分节点执行成功,那么势必会导致各分布式节点不一致.二阶段提交就是用来保证要么所有的节点都

分布式入门之4:二阶段提交

1. 背景: 初时提出,是为解决分布式数据库的事务问题.单机数据库事务可靠日志技术,MVCC技术实现.分布式情况下,就需要额外的手段来保证,这才出现了二阶段提交. 2. 流程: 从角色上,二阶段提交分为两种角色:协调者(coordinate),参与者(participant).流程思路上很简单: 1. 协调者询问询问所有参与者,能否提交:参与者返回是否能提交的结果: 2. 协调者根据参与者的返回结果决定是否提交事务,并通知参与者执行. 但实际上,二阶段提交需要考虑不少异常场景: 对照上图: 1

Mysql事物与二阶段提交

 1.事务的四种特性(ACID) 事务可以是一个非常简单的SQL构成,也可以是一组复杂的SQL语句构成.事务是访问并且更新数据库中数据的一个单元,在事务中的操作,要么都修改,要么都不做修改,这就是事务的目的,也是事务模型区别于其他模型的重要特征之一. 事务的原子性:原子是不可分割的,事务不可分割(没有commit数据不能被读到). 事务的持久性:在commit之后,不能丢数据.(就是在提交后,数据必须落盘redo落盘). 事务的隔离性:在数据库里面,各个事务之间不能互相影响. 事务的一致性:事务

二阶段提交和三阶段提交

一.2PC 2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase).提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段 整个事务过程由事务管理器和参与者组成,事务管理器负责 决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚 在计算机中部分关系数据库如Oracle.MySQL支持两阶段提交协议,如下图: 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者

二阶段提交应用项目(Two-phase commit protocol )2PC 高并发

整个系统的需求文档为英文描述. A Simple 2-Phase Commit System The company ABC provides its customers wire transfer service. For example, it can withdraw $1000 from John's account in Bank of China and deposit the money to John's another account in China Construction

【mongoDB查询进阶】聚合管道(二) -- 阶段操作符

https://segmentfault.com/a/1190000010826809 什么是管道操作符(Aggregation Pipeline Operators) mongoDB有4类操作符用于文档的操作,例如find查询里面会用到的$gte,$in等.操作符以$开头,分为查询操作符,更新操作符,管道操作符,查询修饰符4大类.其中管道操作符是用于聚合管道中的操作符. 管道操作符的分类 管道操作符可以分为三类: 阶段操作符(Stage Operators) 表达式操作符(Expression

对分布式事务及两阶段提交、三阶段提交的理解

转载至:http://www.cnblogs.com/binyue/p/3678390.html,最近学习需要,先转载方便用用来强化加深印象 一.分布式数据一致性 在分布式系统中,为了保证数据的高可用,通常会将数据保留多个副本(replica),这些副本会放置在不同的物理的机器上. (1)什么是数据一致性 在数据有多份副本的情况下,如果网络.服务器或者软件出现故障,会导致部分副本写入成功,部分副本写入失败.这就造成各个副本之间的数据不一致,数据内容冲突. 造成事实上的数据不一致. (2)CAP定

分布式事务之两阶段提交

一.二阶段提交协议 一般分为协调器C和若干事务执行者Si两种角色:    当执行某一事务T的所有站点Si都通知C事务执行完成,C即启动二阶段提交协议.    (1) 首先C向所有Si发<prepare>消息(C先将<prepare>消息写到本机日志) ,Si收到<prepare>消息后,根据本机T的执行情况,如果成功返回<ready T>,不成功返回<abort T>.(返回前都应把要返回的消息写到日志里)     (2) C收集完所有Si的返回

并发复制系列 一:binlog组提交

http://blog.itpub.net/28218939/viewspace-1975809/ 作者:沃趣科技MySQL数据库工程师  麻鹏飞 MySQL  Binary log在MySQL 5.1版本后推出主要用于主备复制的搭建,我们回顾下MySQL 在开启/关闭 Binary Log功能时是如何工作的 . MySQL没有开启Binary log的情况下: InnoDB存储引擎通过redo和undo日志可以safe crash recovery数据库,当数据crash recovery时,