mongodb大量数据删除效率问题

有两个集合
data 存储数据,
attach 存储数据的附件
data和attach,一对多关系。
data和attach的数量比大概1:10左右,也就是1条data数据,下面可能有10-50条数据。
data几千万的时候,attach可能就是几亿了。

现在的问题是,假如用户删除了data,那么attach也必须相应的删除。
对几亿条数据进行操作,这个过程是很漫长的。数据库压力也很大。
所以考虑是不是可以软删除,先把data更新状态为删除,attach先不管。然后后期用程序一点一点的在后台清除。
毕竟更新个几千万数据,等待时间还是不算长的。
但有个问题,attach是需要做数据统计的。比如用户删除之前,计算出他的附件占用空间20G,删除之后,你要给他删除之后的附件占用量,否则计费就不准确。

所以这个问题如何解决呢?

阅读 11.4k
2 个回答

这是个很常见的问题,在大数据场景中很多时候确实是用软删除代替删除,所以方案本身没有问题。第一点是必须做的。如果你对GridFS有了解,其实在GridFS的fs.files中的记录就相当于你的data,而fs.chunks则相当于你的attach。在fs.files中就存有文件大小,文件名,路径等等一系列元数据。
另外既然你已经标记了这个文件删除,那么在统计文件大小的时候可以很简单地根据删除标记过滤一下就好了。例如通过isDeleted=true标记了删除,则你需要的查询是:

db.foo.aggregate([
    {$match: {userId: "<userId>", isDeleted: false}},
    {$group: {_id: "$userId", size: "$size"}}    // 假设size中存放的是文件大小
]);

为了让这个查询更快,可以从以下几个方面做优化:

合理的索引

如果对数据库比较熟悉应该都能看出来这里最合适的索引是:

db.foo.createIndex({userId: 1, isDeleted: 1})

进一步的优化

实际上因为你关心的是“没有被删除的文件”,而不关心“已经被删除的文件”,所以进一步的优化可以考虑只对没有删除的文件做索引,即MongoDB中的部分索引

db.foo.createIndex({
    userId: 1,
    isDeleted: 1
}, {
    partialFilterExpression: {
        isDeleted: false
    }
});

再进一步的优化

如果这个查询的性能非常重要且频繁,那么更进一步的优化可以考虑查询覆盖(Covered Query)。在其他数据库中应该也有类似的概念,即通过索引查询后直接从索引中获得数据,而不用查询数据页。这在大规模统计的时候往往会有帮助。其代价则是消耗更大的内存空间。或者说你必须有更大的内存才有可能从中获益。这时应该建立的索引和相应的查询应该是:

db.foo.createIndex({
    userId: 1,
    isDeleted: 1,
    size: 1
}, {
    partialFilterExpression: {
        isDeleted: false
    }
});
db.foo.aggregate([
    {$match: {userId: "<userId>", isDeleted: false}},
    {$project: {_id: 0, userId: 1, size: 1}},
    {$group: {_id: "$userId", size: "$size"}}
]);

EDIT

关于删除的问题,删除确实是一个很重的操作,每条删除的数据都会导致全部相关的索引做变更,因此速度不会太快。你也已经提到了“软删除”,然后再由另外的线程来完成工作,这本质上是一种异步删除操作,思路没有问题,不过可以更简单一些,即在删除的时候同时设置一个过期时间,利用MongoDB的TTL Index帮助你自动完成删除操作。从节省空间的角度,这个过期时间的索引也只针对删除的数据有效,所以也是部分索引:

db.foo.createIndex({
    expire: 1
}, {
    partialFilterExpression: {
        isDeleted: true
    },
    expireAfterSeconds: 0
})

在操作删除时,只需要:

db.foo.update({...}, {$set: {isDeleted: true, expire: new Date()}});

这样在删除时过期时间被设置为现在(马上过期),那么下一次TTL Index启动时(一分钟一次)就会帮助你删除这条数据。同样的操作也需要在attah上面完成。

我现在能想到的方案,把每一条data所对应的attach大小,计算好,存储到data里面。毕竟这个大小是固定不变的。
所以附件大小,都是从data里面统计的,而且从几千万条数据里面统计,比从几亿条数据里面统计实在是快多了,而且无论attach是否删除,都不影响统计结果。


我还想到另外一个方案。data里面肯定通过uid来识别是哪个用户的数据,那么用户每次清空操作,我都生成一个新的uid。那么这个用户的数据和他就彻底脱离关系了。当然这个uid不是主键,只是每次随机生成的一个临时的身份识别id,
然后我记录每次的删除记录就行了。每次删除记录下旧的uid。然后后台通过这个旧的uid对他的数据进行清理。
通过这种方式,用户每次删除操作,都是快速响应,不需要等待的。不过,这样一来,整个数据库关系都需要重新梳理一番,的确多了很多工作

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进