Mongodb事务

在前一篇《理解数据库的事务,ACID,CAP和一致性》我已经将数据库的一些基本概念包括事务,ACID,CAP,一致性,隔离性等都深入的介绍了一遍,而此篇主要是针对MongoDB数据库系统做一下深入的了解,主要希望弄清楚如下几个问题:

MongoDB是如何实现事务的?有哪些局限?
MongoDB的一致性是如何保证的?
MongoDB的事务
首先我们需要知道MongoDB是有多种存储引擎的,不同的存储引擎在实现ACID的时候,使用不同的机制。而Mongodb从3.0开始默认使用的是WiredTiger引擎,本文后续所有文字均是针对WiredTiger引擎。
WiredTiger引擎可以针对单个文档来保证ACID特性,但是当需要操作多个文档的时候无法保证ACID,也即无法提供事务支持。但是,我们是否就无法实现事务呢?实际上,MongoDB本身虽然不支持跨文档的事务,但是我们依然可以可以在应用层来获取类似事务的支持。这其中有很多方式,MongoDB公司的Antoine Girbal曾经撰写过文章详细阐释了五种方式来支持事务,可以参考Reference中的链接。不过在此之前,让我们先了解下MongoDB在单文档上是如何实现ACID特性的。

单文档的ACID是如何实现的?
MongoDB在更新单个文档时,会对该文档加锁,而要理解MongoDB的锁机制,需要先了解以下几个概念:

Intent Lock(我把它翻译为意图锁): 意图锁表明读写方(reader-writer)意图针对更细粒度的资源进行读取或写入操作。比如:如果当某个Collection被加了intent lock,那么说明读写方意图针对该Collection中的某个文档进行读或写的操作。如下图所示:
Mongodb事务
上图展示了当reader or writer需要操作文档时,相对更高的层级都需要加intent lock.
Multiple granularity locking (我把它翻译为多粒度锁机制): MongoDB采用的是所谓的MGL多粒度锁机制,具体可以参考文末的wiki链接。简单来说就是结合了多种不同粒度的锁,包括S锁(Shared lock),X锁(Exclusive lock), IS锁(Intent Share lock), IX(Intent Exclusive lock),这几种锁的互斥关系如下表所示:

Mongodb事务
下面,我用一个例子来简单说明下。假设我要更改name为Jim的document
db.user_collection.update({‘name’: ‘Jim’}, {$set: {‘age’: 26, ‘score’: 50}})
如果当age修改成功,而score没有修改成功时,MongoDB会自动回滚,因此我们可以说针对单个文档,MongoDB是支持事务,保证ACID的(严格来说,要想保证Durability,需要在写操作时使用特殊的write concern,这个后边再谈)
所有的锁都是平等的,它们是排在一个队列里,符合FIFO原则。但是,MongoDB做了优化,即当一个锁被采用时,所有与它兼容的锁(即上表为yes的锁)都会被采纳,从而可以并发操作。举个例子,当你针对Collection A中的Document a使用S锁时,其它reader可以同时使用S锁来读取该Document a,也可以同时读取同一个Collection的Document b.因为所有的S锁都是兼容的。那么,如果此时针对Collection A中的Document c进行写操作是否可以呢?显然需要为Document c赋予x锁,此时Collection A就需要IX锁,而由于IX和IS是兼容的,所以没有问题。简单来说,只要不是同一个Document,读写操作是可以并发的;如果是同一个Document,读可以并发,但写不可以。
WiredTiger针对global, db, collection level只能使用intent lock。另外,针对冲突的情况,WiredTiger会自动重试。
跨文档的事务支持
前面已经说过,针对多文档,MongoDB是不支持事务的,但是我们的应用却可以自己去实现类事务的功能,这里只针对其中最常用的两步提交方式来做详细阐释。
假设我们有两个账户A和B,现在我们要让账户A转账100元给账户B,我们需要将整个过程放在一个事务当中,来保证数据的一致性。在这个应用模拟的事务当中,需要涉及两个Collection,一个是accounts collection,另一个是transaction collection(用于存储交易的信息和状态)。
先来看下transaction最终成功的大体流程:如图2所示
Mongodb事务
伪代码如下:

initial accounts
bulk_result = db.accounts.insert(
[
{ _id: “A”, balance: 1000, pendingTransactions: [] },
{ _id: “B”, balance: 1000, pendingTransactions: [] }
]
)
if bulk_result.nInserted != 2:
print “insert account failed.”
return False
add a transaction
write_result = db.transactions.insert(
{ _id: 1, source: “A”, destination: “B”, value: 100, state: “initial”, lastModified: new Date() }
)
if write_result.nInserted != 1:
print “transaction failed”
return False
update transaction to pending
t = db.transactions.findOne( { state: “initial” } )
result = db.transactions.update(
{ _id: t._id, state: “initial” },
{
$set: { state: “pending” },
$currentDate: { lastModified: true }
}
)
if result.nModified != 1:
print “transaction failed”
return False
update accounts & push transaction id
result_source = db.accounts.update(
{ _id: t.source, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)
result_destination = db.accounts.update(
{ _id: t.destination, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)
if result_source.nModified != 1 or result_destination.nModified != 1:

进入回滚的流程


return False
update transaction to applied
result = db.transactions.update(
{ _id: t._id, state: “pending” },
{
$set: { state: “applied” },
$currentDate: { lastModified: true }
}
)
if result.nModified != 1:

重新update accounts & push transaction id

注意:如果上一步是成功的,pendingTransactions列表中会有相应的Transaction,那么就不会重复更新账户


pull transaction id
result_source = db.accounts.update(
{ _id: t.source, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)
result_destination = db.accounts.update(
{ _id: t.destination, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)
if result_source.nModified != 1 or result_destination.nModified != 1:

重新执行pull transaction id


update transaction to done
result = db.transactions.update(
{ _id: t._id, state: “applied” },
{
$set: { state: “done” },
$currentDate: { lastModified: true }
}
)
if result.nModified != 1:

重新从pull transaction id执行

Mongodb事务

上一篇:SSH整合SpringMvc报错FlushMode.MANUAL( 只读模式)


下一篇:MySQL的SQL语句 -事务性语句和锁定语句(1)