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

Posted

技术标签:

【中文标题】如何解决 MongoDB 中缺少事务的问题?【英文标题】:How to work around the lack of transactions in MongoDB? 【发布时间】:2011-10-01 22:02:50 【问题描述】:

我知道这里也有类似的问题,但如果我需要事务或使用atomic operations 或two-phase commit,它们要么是telling me 以切换回常规RDBMS 系统。第二种解决方案似乎是最佳选择。第三个我不想遵循,因为似乎很多事情都可能出错,而且我无法在各个方面都对其进行测试。我很难重构我的项目以执行原子操作。我不知道这是否来自我有限的观点(我到目前为止只使用过 SQL 数据库),还是实际上无法做到。

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

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

在这个简化的系统中想象一个具有以下步骤的 SMS 发送任务:

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

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

    通过发送消息的成本减少用户的余额

    在事务集合中记录事务

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

我想出了两个想法:

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

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

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

【问题讨论】:

如果我错了,请原谅我,但看起来这个项目将使用 NoSQL 数据存储,无论它是否会从中受益。 NoSQL 不是作为“时尚”选择的 SQL 的替代品,而是当关系 RDBMS 的技术不适合问题空间而非关系数据存储适合时。您的很多问题都是“如果是 SQL,那么……”,这给我敲响了警钟。所有的 NoSQL 都来自于解决 SQL 无法解决的问题的需要,然后它们被普遍化以使其更易于使用,然后当然潮流开始滚动。 我知道这个项目并不是尝试 NoSQL 的最佳选择。但是我很害怕如果我们开始将它与其他项目一起使用(假设是图书馆馆藏管理软件,因为我们从事馆藏管理)并且突然出现某种需要交易的请求(实际上它就在那里,想象一本书从一个集合转移到另一个集合)我们需要知道如何克服这个问题。也许只是我心胸狭窄,认为交易总是有必要的。但可能有办法以某种方式克服这些问题。 我同意 PurplePilot,您应该选择适合解决方案的技术,而不是试图将不适合的解决方案移植到问题上。图数据库的数据建模与 RDBMS 设计是完全不同的范式,你必须忘记你所知道的一切,重新学习新的思维方式。 我明白我应该使用适当的工具来完成这项任务。但是对我来说——当我读到这样的答案时——NoSQL 似乎不适用于数据至关重要的任何事情。这对 Facebook 或 Twitter 来说是件好事,如果一些 cmets 迷路了,世界还在继续,但除此之外的任何东西都停业了。如果那是真的,我不明白为什么其他人关心建设例如。一个带有 MongoDB 的网上商店:kylebanker.com/blog/2010/04/30/mongodb-and-ecommerce 它甚至提到大多数事务都可以通过原子操作来克服。我正在寻找的是方法。 您说“NoSQL 似乎不适用于数据至关重要的任何事情”在它不好(也许)是事务性 ACID 类型事务处理的地方是不正确的。此外,NoSQL 专为分布式数据存储而设计,当您进入主从复制场景时,SQL 类型的存储可能很难实现。 NoSQL 有最终一致性的策略,并确保只使用最新的数据集而不是 ACID。 【参考方案1】:

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

更多详情请见here。

拥有分布式事务并不意味着您应该像在表格关系数据库中那样对数据进行建模。拥抱文档模型的力量并遵循数据建模的良好和推荐practices。

【讨论】:

交易已到! 4.0 通用版。 mongodb.com/blog/post/… MongoDB 事务仍然对事务 16 MB 的大小有限制,最近我有一个用例,我需要将文件中的 50k 记录放入 mongoDB,所以为了维护我认为的原子属性使用事务但由于 50k json 记录超过此限制,因此会引发错误“所有事务操作的总大小必须小于 16793600。实际大小为 16793817”。有关更多详细信息,您可以通过在 mongoDB jira.mongodb.org/browse/SERVER-36330 上打开的官方 jira 票证 MongoDB 4.2(目前处于测试阶段,RC4)支持大型事务。通过表示跨多个 oplog 条目的事务,您将能够在单个 ACID 事务中写入超过 16MB 的数据(取决于现有的 60 秒默认最大执行时间)。你现在可以试试 - mongodb.com/download-center/community MongoDB 4.2 现在是 GA,完全支持分布式事务。mongodb.com/blog/post/…【参考方案2】:

检查this,由 Tokutek 提供。他们为 Mongo 开发了一个插件,它不仅承诺交易,还承诺提高性能。

【讨论】:

@Giovanni Bitliner。此后,Tokutek 已被 Percona 收购,在您提供的链接中,我没有看到任何信息提及自该帖子以来发生的任何事情。你知道他们的努力发生了什么吗?我通过电子邮件发送了该页面上的电子邮件地址以进行查找。 您具体需要什么?如果您需要将 toku 技术应用于 Mongodb,请尝试 github.com/Tokutek/mongo ,如果您需要 mysql 版本,也许他们将其添加到他们通常提供的标准版本的 Mysql 中 如何将 tokutek 与 nodejs 集成。【参考方案3】:

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

【讨论】:

我猜你的意思是 NoSQL 可以用作经典 RDBMS 的辅助数据库。我不喜欢在同一个项目中混合 NoSQL 和 SQL 的想法。它增加了复杂性,并可能引入一些重要的问题。 NoSQL 解决方案很少单独使用。文档存储(mongo 和 couch)可能是这条规则的唯一例外。【参考方案4】:

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

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

