如何解决MongoDB中缺少事务的问题?


139

我知道这里也有类似的问题,但是如果我需要事务或使用原子操作两阶段提交,它们要么告诉我切换回常规RDBMS系统。第二种解决方案似乎是最佳选择。第三,我不想跟进,因为看来很多事情可能出错,而且我无法在各个方面进行测试。我很难重构项目以执行原子操作。我不知道这是否来自我有限的观点(到目前为止,我仅使用SQL数据库),还是实际上无法完成。

我们想在我们公司试行MongoDB。我们选择了一个相对简单的项目-SMS网关。它使我们的软件可以将SMS消息发送到蜂窝网络,而网关则可以完成肮脏的工作:实际上是通过不同的通信协议与提供商进行通信。网关还管理消息的计费。每个申请该服务的客户都必须购买一些积分。发送消息时,系统自动减少用户的余额,如果余额不足,则拒绝访问。另外,由于我们是第三方SMS提供商的客户,因此我们也可能拥有自己的余额。我们也必须跟踪这些。

我开始考虑如果我降低了一些复杂性(外部账单,排队的SMS发送),如何使用MongoDB存储所需的数据。来自SQL世界,我将为用户创建一个单独的表,为用户创建另一个表,用于存储SMS消息,以及一个用于存储有关用户余额的事务的表。假设我为MongoDB中的所有数据库创建了单独的集合。

设想一下此简化系统中的SMS发送任务,其步骤如下:

  1. 检查用户是否有足够的余额;如果信用额不足,请拒绝访问

  2. 将消息以及详细信息和成本发送并存储在SMS集合中(在实时系统中,消息将具有status属性,任务将提取该消息进行传递,并根据其当前状态设置SMS的价格)

  3. 通过发送邮件的费用减少用户的余额

  4. 将交易记录在交易集合中

现在有什么问题吗?MongoDB只能对一个文档进行原子更新。在上一个流程中,可能会发生某种错误,消息已存储在数据库中,但用户的余额未更新和/或未记录交易。

我提出了两个想法:

  • 为用户创建一个集合,并将余额存储为字段,将与用户相关的交易和消息存储为用户文档中的子文档。因为我们可以原子地更新文档,所以实际上解决了交易问题。缺点:如果用户发送许多SMS消息,则文档的大小可能会变大,并且可能会达到4MB的文档限制。也许我可以在这种情况下创建历史记录文档,但是我认为这不是一个好主意。另外,我不知道如果将越来越多的数据推送到同一个大文档中,系统将有多快。

  • 为用户创建一个集合,为事务创建一个。可以有两种交易:余额更改为正的信用购买和余额更改为负的发送的消息。交易中可能有一个子文档;例如,在发送消息中,可以将SMS的详细信息嵌入交易中。缺点:我不存储当前用户余额,因此每次用户尝试发送消息时都必须进行计算,以判断消息是否可以通过。恐怕随着存储的事务数量的增加,此计算可能会变慢。

我对选择哪种方法有些困惑。还有其他解决方案吗?在网上找不到关于如何解决此类问题的最佳实践。我猜想许多试图熟悉NoSQL世界的程序员在一开始就面临类似的问题。


61
如果我错了,请原谅我,但看起来该项目将使用NoSQL数据存储,无论它是否会从中受益。NoSQL不能替代SQL作为“时尚”选择,而是用于关系RDBMS的技术不适合问题空间而非关系数据存储适合的情况。您的很多问题都带有“如果是SQL,那么...”,这对我敲响了警钟。所有NoSQL都来自需要解决SQL无法解决的问题,然后对其进行了某种泛化以使其易于使用,并且当然流行。
PurplePilot 2011年

4
我知道这个项目并不是尝试NoSQL的最佳选择。但是,如果我们开始将其与其他项目一起使用(我说是一个图书馆馆藏管理软件,因为我们正从事馆藏管理),而突然出现某种需要交易的请求(实际上在那儿,想象一本书),我会感到很惊讶。从一个集合转移到另一个集合),我们需要知道如何克服这个问题。也许只有我一个人思维狭窄,并且认为总是需要进行交易。但这可能是一种以某种方式克服这些困难的方法。
NagyI 2011年

