Mongoose在文档数组编辑和版本控制方面是否不具可扩展性?

4
我正在使用Node.js和MongoDB/Mongoose开发一个Web应用程序。我们最常用的模型是“记录”,它有许多子文档数组,例如“评论”、“预订”和“订阅者”。
在客户端应用程序中,每当用户点击“删除”按钮时,它就会向该特定评论的删除路由发出AJAX请求。我遇到的问题是,当许多这样的AJAX调用同时到达时,Mongoose会在某些(但不是所有)调用上失败并出现“找不到文档”的错误。
只有在快速且频繁地进行调用时才会出现这种情况。我认为这是由于Mongoose中的版本引起的文档冲突。我们当前的删除过程如下:
1. 使用Record.findById()获取文档。 2. 从相应的数组中删除子文档(使用comment.remove())。 3. 调用record.save()
我找到了一种解决方案,可以手动更新集合,然后使用$pull操作符。但是,这意味着我们不能使用mongoose的任何中间件,完全失去了版本控制。我越想越多,越意识到类似情况可能发生,我必须使用Mongoose的包装函数,例如findByIdAndUpdatefindAndRemove。我能想到的唯一解决方案是将删除尝试放入while循环中并希望它有效,但这似乎是一个非常糟糕的修复方法。
使用Mongoose包装器并不能真正解决我的问题,因为它不允许我使用任何类型的中间件或钩子,而这实际上是使用Mongoose的巨大优势之一。
这是否意味着Mongoose在快速编辑方面基本无用,我最好使用原生的MongoDB驱动程序?我是否误解了Mongoose的限制?我该如何解决这个问题?
4个回答

7
Mongoose的版本化文档数组编辑不可扩展,因为它不是原子操作。因此,您进行越多的数组编辑活动,就越有可能发生两次编辑冲突,并且您的代码将承担重试/恢复的开销。
为了进行可伸缩的文档数组操作,您必须使用update和原子数组更新$pull[All]$push[All]$pop$addToSet$运算符:操作符。当然,如果您还需要原始文档或结果文档,则也可以使用这些运算符与findByIdAndUpdatefindOneAndUpdate基于findAndModify的原子方法。
正如您提到的,使用update而不是findOne+save的一个很大的缺点是在update期间不执行任何Mongoose中间件和验证。但如果您想要一个可扩展的系统,我认为您没有选择。我宁愿手动复制一些中间件和验证逻辑以处理更新情况,也不愿意承受使用Mongoose版本化文档数组编辑的可扩展性惩罚。嘿,至少您仍然可以在更新期间获得Mongoose基于架构的类型转换的好处!

不是我所希望的答案,但却是最有道理的答案。感谢您提供了一个合适的可扩展解决方案! - Chris Foster

3
我认为,从我们自己的经验来看,你问题的答案是“是的”。对于快速基于数组的更新,Mongoose不具有可扩展性。
背景
我们在HabitRPG也遇到了同样的问题。在最近用户增长激增(将我们的数据库增加到6GB)后,我们开始遇到许多基于数组的更新的VersionError关于VersionError的背景)。ensureIndex({_id:1,__v1:1})有所帮助,但随着更多用户的加入,这种帮助逐渐减弱。在我看来,Mongoose确实不适合基于数组的更新。您可以在这里查看我们整个调查过程
解决方案
如果您可以从数组转换为对象,请这样做。例如:comments: Schema.Types.Array => comments: Schema.Types.Mixed,并按照post.comments.{ID}.date进行排序,或者根据需要甚至手动使用post.comments.{ID}.position
如果您被困在数组中:
  1. db.collection.ensureIndex({_id:1,__v:1})
  2. 使用上述描述的方法。您将无法受益于钩子和验证,但还有更糟糕的事情。

复制集如何帮助这里?它是为了高可用性。OP(和您)需要更快的处理或写入(或更多并行写入),这只能通过分片来实现。 - Asya Kamsky
你是对的。我没有意识到所有的写入操作都会发送到主服务器,副本只被用于读取操作。已经更新。 - lefnire
正如顶部评论所建议的那样,尽可能使用原子操作比使用对象更有意义。感谢您链接 Github 问题讨论,这是一篇非常有趣的阅读! - Chris Foster

1
我强烈建议将这些数组拆分到新的集合中。例如,一个评论集合,每个文档都有一个记录ID来表示它属于哪里。这是一个更具可扩展性的解决方案。
你是正确的,Mongoose的数组操作不是原子操作,因此不具有良好的可伸缩性。

0

我想到了另一个想法,虽然不确定但似乎值得提供:软删除。

Mongoose非常关注数组结构的更改,因为它们会使未来的更改变得模糊不清。但是,如果您只是使用comment.deleted=true标记评论子文档,则可能能够执行更多此类操作而不会遇到冲突。然后,您可以有一个cron任务,通过实际删除这些评论来进行操作。

哦,另一个想法是使用某种内存缓存,因此如果记录在过去几分钟内已被访问/编辑,则可以在不必从服务器中拉取它的情况下使用它,这意味着同时到达的两个请求将修改相同的对象。

注意:我实际上并不确定这两个想法是否总体上都是好主意或者它们是否能解决您的问题,所以如果它们不好,请随时进行编辑/评论/反对 :)


我认为这个问题可能会类似。我认为对文档进行任何更改都会导致版本号增加(无论是将字段设置为“true”还是删除数组元素),这将在尝试调用“update()”时被拒绝,但如果我错了,请纠正我。 - Chris Foster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接