【问题标题】:It's not possible to lock a mongodb document. What if I need to?无法锁定 mongodb 文档。如果我需要怎么办?
【发布时间】:2012-06-20 00:49:55
【问题描述】:

我知道我无法锁定单个mongodb文档,实际上也没有办法锁定集合。

但是,我遇到了这种情况,我认为我需要某种方法来防止多个线程(或进程,这并不重要)修改文档。这是我的场景。

我有一个包含 A 类型对象的集合。我有一些代码可以检索 A 类型的文档,在数组中添加一个元素,该元素是文档的属性 (a.arr.add(new Thing()),然后保存回文档到mongodb。这段代码是并行的,我的应用程序中的多个线程可以执行这些操作,现在没有办法阻止线程在同一个文档上并行执行这些操作。这很糟糕,因为其中一个线程可能会覆盖另一个线程的工作。

我确实使用存储库模式来抽象对 mongodb 集合的访问,因此我只有 CRUD 操作可供我使用。

现在我想起来了,也许是存储库模式的限制,而不是 mongodb 的限制给我带来了麻烦。无论如何,我怎样才能使这段代码“线程安全”?我想这个问题有一个众所周知的解决方案,但是对于 mongodb 和存储库模式是新手,我不会立即看到它。

谢谢

【问题讨论】:

    标签: mongodb repository-pattern


    【解决方案1】:

    从 4.0 开始,MongoDB 支持 Transactions 用于副本集。 MongoDB 4.2 将支持分片集群。使用事务,如果发生写入冲突,数据库更新将被中止,从而解决您的问题。

    事务在性能方面的成本要高得多,所以不要将事务作为糟糕的 NoSQL 架构设计的借口!

    【讨论】:

    • 有没有人除了理论还有编码方案?
    【解决方案2】:

    我有一个类似的问题,我有同一个应用程序的多个实例,它们将从数据库中提取数据(顺序无关紧要;所有文档都必须更新 - 高效),处理它并写回结果。然而,在没有任何锁定的情况下,所有实例显然都提取了相同的文档,而不是智能地分配他们的劳动力。

    我试图通过在应用程序级别上实现锁定来解决它,这将在当前正在编辑的相应文档中添加一个locked-字段,这样我的应用程序的其他实例就不会选择相同的文档并且通过执行与其他实例相同的操作来浪费时间。

    但是,当运行我的应用程序的数十个或更多实例时,读取文档(使用find())和将locked-field 设置为true(使用update())之间的时间跨度和实例仍然从数据库中提取相同的文档,这让我使用多个实例加快工作速度的想法毫无意义。

    这里有 3 条建议,可能会根据您的情况解决您的问题:

    1. 使用findAndModify() 因为使用该函数的读写操作是原子的。从理论上讲,您的应用程序的一个实例所请求的文档应该对其他实例显示为已锁定。并且当文档被解锁并再次对其他实例可见时,它也会被修改。

    2. 但是,如果您需要在读取 find() 和写入 update() 操作之间做其他事情,您可以使用 transactions

    3. 或者,如果这不能解决您的问题,那么一些简单的解决方案(可能就足够了)是让应用程序大批量提取文档,并让每个实例从该批次中选择一个随机文档并对其进行处理。显然,这种阴暗的解决方案是基于这样一个事实,即巧合不会影响您的应用程序的效率。

    【讨论】:

      【解决方案3】:

      如果数组中元素的顺序对您来说并不重要,那么$push 运算符应该足够安全,以防止线程覆盖彼此的更改。

      【讨论】:

        【解决方案4】:

        我没有在另一个问题中写这个问题,而是尝试回答这个问题:我想知道这个 WiredTiger Storage 是否会处理我在这里指出的问题: Limit inserts in mongodb

        【讨论】:

          【解决方案5】:

          更新: MongoDB 3.2.2 使用 WiredTiger Storage 实现作为默认引擎,MongoDB 在文档级别使用默认锁定。它是在 3.0 版中引入的,但在 3.2.2 版中默认。因此 MongoDB 现在有文档级锁定。

          【讨论】:

          • 有没有人除了理论还有编码方案?
          【解决方案6】:

          在进行 mongodb 升级时偶然发现了这个问题。与提出这个问题时不同,现在 mongodb 支持开箱即用的文档级别锁定。

          发件人:http://docs.mongodb.org/manual/faq/concurrency/

          “MongoDB 中的锁有多精细?

          在 3.0 版中更改。

          从 3.0 版开始,MongoDB 附带了 WiredTiger 存储引擎,该引擎对大多数读写操作使用乐观并发控制。 WiredTiger 仅在全局、数据库和集合级别使用意图锁。当存储引擎检测到两个操作之间的冲突时,会引发写入冲突,导致 MongoDB 透明地重试该操作。”

          【讨论】:

          • 如果我想在创建文档对象的过程中加锁怎么办?
          【解决方案7】:

          如果您的系统拥有超过 1 台服务器,那么您将需要一个分布式锁。

          我更喜欢使用Hazelcast

          在保存时,您可以通过实体 id 获取 Hazelcast 锁,获取和更新数据,然后释放锁。

          例如: https://github.com/azee/template-api/blob/master/template-rest/src/main/java/com/mycompany/template/scheduler/SchedulerJob.java

          只需使用lock.lock() 而不是lock.tryLock()

          在这里你可以看到如何在你的 spring 上下文中配置 Hazelcast:

          https://github.com/azee/template-api/blob/master/template-rest/src/main/resources/webContext.xml

          【讨论】:

            【解决方案8】:

            当您想要使某些东西成为线程安全的东西时,经典的解决方案是使用锁(互斥锁)。 这也称为悲观锁定,而不是here 描述的乐观锁定

            在某些情况下悲观锁定更有效(更多细节here)。它也更容易实现(乐观锁定的主要困难是从碰撞中恢复)。

            MongoDB 不提供锁机制。但这可以在应用程序级别轻松实现(即在您的代码中):

            1. 获取锁
            2. 阅读文档
            3. 修改文档
            4. 写文档
            5. 解除锁定

            锁的粒度可以不同:全局、特定于集合、特定于记录/文档。锁越具体,其性能损失就越小。

            【讨论】:

            • 你如何等待锁?
            • 获取锁操作通常会等待其他线程持有的锁。
            • 这不适用于具有多个实例的应用程序。
            • 有没有人除了理论还有编码方案?
            【解决方案9】:

            听起来你想使用 MongoDB 的原子运算符:http://www.mongodb.org/display/DOCS/Atomic+Operations

            【讨论】:

            • 原子操作符的问题在于它们并没有真正帮助我,因为我使用的是存储库模式,所以我只能使用 CRUD 操作。
            【解决方案10】:

            另一种方法是in place update

            例如:

            http://www.mongodb.org/display/DOCS/Updating#comment-41821928

            db.users.update( { level: "Sourcerer" }, { '$push' : { 'inventory' : 'magic wand'} }, false, true );
            

            这会将“魔杖”推送到所有“Sourcerer”用户的库存数组中。对每个文档/用户的更新是原子的。

            【讨论】:

              【解决方案11】:

              回答我自己的问题,因为我在互联网上进行研究时找到了解决方案。

              我认为我需要做的是使用Optimistic Concurency Control

              它包括为每个文档添加时间戳、哈希或其他唯一标识符(我将使用 UUID)。每次修改文档时都必须修改唯一标识符。在更新文档之前,我会做这样的事情(在伪代码中):

              var oldUUID = doc.uuid;
              doc.uuid = new UUID();
              BeginTransaction();
              if (GetDocUUIDFromDatabase(doc.id) == oldUUID)
              {
                 SaveToDatabase(doc);
                 Commit();
              }
              else
              {
                 // Document was modified in the DB since we read it. We can't save our changes.
                 RollBack();
                 throw new ConcurencyException();
              }
              

              【讨论】:

              • 是的,这是解决冲突的一种方法。
              • 您可以这样做,但是使用原子运算符描述的其他一些答案可能是您想要的(并且是您想要的原子)。以下是文档:mongodb.org/display/DOCS/Atomic+Operations
              • 我们有一个类似的问题,我们发布了一个类似的问题,但方法有点不同。我们仍然不确定性能。你可以在这里阅读:stackoverflow.com/questions/58609347/…
              【解决方案12】:

              嘿,我认为现在唯一的方法是添加一个状态参数并使用操作findAndModify(),它使您能够原子地修改文档。它有点慢,但应该可以解决问题。

              假设您添加了一个状态属性,当您检索文档时,将状态从“IDLE”更改为“PROCESSING”。然后更新文档并将其保存回集合,再次将状态更新为“IDLE”。

              代码示例:

              var doc = db.runCommand({
                            "findAndModify" : "COLLECTION_NAME",
                            "query" : {"_id": "ID_DOCUMENT", "status" : "IDLE"},
                            "update" : {"$set" : {"status" : "RUNNING"} }
              }).value
              

              将 COLLECTION_NAME 和 ID_DOCUMENT 更改为适当的值。默认情况下 findAndModify() 返回旧值,这意味着状态值在客户端仍然是 IDLE。因此,当您完成更新后,只需再次保存/更新所有内容。

              唯一需要注意的是,您一次只能修改一个文档。

              希望对你有帮助。

              【讨论】:

              • 你可以使用简单的 update() 来达到同样的目的,这是 MongoDB 网站上提供的官方解决方案:docs.mongodb.org/manual/tutorial/isolate-sequence-of-operations 这个解决方案的主要复杂性是你必须为这个案例编写代码更新失败时。 IE。重试更新。根据您的代码,您可能不得不遇到更多复杂情况以避免重试时的副作用等。
              • 另一个客户端如何等待锁被释放?即当status 发生变化时,您如何获得通知?
              • 如果我想在创建文档对象的过程中加锁怎么办?
              • @slezica 是对的,您能找到解决方案吗?另一个客户端如何得知释放被锁定的文档?
              • 很遗憾他们没有扩展 findAndModify() 以处理多个文档。
              【解决方案13】:

              “医生,我这样做的时候很痛这个

              “那就不要那个!”

              基本上,您所描述的内容听起来像是您在那里有一个串行依赖项——MongoDB 或其他什么,您的算法有一个操作必须被序列化的点。这将是一个固有的瓶颈,如果您绝对必须这样做,则必须安排某种信号量来保护它。

              所以,要看你的算法。你能消除它吗?例如,您能否通过某种冲突解决方法来处理它,例如“将记录获取到本地更新;存储记录”,以便在存储后新记录将成为该键上的记录?

              【讨论】:

              • 查理,感谢您的回答。我不明白你提出的冲突解决方案。我同意我确实需要改变我的算法,我可以想象一些解决方案,但我觉得必须有一些商定的解决方案来解决这个问题。在我看来,这是许多使用 mongodb(或任何数据库)的人都遇到的经典问题。如果它是内存更新,我会知道如何使用互斥锁来“锁定”我想要更新的变量,因此一次只有一个线程更新它。我想我的问题是:其他程序员通常如何处理这种情况?
              • 很棒的评论。即使这是您必须做的工作,也不要因为某些工具不够好而这样做。
              • MongoDB 终于支持 Transactions :D stackoverflow.com/a/53800048/2757916
              猜你喜欢
              • 2021-09-13
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2022-01-17
              • 1970-01-01
              • 2014-10-19
              • 2013-11-14
              相关资源
              最近更新 更多