标签:注释 comm ble block 并行执行 数组 数据持久化 访问 int
【原文地址】https://docs.mongodb.com/manual/
CRUD操作(三)
主要内容:
原子性和事务(Atomicity and Transactions),读隔离、一致性和新近性,分布式查询(Distributed Queries),分布式写操作,模拟两阶段任务提交,在副本集中执行配额读取
在MongoDB中,写操作在单文档级别具有原子性,即使修改一个文档中的多个嵌入式文档也是如此。
当一个写操作修改多个文档时,对每一个文档的修改都是原子的,但是整体操作却不是原子的,并且其他的操作可能是交替进行的。然而,使用
$isolated 操作符可以隔离影响多个文档的写操作。
$isolated操作符
使用$isolated操作符,一旦一个操作多个文档的写操作修改了第一个文档,交替地修改多个文档的行为将被阻止。这确保直到写操作完成或者有错误抛出时,客户端才会看到变化。
$isolated不能用于分片集群。
一个隔离写操作不能提供“要么全有要么全无的”的原子性。这是因为,写操作执行过程中产生错误时不能回滚到错误之前的状态。
注:
$isolated 操作符使写操作获得一个集合的排他锁,即使对于文档级锁存储引擎WiredTiger也是如此。因为,$isolated 操作符会使WiredTiger 在执行操作期间以单线程的方式运行。
$isolated不能用于分片集群。
例如更新操作,删除操作都可使用$isolated操作符。
同事务语义
因为一个文档可以包含多个嵌入式文档,单文档原子性可满足许多实际用例。对于那些执行一系列操作的情况,可在你的应用程序中实施两阶段任务提交策略(two-phase commit)。
然而,两阶段任务提交策略(two-phase commit)仅是对事务的模拟。使用两阶段任务提交策略(two-phase commit)可以确保数据一致性,但对于应用程序来说,可能返回两阶段事务提交或回滚执行过程中的数据。
并发控制
并发控制机制可保证多个应用程序并行执行时不会引起数据不一致或存在冲突。
一种方法是在具有唯一值的字段上创建唯一索引。这样可防止插入操作或更新操作产生重复数据。在多个字段上创建唯一索引时,强制要求多个字段值的组合具有唯一性。
另一种方法是,对于写操作来说,在查询谓词中指定一个字段期望的当前值。两阶段任务提交模式提供一个变异的版本:在写操作中,查询谓词包含应用标识以及数据的期望状态。
2.1隔离保障
未提交读
在MongoDB中,客户端可以看到数据持久化之前的写入结果。
未提交读是默认的隔离级别并被应用于独立的mongod 实例以及副本集和分片集群。
未提交读和单文档原子性
写操作具有单文档级别原子性;例如,一个写操作更新一个文档中的多个字段,不会发生只更新了其中某些字段的情况。
对于一个独立的mongod 实例,对一个文档的一系列读写操作是串行化的。对于一个副本集,仅在数据没有回滚的情况下对一个文档的一系列读写操作是串行化的。
然而,尽管读者不会看到部分更新的文档,未确认读意味着在变化持续阶段执行并发访问的读者可能会看到已更新的那部分文档。
未确认读和多文档写操作
当一个写操作修改多个文档时,对每个文档的修改都是原子的,但整个操作不是原子的并且对每个文档的写操作可能交替执行。但是可以使用
$isolated操作符隔离影响多个文档的一个写操作。
如果没有隔离多文档写操作,MongoDB 会表现出如下行为:
1.非时间点读操作。假设读操作起始于时刻t1 并且开始读取多个文档。然后在随后的t2时刻写操作更新了其中一个文档。读者可能会看到那个文档的新版本,并且因此不会看到某一时间点的数据快照。
2.非串行化操作。假设在t1 时刻读操作读取文档d1 并且在随后的t3时刻写操作更新了d1。这产生了读写依赖:如果操作不是串行化的,读操作必须先于写操作被执行。但同样假设写操作在 t2 时刻更新文档d2并且读操作在随后的t4时刻读取d2 ,这引入了写读依赖,写读依赖要求写操作先于读操作以串行化的方式执行。存在循环依赖关系使得串行化不可得。
3.读操作匹配到某一文档,读取的同时此文档被更新,这时读操作可能会漏掉此文档。
使用$isolated操作符,一旦写操作修改了第一个文档,影响多个文档的写操作能够阻止操作交替进行。这能够保证没有客户端会看到变化直到写操作完成或者有错误抛出。
$isolated操作符不能用于分片集群。
一个隔离的写操作不能提供“要么全有要么全无的”的原子性。这是因为,写操作执行过程中产生错误时不能回滚到错误之前的状态。
$isolated操作符使得写操作获得集合上的排他锁,即使对于文档级锁存储引擎WiredTiger也是如此。因为,$isolated 操作符会使WiredTiger 在执行操作期间以单线程的方式运行。
游标快照
某些情况下,MongoDB 游标不止一次地返回同一文档。当游标返回一些文档时,伴随着查询操作的其他操作可能交替进行。如果上述操作中的某些操作是使文档移动的更新操作(例如使用MMAPv1存储引擎,文档增大时)或者改变了所查询字段的索引,游标会返回相同文档不止一次。
在非常特殊的情况下,你可以使用cursor.snapshot() 方法阻止游标多次返回同一文档。snapshot()确保查询返回每个文档最多一次。
警告:
一个替代的解决方案是,如果你的集合中有一个或多个字段从不被修改,你可以在这个字段或这些字段上创建唯一索引,达到和snapshot()同样的效果。查询操作使用hint() 以明确强制查询使用哪些索引。
2.2 一致性保障
单调读
MongoDB 提供了从独立的mongod 实例单调读取的功能。假设一个应用执行一系列的操作,这些操作中包含了读操作R1 ,紧跟 R1 后面的操作是读操作 R2。如果应用在独立的mongod 实例上执行这一系列操作,那么 R2的返回结果所反应的状态不会比R1 早。例如R2返回的数据多于R1 所返回的数据。
3.2版本中的变化:对于副本集和分片集群,如果读操作指定 Read Concern为"majority" 并且优先读主成员,那么MongoDB 提供单调读操作。
在以前的版本中MongoDB 不能保证单调读副本集和分片集群。
单调写
对于mongod 实例,副本集和分片集群,MongoDB 提供单调写。
假设一个应用执行一系列的操作,这些操作中包含了写操作W1 ,紧跟 W1 后面的操作是写操作 W2。MongoDB 保证W1 在W2之前执行。
2.3 新近性
在MongoDB中,一个副本集有一个主成员[1]。
注释[1]:
在某些情况下,副本集中的两个成员可能被持续片刻地认为都是主成员。但至多他们中的一个会执行{ w: "majority" } 的写操作。可以完成
{ w: "majority" }写操作的成员是当前的主成员,另一个成员是之前的主成员,只不过它还没有意识到它已被降级,例如由于网络引起的。这种情况发生时,尽管已经请求优先读取主成员数据,但连接之前主成员的客户端可能看到的是旧的数据,并且对于之前的主成员的新的写操作最终会回滚。
分片集群的读操作
分片集群允许以一种对应用几乎透明的方式将数据分布到mongod 实例集群中。
对于分片集群,应用发出对集群中的一个mongod 实例的操作。
当定位到分片集群中一个指定的分片时,读操作是最高效。查询分片集合应该包含集合的片键。当查询包含片键时,mongos 能够使用
config database中的集群元数据路由到片键。
如果一个查询不包含片键,mongos 必须查询所有的分片。这种分散聚集查询是低效的。在一个巨大的集群上,对于常规操作来讲,分散聚集查询是不可行的。
对于副本集分片,查询副本集的第二分片可能不会反映主成员的当前状态。读优先设置指定读不同服务器,这可能会导致非单调的读。
副本集的读操作
默认地,客户端读副本集的主成员。客户端能够设置读优先配置(read preference)来指定读其他的副本集成员。例如,客户端能够配置读优先
(read preference)以读取第二成员或者最近的成员:
查询副本集的第二分片可能不会反映主成员的当前状态。读优先设置指定读不同服务器,这可能会导致非单调的读。
4 分布式写操作
分片集群上的写操作
对于分片集群上的分片集合,mongos 指定来自应用的写操作给分片,这些分片上存储了数据集的指定部分。mongos 使用来自
config database 的集群元数据将写操作路由到适当的分片上。
一个分片集合上的分区数据分布范围取决于分片键值。MongoDB 将这些块分布到片上。片键决定了块的分布。这会影响集群写操作的性能。
重点:
作用于一个文档的更新操作必须包含片键或_id字段。如果使用片键,作用于多个文档的更新操作在某些情况下更高效,但这种操作会广播到所有分片。
如果每次执行插入操作片键的值会增加或者减小,那么所有的插入操作都是针对同一个分片。结果,一个分片的容量限制就成了整个分片集群的容量限制。
副本集的写操作
在副本集中,所有的写操作都是针对主成员的。主成员用于写操作并且在主成员操作日志或oplog中记录操作行为。oplog是一系列可重用数据集操作。第二成员不断地复制oplog 并以异步的方式执行那些操作。
5 模拟两阶段任务提交
5.1简介
本文提供了一种使用两阶段任务提交方法完成多文档更新或执行多文档事务的方式。另外你可以扩展这个处理过程来模拟回滚功能。
5.2背景
对于MongoDB来说,单文档操作总是具有原子性的。对多文档操作不具有原子性,这种操作常常涉及到多文档事务。因为文档结构可以比较复杂并且可以包含嵌套的文档,所以对许多实际的用例来讲,单文档原子性提供了足够的支持。
尽管单文档原子性足够有力,还是有一些用例需要多文档事务。当执行有多个操作构成的事务时,问题便显现出来:
对于需要多文档事务的情形,可以在你的应用中实现两阶段任务提交以支持这种需要多文档更新的情形。使用两阶段任务提交确保数据一致性,并且一旦发生错误,会回滚到之前的状态。然而,在处理的过程中,文档能够表示待定的数据和状态。
注:
MongoDB中仅对单文档操作具有原子性,两阶段任务提交仅模拟了事务。在两阶段任务提交或回滚的过程中,应用能够返回中间事务。
5.3模式
概述
假设你要将A账户中的资金转入B账户。在关系数据库系统中,你可以使用多语句事务减去A账户的资金加到B账户上。在MongoDB中,你可以模仿两阶段任务提交模式来达到相当的效果。
本例中使用下面两个集合:
accounts 集合存储账户信息。
transactions 集合存储资金转移信息。
初始化数据源和目标账户
向accounts 集合中插入一个文档表示账户A和另一个文档表示账户B。
db.accounts.insert(
[
{ _id: "A", balance: 1000, pendingTransactions: [] },
{ _id: "B", balance: 1000, pendingTransactions: [] }
]
)
操作返回含有操作执行状态的BulkWriteResult() 对象。当成功插入时,BulkWriteResult()中的nInserted 被设置为2。
初始化转移记录
对于每次资金转移,将含有转移信息的文档插入transactions 集合中。文档包含下面的字段:
初始化transactions集合,将账户A转移到B的金额设置为100,state 设置为“initial”,lastModified 字段值设置为当前日期,向集合中插入文档:
db.transactions.insert(
{ _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() })
返回结果为WriteResult()对象,nInserted 值为1。
使用两阶段任务提交模式执行转账
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") }
2 )更新事务状态为pending
将事务状态由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的话,返回第一步,找到一个不同的事务文档并且从新开始此过程。
3)将事务用于两个账户
如果事务还没有用于两个账户,那么使用update() 方法将事务t应用于两个账户。在更新条件中,为了避免此步骤执行多次而引起重复应用事务,更新条件包括pendingTransactions: { $ne: t._id }。
为了将事务用于两个账户,同时更新字段balance 和pendingTransactions 。
更新源账户,从账户中减去事务文档中value字段值,并将事务文档的_id插入自身数组pendingTransactions 中。
db.accounts.update(
{ _id: t.source, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } })
更新成功后返回WriteResult() 对象,其中nMatched 和nModified 的值为1。
更新目标账户,将事务文档value字段值加到账户中并且将事务文档的_id插入自身数组pendingTransactions 中。
db.accounts.update(
{ _id: t.destination, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: t.value }, $push: { pendingTransactions: t._id } })
更新成功后返回WriteResult() 对象,其中nMatched 和nModified 的值为1。
4 )更新事务文档state字段值为applied
使用update()方法将事务文档state字段值由pending更新为applied并将lastModified 字段值设置为当前时间。
db.transactions.update(
{ _id: t._id, state: "pending" },
{
$set: { state: "applied" },
$currentDate: { lastModified: true }
}
)
更新成功后返回WriteResult() 对象,其中nMatched 和nModified 的值为1。
5 )更新两个账户的pendingTransactions数组
将两个账户pendingTransactions 数组中的已应用的事务文档_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。
6 )更新事务文档state字段值为done
通过设置事务文档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 状态
错误发生在将事务状态更新为pending之后与将事务状态更新为applied之前时,为了从错误中恢复,在transactions 集合中检索状态为
pending 的事务文档并将其恢复:
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
var t = db.transactions.findOne( { state: "pending", lastModified: { $lt: dateThreshold } } );
并从步骤“将事务应用到两个账户”重新开始。
事务处于Applied 状态
错误发生在将事务状态更新为applied之后与将事务状态更新为done之前时,为了从错误中恢复,在transactions 集合中检索状态为applied 的事务文档并将其恢复:
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
var t = db.transactions.findOne( { state: "applied", lastModified: { $lt: dateThreshold } } );
并从步骤“更新两个账户的pendingTransactions数组”重新开始
回滚操作
在某些情况下,你可能需要回滚或者撤销事务。例如,如果应用需要撤销事务或一个账户不存在或账户已停用。
事务处于Applied 状态
执行完步骤“更新事务状态为Applied ”后,不应该回滚。而要通过改变源账户和目的账户的value字段值的方式来完成事务并创建一个新的事务文档来换掉已有的事务文档。
事务处于Pending 状态
更新事务文档为“pending”状态完成之后,但在更新事务文档为“applied”状态之前,可依照下面的步骤执行回滚:
1 )更新事务状态为“canceling”
db.transactions.update(
{ _id: t._id, state: "pending" },
{
$set: { state: "canceling" },
$currentDate: { lastModified: true }
}
)
更新成功后返回WriteResult() 对象,其中nMatched 和nModified 的值为1。
2 )取消两个账户的事务
为了取消两个账户的事务,查询事务t是否已被使用。在更新条件中包含pendingTransactions: t._id 来更新文档,仅当pending 事务已被使用时。
更新目标账户,从账户中减去事务文档balance 字段值并将事务文档_id值从源账户数组pendingTransactions 中移除。
db.accounts.update(
{ _id: t.destination, pendingTransactions: t._id },
{
$inc: { balance: -t.value },
$pull: { pendingTransactions: t._id }
})
更新成功后返回WriteResult() 对象,其中nMatched 和nModified 的值为1。
如果pending 状态的事务没有用于两个账户,那么匹配不到任何文档并且nMatched 和nModified 的值为0。
更新源账户,将事务文档中balance 字段值加到源账户上将事务文档_id值从源账户数组pendingTransactions 中移除。
db.accounts.update(
{ _id: t.source, pendingTransactions: t._id },
{
$inc: { balance: t.value},
$pull: { pendingTransactions: t._id }
})
更新成功后返回WriteResult() 对象,其中nMatched 和nModified 的值为1。
如果pending 状态的事务没有用于两个账户,那么匹配不到任何文档并且nMatched 和nModified 的值为0。
3 )更新事务状态为canceled
db.transactions.update(
{ _id: t._id, state: "canceling" },
{
$set: { state: "cancelled" },
$currentDate: { lastModified: true }
})
更新成功后返回WriteResult() 对象,其中nMatched 和nModified 的值为1。
多个应用
某种程度上,有了事务,多个应用才能连续不断地创建并执行操作而不会引起数据不一致或相互冲突。在我们的处理过程中为了更新或查询事务文档,更新条件中包含state 字段来阻止多个应用程序重复地应用事务。
例如,应用App1和App2获取了相同的事务,此时事务的状态为initial。App1 在App2启动之前使用了整个事务。当App2 试着执行“更新应用状态为pending”这步时,使用的更新条件包括state: "initial",此时不会匹配到任何文档且返回对象WriteResult()中的nMatched 和nModified 的值为0。这指示App2 应该退回到第一步,使用不同的事务文档重新开始。
当多个应用程序运行时,在任意一个时间点上,只有一个应用程序能够控制指定的事务是关键。像这样,除了在更新条件中包含预期的事务状态,你也可以在事务文档中创建一个标志来指明那个应用程序在使用这个事务文档。
使用findAndModify() 方法修改事务文档并且一步完成。
t = db.transactions.findAndModify(
{
query: { state: "initial", application: { $exists: false } },
update:
{
$set: { state: "pending", application: "App1" },
$currentDate: { lastModified: true }
},
new: true
}
)
修改事务操作能够确保只有匹配了application 字段值的应用使用这个事务文档。
如果在事务执行过程中App1 失败了,你可以使用恢复步骤,但是应用要确保在使用事务文档之前已经拥有事务文档。例如找到并重新开始待定的工作:
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
db.transactions.find(
{
application: "App1",
state: "pending",
lastModified: { $lt: dateThreshold }
})
在生产应用中使用两阶段任务提交模式
上面的例子被刻意简化了。例如,当一个账户的金额是负值时可能进行回滚操作。
产品的实现可能更复杂,通常账户需要关于账户余额,待处理存款,待处理借款的信息。
对于所有的事务文档使用适当的write concern 级别。
6 在副本集中执行配额读取
简介
当从副本集主成员中读取数据时,读取的数据可能不是最新的或者不是持久化的数据,这取决于所使用的read concern。read concern级别为
“local”时,客户端读取的数据是持久化之前的数据;因此,在他们被传送到足够的副本集成员之前避免回滚。read concern级别为“majority”时,能够保证读取的数据为持久化的数据,但是读取的数据可能不是最新的,数据已经被其他的写操作重写了。
本文介绍了使用db.collection.findAndModify() 读取的数据可能不是最新的并且不能回滚。为了能够这样做,使用findAndModify() 和
write concern来修改文档中的哑变量。特别地,这个过程需要:
重要的:
使用 read concern of "majority" 执行“配额读”的开销很大,因为这会引起写延迟而不是读延迟。这项技术仅应用在数据过期是无法容忍的情形下。
先决条件
本文实例读取集合products。使用下面的操作初始化集合:
db.products.insert( [
{
_id: 1,
sku: "xyz123",
description: "hats",
available: [ { quantity: 25, size: "S" }, { quantity: 50, size: "M" } ],
_dummy_field: 0
},
{
_id: 2,
sku: "abc123",
description: "socks",
available: [ { quantity: 10, size: "L" } ],
_dummy_field: 0
},
{
_id: 3,
sku: "ijk123",
description: "t-shirts",
available: [ { quantity: 30, size: "M" }, { quantity: 5, size: "L" } ],
_dummy_field: 0
}] )
在这个集合的文档中包含了名为_dummy_field的哑变量,使用db.collection.findAndModify()方法使哑变量递增。如果这个字段不存在,那么db.collection.findAndModify()方法会添加这个字段。添加这个字段的目的是确保db.collection.findAndModify()修改了文档。
过程
1)创建唯一索引
创建唯一索引在一些字段上,这些字段将被用于为db.collection.findAndModify()方法指定精确测查询条件。
本教程使用sku 字段来指定精确的匹配条件。创建唯一索引在sku 字段上。
db.products.createIndex( { sku: 1 }, { unique: true } )
2)使用findAndModify 读取提交的数据
使用db.collection.findAndModify()方法更新你要读的文档并返回已修改的文档。{ w: "majority" }的write concern 是必须的。为了指定要读取的文档,你必须使用被索引支持的精确查询条件。
下面使用findAndModify() 方法,指定关于具有唯一索引的字段sku 的精确查询条件并使匹配文档中_dummy_field字段的值加1。对于这个命令来说,write concern中包含值为5000 毫秒的wtimeout 字段,如果写操作没有传播到被选中成员的多数成员,那么这样设置将会防止写操作永远阻塞应用,而这样设置不是必须的。
var updatedDocument = db.products.findAndModify(
{
query: { sku: "abc123" },
update: { $inc: { _dummy_field: 1 } },
new: true,
writeConcern: { w: "majority", wtimeout: 5000 }
});
即使在副本集中有两个成员被认为是主成员的情况下,也仅会有一个成员完成w: "majority"的写操作。这样使用了 write concern 为"majority"的findAndModify() 方法仅当客户端连接到真正的主成员时执行才会成功。
因为配额读的过程仅是在文档中增加了dummy 字段而已,因此可以安全地反复调用findAndModify()方法,必要时调整wtimeout 的值。
-----------------------------------------------------------------------------------------
转载与引用请注明出处。
时间仓促,水平有限,如有不当之处,欢迎指正。
标签:注释 comm ble block 并行执行 数组 数据持久化 访问 int
原文地址:http://www.cnblogs.com/hdwgxz/p/6028480.html