首页 文章

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

提问于
浏览
134

我知道这里有类似的问题,但如果我需要交易或使用atomic operationstwo-phase commit,它们要么切换回常规的RDBMS系统 . 第二种解决方案似乎是最佳选择 . 第三,我不会在各个方面对它进行测试 . 我知道这是否来自我有限的观点(到目前为止我只使用过SQL数据库),或者它是否真的无法完成 .

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

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

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

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

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

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

  • 在事务集合中记录事务

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

我提出了两个想法:

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

  • 为用户创建一个集合,为事务创建一个集合 . 可以有两种交易:具有正余额变化的信用购买和具有负余额变化的消息 . 交易可能有一个子文件;例如,在发送的消息中,SMS的详细信息可以嵌入到事务中 . 缺点:我不希望随着存储事务数量的增加,这种计算变得缓慢 .

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

10 回答

  • 7

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

  • 6

    没有交易的生活

    事务支持ACID属性,但虽然 MongoDB 中没有事务,但我们确实有原子操作 . 好吧,原子操作意味着当您处理单个文档时,该工作将在其他人看到该文档之前完成 . 他们必须开始交易并完成所有这些更新,然后结束交易 . 但是对于 MongoDB ,我们're going to embed the data, since we'在文档中转到 pre-join 它们're these rich documents that have hierarchy. We can often accomplish the same thing. For instance, in the blog example, if we wanted to make sure that we updated a blog post atomically, we can do that because we can update the entire blog post at once. Where as if it were a bunch of relational tables, we' d可能要打开一个事务以便我们可以更新post集合和评论集合 .

    那么,我们可以采取什么方法来克服缺乏交易?

    • restructure - 重构代码,以便我们全部设置 .

    • implement in software - 我们可以通过创建关键部分来实现软件锁定 . 我们可以使用查找和修改来构建测试,测试和设置 . 如果需要,我们可以 Build 信号量 . 从某种程度上说,这就是大世界的工作方式 . 如果我们考虑一下,如果一家银行需要将资金转移到另一家银行,那么即使我们无法在这些数据库系统中开始交易和结束交易,也只能在一个银行内的一个系统内,它们能够协调该操作 . 因此,软件无疑可以解决问题 .

    • tolerate - 最终方法,通常适用于现代网络应用程序和其他需要大量数据的应用程序,只是容忍一些不一致 . 如果我们每个人都同时看到您的墙壁更新,那么就是一个例子 . 如果有人认为,如果一个人在很多系统设计中都很关键,那么一切都要保持完全一致,并且每个人都拥有完全一致且相同的数据库视图 . 所以我们可以简单地容忍一点点不一致,这有点暂时 .

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

  • 81

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

    有关详细信息,请参阅here .

  • 11

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

  • 14

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

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

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

  • 6

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

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

  • 4

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

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

    可能是RDMBS MongoDB,但这将增加复杂性和意志使管理和支持应用程序变得更加困难 .

  • 2

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

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

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

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

    日志调节:最强大的技术,非常适合金融系统

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

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

  • 6

    这已经很晚了,但认为这将有助于将来 . 我使用Redis来制作一个queue来解决这个问题 .

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

    • How a queue help you
      队列确保在许多函数中 lock()release() 之间的每个块代码都不会同时运行,使它们隔离 .

    function action1(){
    阶段1();
    queue.lock( “action_domain”);
    阶段2();
    阶段3();
    queue.release( “action_domain”);
    }

    function action2(){
    阶段1();
    queue.lock( “action_domain”);
    阶段2();
    queue.release( “action_domain”);
    }

    • How to build a queue
      我将只关注在后端站点上构建队列时如何避免race conditon部分 . 如果你不知道队列的基本思路,请来here .
      下面的代码只显示概念,您需要以正确的方式实现 .

    function lock(){
    if(isRunning()){
    addIsolateCodeToQueue(); //使用回调,委托,函数指针......取决于您的语言
    } else {
    setStateToRunning();
    pickOneAndExecute();
    }
    }

    function release(){
    setStateToRelease();
    pickOneAndExecute();
    }

    但是你需要隔离它自己,否则你会再次遇到竞争状态 . 为此,我选择Redis用于ACID目的和可扩展 .
    Redis document谈论它的交易:

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

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

  • 24

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

相关问题