3
我同意PurplePilot的观点,您应该选择适合解决方案的技术,而不是尝试嫁接不适合解决问题的解决方案。图数据库的数据建模与RDBMS设计是完全不同的范例,您必须忘记所有已知知识并重新学习新的思维方式。

9
我知道我应该为任务使用适当的工具。但是对我来说-当我读到这样的答案时-似乎NoSQL对于数据至关重要的任何事物都不适合。对于Facebook或Twitter来说,这是一件好事,如果世界上的某些评论迷失了方向,但以上所有内容都将失效。如果那是真的,我不明白为什么其他人会关心建筑。拥有MongoDB的网络商店:kylebanker.com/blog/2010/04/30/mongodb-and-ecommerce甚至提到大多数交易都可以通过原子操作来克服。我在寻找的是方法。
NagyI 2011年

2
您说:“似乎NoSQL不适用于对数据至关重要的任何事物”,对于不好的(可能是)事务ACID类型的事务处理,则不正确。同样,NoSQL是为分布式数据存储而设计的,当您进入主从复制场景时,SQL类型存储可能很难实现。NoSQL具有最终一致性的策略,并确保仅使用最新的数据集而不使用ACID。
PurplePilot 2011年

Answers:


23

从4.0开始,MongoDB将具有多文档ACID事务。该计划是首先启用副本集部署中的部署,然后再启用分片群集。MongoDB中的事务就像使用关系数据库的开发人员所熟悉的一样-它们将是多语句的,具有相似的语义和语法(如start_transactioncommit_transaction)。重要的是,启用事务的MongoDB更改不会影响不需要事务的工作负载的性能。

有关更多详细信息,请参见此处

拥有分布式事务,并不意味着您应该像在表格关系数据库中那样对数据建模。拥抱文档模型的强大功能,并遵循数据建模的良好和推荐做法


1
交易已到!4.0 GA版。mongodb.com/blog/post/...
格里戈里·梅尔尼克

MongoDB事务仍然对事务大小有限制,为16 MB,最近我有一个用例,我需要将一个文件中的50k条记录放入mongoDB中,因此为了维护原子属性,我想到了使用事务,但由于有50k json记录超过此限制,将引发错误“所有事务操作的总大小必须小于16793600。实际大小为16793817”。有关更多详细信息,请查看mongoDB上
Gautam Malik,

MongoDB 4.2(当前处于beta版,RC4)支持大型事务。通过跨多个oplog条目表示事务,您将能够在单个ACID事务中写入超过16MB的数据(取决于现有的60秒默认最大执行时间)。您现在可以尝试使用它们-mongodb.com/download-center/community
Grigori Melnik,

MongoDB 4.2现在是GA,全面支持分布式事务。mongodb.com/blog/post/...
格里戈里·梅尔尼克

83

没有交易生活

事务支持ACID属性,但是尽管中没有事务MongoDB,但我们确实有原子操作。好吧,原子操作意味着当您处理单个文档时,该工作将在其他人看到该文档之前完成。他们将看到我们所做的所有更改,或者全部都看不到。使用原子操作,您通常可以完成与使用关系数据库中的事务可以完成的相同的工作。原因是,在关系数据库中,我们需要跨多个表进行更改。通常,表需要连接,因此我们希望一次完成所有连接。要做到这一点,由于有多个表,我们必须开始一个事务并进行所有这些更新,然后结束该事务。但是随着MongoDB,我们将嵌入数据,因为我们将其预先加入文档中,而这些正是具有层次结构的丰富文档。我们通常可以完成同一件事。例如,在博客示例中,如果我们要确保原子更新博客文章,则可以这样做,因为我们可以立即更新整个博客文章。好像是一堆关系表一样,我们可能必须打开一个事务,以便我们可以更新post集合和comment集合。