这里真正的问题是用户可以利用竞争条件并发送比他的余额允许的更多的消息。这也适用于 RDBMS,除非您使用余额字段锁定在事务内部发送 SMS(这将是一个很大的瓶颈)。作为 MongoDB 的一个可能的解决方案是首先使用findAndModify 来减少余额并检查它,如果它是负数,则不允许发送并退还金额(原子增量)。如果是肯定的,请继续发送,如果失败,请退还金额。还可以维护余额历史集合以帮助修复/验证余额字段。

【讨论】:

感谢您的出色回答!我确实知道,如果我使用支持事务的存储,数据可能会因为我无法控制的 SMS 系统而损坏。然而,对于 Mongo,数据错误也有可能在内部发生。假设代码使用 findAndModify 更改了用户的余额,余额变为负数,但在我纠正错误之前发生错误并且应用程序需要重新启动。我猜你的意思是我应该基于事务集合实现类似于两阶段提交的东西,并对数据库进行定期更正检查。 不正确,如果您不进行最终提交,事务存储将回滚。 另外,您不发送短信然后登录数据库,这是完全错误的。首先将所有内容存储在数据库中并进行最终提交,然后您可以发送消息。此时某些事情仍然可能失败,因此您需要一个 cron 作业来检查消息是否实际发送,如果没有尝试发送。也许专用的消息队列会更好。但整个事情归结为您是否可以以交易方式发送短信...... @NagyI 是的,这就是我的意思。为了便于扩展,人们必须交换交易的好处。基本上应用程序必须期望不同集合中的任何两个文档都可以处于不一致的状态并准备好处理这个问题。 @yi_H 它将回滚,但状态将不再真实(有关消息的信息将丢失)。这并不比只有部分数据好多少(比如余额减少但没有消息信息,反之亦然)。 我明白了。这实际上不是一个容易的约束。也许我应该更多地了解 RDBMS 系统如何进行交易。你能推荐一些我可以阅读的在线资料或书籍吗?【参考方案5】:

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

所以,如果你真的想要一个 MongoDB 的试点项目,请选择一个在 方面简单的项目。

【讨论】:

感谢您的解释。听到这个消息很难过。我喜欢 NoSQL 的简单性和 JSON 的使用。我们正在寻找 ORM 的替代品,但看起来我们必须坚持一段时间。 您能给出任何充分的理由说明为什么 MongoDB 在这项任务上比 SQL 更好吗?试点项目听起来有点傻。 我没有说 MongoDB 比 SQL 好。我们只是想知道它是否比 SQL+ORM 更好。但现在越来越清楚的是,他们在这类项目中没有竞争力。【参考方案6】:

由于正当的原因,MongoDB 中没有事务。这是使 MongoDB 更快的原因之一。

在你的情况下,如果交易是必须的,mongo 似乎不太合适。

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

【讨论】:

现在有一个名为 TokuMX 的 MongoDB 发行版,它使用分形技术将性能提高 50 倍,同时提供完整的 ACID 事务支持:tokutek.com/tokumx-for-mongodb 交易怎么可能不是“必须的”。一旦您需要 1 个需要更新 2 个表的简单案例,mongo 突然不再适合?这根本不会留下很多用例。 @Mr_E 同意,这就是 MongoDB 有点笨的原因 :)【参考方案7】:

这可能是我找到的关于为 mongodb 实现类似事务的功能的最佳博客。!

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

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

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

Log Reconciliation:最强大的技术,非常适合金融系统

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

阅读本文了解更多信息:https://dzone.com/articles/how-implement-robust-and

【讨论】:

请在您的答案中包含回答问题所需的链接资源的相关部分。照原样,您的答案很容易受到链接失效的影响(即,如果链接的网站出现故障或更改您的答案可能毫无用处)。 感谢@mech 的建议【参考方案8】:

这已经晚了,但认为这对将来会有帮助。我使用Redis 制作queue 来解决这个问题。

要求: 下图显示 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");

如何建立队列 我将只关注在后端站点上构建队列时如何避免race conditon 部分。如果你不知道队列的基本概念,来here。 下面的代码只展示了概念,你需要以正确的方式实现。

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 目的和可扩展性。 Redisdocument说说它的事务:

事务中的所有命令都被序列化并执行 依次。永远不会发生另一个人发出的请求 客户端在 Redis 执行过程中提供服务 交易。这保证了命令作为 单个隔离操作。

P/s: 我使用 Redis 是因为我的服务已经在使用它,您可以使用任何其他支持隔离的方式来做到这一点。 我的代码中的 action_domain 上面是当您只需要用户 A 阻止用户 A 的操作 2 的操作 1 时,不要阻止其他用户。这个想法是为每个用户锁定一个唯一的密钥。

【讨论】:

如果您的分数已经更高,您会收到更多的赞成票。这就是这里的大多数人的想法。您的答案在问题的上下文中很有用。我给你投了赞成票。【参考方案9】:

事务现在在 MongoDB 4.0 中可用。示例here

// 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();

【讨论】:

以上是关于如何解决 MongoDB 中缺少事务的问题?的主要内容,如果未能解决你的问题,请参考以下文章

156期数据库分库分表之后,如何解决事务问题?

如何在 mongodb 事务中测试翻转功能

如何使用 Mongoose 使用 MongoDB 事务?

常见问题:MongoDB基础知识

MongoDB Compass 缺少架构选项卡

面试突击86:SpringBoot 事务不回滚?怎么解决?