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

时间:2011-07-09 15:34:01

标签: mongodb transactions

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

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

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

想象一下SMS发送任务,在此简化系统中执行以下步骤:

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

  2. 将消息发送并存储在SMS集合中,其中包含详细信息和成本(在实时系统中,消息将具有status属性,并且任务将获取它以进行传递并设置价格短信根据其现状)

  3. 通过发送消息的费用减少用户的余额

  4. 在事务集合中记录事务

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

    我提出了两个想法:

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

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

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

10 个答案:

答案 0 :(得分:81)

没有交易的生活

事务支持ACID属性,但虽然MongoDB中没有事务,但我们确实有原子操作。好吧,原子操作意味着当您处理单个文档时,该工作将在其他人看到该文档之前完成。他们会看到我们所做的所有更改,或者都没有。使用原子操作,您通常可以完成使用关系数据库中的事务完成的相同操作。原因是,在关系数据库中,我们需要跨多个表进行更改。通常需要加入的表格,因此我们希望一次完成所有这些操作。要做到这一点,因为有多个表,我们必须开始一个事务并执行所有这些更新,然后结束事务。但是对于MongoDB,我们会嵌入数据,因为我们会在文档中预加入,并且他们会将这些丰富的文档添加到文档中有层次结构。我们经常可以完成同样的事情。例如,在博客示例中,如果我们想确保以原子方式更新博客文章,我们可以这样做,因为我们可以立即更新整个博客文章。如果它是一堆关系表,我们可能必须打开一个事务,以便我们可以更新帖子集合和评论集合。

那么我们采用MongoDB来克服缺乏交易的方法是什么?

  • 重组 - 重新构建代码,以便我们在单个文档中工作并利用我们在该文档中提供的原子操作。如果我们这样做,那么通常我们都会设置好。
  • 在软件中实施 - 我们可以通过创建关键部分来实现软件锁定。我们可以使用查找和修改来构建测试,测试和设置。如果需要,我们可以建立信号量。从某种程度上说,这就是大世界的工作方式。如果我们考虑一下,如果一家银行需要将资金转移到另一家银行,他们就不会生活在同一个关系系统中。而且他们每个人都经常拥有自己的关系数据库。即使我们无法跨这些数据库系统开始事务和结束事务,也只能在一个银行内的一个系统内,他们能够协调该操作。因此,软件中肯定有办法解决问题。
  • 容忍 - 最终的方法,通常适用于现代网络应用程序和其他接收大量数据的应用程序,只是容忍一些不一致。一个例子是,如果我们在Facebook上谈论朋友提要,如果每个人都同时看到你的墙更新并不重要。如果有的话,如果有一个人在几秒钟内击败了他们并且他们赶上了。在许多系统设计中,通常并不重要,所有内容都保持完全一致,并且每个人都拥有完全一致且相同的数据库视图。因此,我们可以简单地容忍一点点不一致,这有点暂时。

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

答案 1 :(得分:24)

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

答案 2 :(得分:16)

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

有关详细信息,请参阅here

答案 3 :(得分:11)

提出要点:如果事务完整性是必须,那么不要使用MongoDB,而只使用支持事务的系统中的组件。在组件之上构建一些东西是非常困难的,以便为非ACID兼容组件提供类似ACID的功能。根据个人用例,以某种方式将行动分为交易和非交易行为可能是有意义的......

答案 4 :(得分:7)

  

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

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

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

答案 5 :(得分:6)

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

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

答案 6 :(得分:6)

由于正当理由,MongoDB中缺少交易。这是使MongoDB更快的事情之一。

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

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

答案 7 :(得分:6)

这可能是我发现的关于为mongodb实现交易功能的最佳博客。!

同步标记:最好只从主文档复制数据

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

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

日志对帐:最强大的技术,非常适合财务系统

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

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

答案 8 :(得分:4)

这已经很晚了但是认为这将有助于将来。我使用Redis制作queue来解决此问题。

  • <强>要求:
    下图显示了2个动作需要同时执行,但动作1的阶段2和阶段3需要在动作2的开始阶段2之前完成或相反(A阶段可以是请求REST api,数据库请求或执行javascript代码......)。 enter image description here

  • 队列如何帮助您
    队列确保许多函数中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目的并且可扩展 Redis document谈论它的交易:

  

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

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

答案 9 :(得分:3)

事务现在在MongoDB 4.0中可用。样本here

String strInsert = "INSERT INTO " +
        "Orders (OCollectionmode, OBillingname, OBillingAddress, OBillingPostalCode, OBillingContact, ODeliveryDate, ODeliverytime, OBillingoutlet, OStatus"
        + " values (@collection, @bname, @baddress, @bpostal, @bcontact,@ddate,@dtime, @boutlet, @status)";