那么,我们可以采取哪些方法MongoDB来克服交易不足?

  • 重组 -重组代码,以便我们在单个文档中进行工作并利用我们在该文档中提供的原子操作。如果我们这样做,通常情况下我们都准备就绪。
  • 在软件中实现 -通过创建关键部分,我们可以在软件中实现锁定。我们可以使用查找和修改来构建测试,测试和设置。如果需要,我们可以建立信号灯。从某种意义上讲,这就是大世界的运作方式。如果我们考虑一下,如果一家银行需要将资金转移到另一家银行,那么它们就不在同一个关系系统中。他们每个人经常都有自己的关系数据库。即使我们不能在一个数据库中的一个银行中的一个系统中跨数据库系统开始事务和结束事务,他们也必须能够协调该操作。因此,软件中肯定有解决该问题的方法。
  • 容忍 -最终的方法通常是为了容忍一点不一致,它通常可以在现代Web应用程序和需要大量数据的其他应用程序中使用,这是可以接受的。例如,如果我们谈论的是Facebook中的朋友供稿,那么每个人是否同时看到您的墙更新都无关紧要。如果很好,那么如果一个人落后几秒钟,他们就追上了。在许多系统设计中,通常并不重要的一点是,所有内容都应保持完全一致,并且每个人都具有完全一致的数据库视图。因此,我们可以简单地容忍一些暂时性的不一致。

UpdatefindAndModify$addToSet(更新内)$push(更新内)操作在单个文档中以原子方式操作。


2
我喜欢这个答案的方式,而不是继续质疑我们是否应该回到关系数据库。谢谢@xameeramir!
DonnyTian

3
如果您有多台服务器,则必须使用外部分布式锁定服务,否则关键的代码部分将无法工作
Alexander Mills

@AlexanderMills您能详细说明一下吗?
Zameer

答案似乎来自这里的视频抄本:youtube.com/watch?
Fritz,

我认为这很好,直到我们被限制必须对单个集合进行操作。但是由于种种原因(文档大小或您使用的是引用),我们无法将所有内容都放在一个文档中。我认为那时我们可能需要交易。
user2488286

24

看看这个,Tokutek。他们为Mongo开发了一个插件,该插件不仅可以保证交易,而且可以提高性能。


@乔凡尼·比特里纳(Giovanni Bitliner)。此后,Percona收购了Tokutek,在您提供的链接上,我看不到任何有关自该职位以来发生的任何事情的信息。你知道他们的努力发生了什么吗?我通过电子邮件发送了该页面上的电子邮件地址以进行查找。
泰勒·科利尔

您具体需要什么?如果您需要将Toku技术应用于Mongodb,请尝试github.com/Tokutek/mongo,如果您需要mysql版本,也许他们将其添加到了他们通常提供的Mysql的标准版本中
Giovanni Bitliner

如何将tokutek与nodejs集成在一起。
Manoj Sanjeewa

11

直截了当:如果必须具有事务完整性,则不要使用MongoDB,而只能使用系统中支持事务的组件。为不兼容ACID的组件提供类似于ACID的功能而在组件之上构建某些东西非常困难。根据各个用例,以某种方式将动作分为事务性动作和非事务性动作可能是有道理的...


1
我猜你的意思是NoSQL可以用作经典RDBMS的辅助数据库。我不喜欢在同一项目中混合使用NoSQL和SQL的想法。它增加了复杂性,并且可能还会引入一些非平凡的问题。
NagyI,2011年

1
NoSQL解决方案很少单独使用。文件存储(mongo和沙发)可能是此规则唯一的理解。
卡罗里·霍瓦斯

7

现在有什么问题吗?MongoDB只能对一个文档进行原子更新。在上一个流程中,可能会发生某种错误,消息已存储在数据库中,但用户余额没有减少和/或未记录交易。

这不是真正的问题。您提到的错误是逻辑(错误)或IO错误(网络,磁盘故障)。这种错误可能会使无事务存储和事务存储处于不一致状态。例如,如果它已经发送了SMS,但是在存储消息时发生错误-它无法回滚SMS发送,这意味着将不会对其进行记录,不会减少用户余额等。

这里真正的问题是用户可以利用比赛条件并发送超出其余额允许范围的消息。这也适用于RDBMS,除非您使用余额字段锁定(这将是一个很大的瓶颈)在事务内部进行SMS发送。对于MongoDB来说,可能的解决方案是findAndModify先减少余额并检查余额,如果负数则不允许发送和退款(原子增量)。如果是肯定的,则继续发送,以防万一退款失败。还可以维护余额历史记录收集,以帮助修复/验证余额字段。


感谢您的出色回答!我确实知道,如果我使用支持事务的存储,由于SMS系统无法控制,数据可能会损坏。但是,使用Mongo时,内部也有可能发生数据错误。假设代码使用findAndModify更改了用户的余额,余额变为负数,但是在我可以纠正错误之前,会发生错误,并且应用程序需要重新启动。我猜你的意思是我应该基于事务集合实现类似于两阶段提交的操作,并对数据库进行定期更正检查。
NagyI,2011年

9
并非如此,如果您不进行最终提交,则事务性存储将回滚。
卡罗里·霍瓦斯

9
另外,您不发送SMS,然后登录到DB,那是完全错误的。首先将所有内容存储在数据库中并进行最后的提交,然后可以发送消息。此时,某些操作仍然可能失败,因此您需要执行cron作业以检查消息是否已实际发送(如果未尝试发送)。也许专用的消息队列对此会更好。但是,总的来说,要归结为您是否可以通过交易方式发送
SMS

@NagyI是的,这就是我的意思。为了易于扩展,必须权衡交易的好处。基本上,应用程序必须期望不同集合中的任何两个文档可能处于不一致状态,并准备好处理该状态。@yi_H它将回滚,但是状态将不再是实际的(有关消息的信息将丢失)。这并不仅仅是拥有部分数据(比如减少了余额但没有消息信息,反之亦然)并没有多大好处。
pingw33n

我懂了。这实际上不是一个容易的约束。也许我应该更多地了解RDBMS系统如何进行事务处理。您可以推荐一些在线材料或书以供我阅读吗?
NagyI,2011年

6

该项目很简单,但是您必须支持交易进行付款,这使整个事情变得困难。因此,例如,具有数百个集合(论坛,聊天,广告等)的复杂门户系统在某种程度上更简单,因为如果您丢失了论坛或聊天条目,没有人真正在乎。另一方面,如果您丢失了付款交易,这是一个严重的问题。

因此,如果您真的想要MongoDB的试验项目,请选择一个在方面很简单的项目。


感谢您的解释。悲伤,就听到这个消息。我喜欢NoSQL的简单性和JSON的使用。我们正在寻找ORM的替代方案,但看起来我们必须坚持一段时间。
NagyI,2011年

您能给出任何很好的理由说明为什么MongoDB在此任务上比SQL更好吗?试点项目听起来有点傻。
卡罗莉·霍瓦斯

我并不是说MongoDB比SQL更好。我们只是想知道它是否比SQL + ORM好。但是现在越来越清楚的是,他们在这类项目中没有竞争力。
NagyI 2011年

6

出于有效的原因,MongoDB中没有事务。这是使MongoDB更快的原因之一。

在您的情况下,如果必须进行交易,则mongo似乎不太适合。

可能是RDMBS + MongoDB,但这会增加复杂性并使管理和支持应用程序变得更加困难。


1
现在有一个名为TokuMX的MongoDB发行版,该发行版使用分形技术提供了50倍的性能改进,并同时提供了完整的ACID交易支持:tokutek.com/tokumx-for-mongodb
OCDev 2015年

9
交易怎么可能不是必须的。一旦您需要一种需要更新2个表的简单情况,mongo突然不再适合吗?这根本不会留下很多用例。
Mr_E

1
@Mr_E同意,这就是MongoDB有点愚蠢的原因:)
Alexander Mills

6

这可能是我发现的有关实现mongodb的类似事务的最佳博客。

同步标志:最适合仅从主文档复制数据

Job Queue:非常通用,可解决95%的情况。无论如何,大多数系统至少需要一个作业队列!

两阶段提交:此技术可确保每个实体始终拥有进入一致状态所需的所有信息

日志对帐:最可靠的技术,非常适合金融系统

版本控制:提供隔离并支持复杂的结构

阅读此以获取更多信息:https : //dzone.com/articles/how-implement-robust-and


请在回答中包括链接资源中回答问题所需的相关部分。照原样,您的答案很容易受到链接腐烂的影响(例如,如果链接的网站出现故障或更改,则您的答案可能无用)。
机甲

感谢@mech的建议
Vaibhav的

4

这已经很晚了,但是认为这会在将来有所帮助。我使用Redis进行排队以解决此问题。

  • 要求:
    下图显示2个操作需要同时执行,但是操作1的阶段2和3需要在开始操作2的阶段2或相反之前完成(一个阶段可以是请求REST api,数据库请求或执行javascript代码... )。 在此处输入图片说明

  • 队列如何帮助您
    队列确保在多个函数之间lock()以及release()在多个函数中的每个块代码都不会同时运行,从而使它们隔离。

    function action1() {
      phase1();
      queue.lock("action_domain");
      phase2();
      phase3();
      queue.release("action_domain");
    }
    
    function action2() {
      phase1();
      queue.lock("action_domain");
      phase2();
      queue.release("action_domain");
    }
  • 如何建立队列
    我将仅着眼于在后端站点上建立队列时如何避免竞争条件。如果您不了解排队的基本概念,请来到这里
    下面的代码仅显示概念,您需要以正确的方式实施。

    function lock() {
      if(isRunning()) {
        addIsolateCodeToQueue(); //use callback, delegate, function pointer... depend on your language
      } else {
        setStateToRunning();
        pickOneAndExecute();
      }
    }
    
    function release() {
      setStateToRelease();
      pickOneAndExecute();
    }

但是您需要isRunning() setStateToRelease() setStateToRunning()隔离自身,否则您将再次面临比赛条件。为此,我选择Redis用于ACID目的并具有可伸缩性。
Redis 文档谈论它的事务:

事务中的所有命令都被序列化并顺序执行。在Redis事务的执行过程中,永远不会发生另一个客户端发出的请求。这样可以确保将命令作为单个隔离操作执行。

P / s:
我使用Redis是因为我的服务已经使用它,您可以使用任何其他支持隔离的方法来实现。
action_domain在我的代码是上面的时候,你只需要操作1调用用户A的用户A块动作2,不要妨碍其他用户的。这个想法是为每个用户锁定一个唯一的密钥。


如果您的分数已经更高,您将获得更多的赞誉。这就是大多数人的想法。您的答案在问题中很有用。我支持你
Mukus '18

3

现在,MongoDB 4.0中提供了事务。在这里取样

// Runs the txnFunc and retries if TransientTransactionError encountered

function runTransactionWithRetry(txnFunc, session) {
    while (true) {
        try {
            txnFunc(session);  // performs transaction
            break;
        } catch (error) {
            // If transient error, retry the whole transaction
            if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes("TransientTransactionError")  ) {
                print("TransientTransactionError, retrying transaction ...");
                continue;
            } else {
                throw error;
            }
        }
    }
}

// Retries commit if UnknownTransactionCommitResult encountered

function commitWithRetry(session) {
    while (true) {
        try {
            session.commitTransaction(); // Uses write concern set at transaction start.
            print("Transaction committed.");
            break;
        } catch (error) {
            // Can retry commit
            if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes("UnknownTransactionCommitResult") ) {
                print("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                print("Error during commit ...");
                throw error;
            }
       }
    }
}

// Updates two collections in a transactions

function updateEmployeeInfo(session) {
    employeesCollection = session.getDatabase("hr").employees;
    eventsCollection = session.getDatabase("reporting").events;

    session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

    try{
        employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } );
        eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } );
    } catch (error) {
        print("Caught exception during transaction, aborting.");
        session.abortTransaction();
        throw error;
    }

    commitWithRetry(session);
}

// Start a session.
session = db.getMongo().startSession( { mode: "primary" } );

try{
   runTransactionWithRetry(updateEmployeeInfo, session);
} catch (error) {
   // Do something with error
} finally {
   session.endSession();
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.