logo头像

Always believe youself.

MongoDB大量数据清理方法

方法

TTL Index

TTL index是一种带有过期时间属性的索引,在时间类型的字段上(确定的值或者时间数组),数据插入的时候会根据索引字段的值确定数据保留时间,当超时后,mongodb后台线程会自动清理过期的文档,默认每60秒线程启动一次。

TTL index有以下特点:

  • 单列索引
  • 自动计算过期时间
  • 索引字段是数组,会根据数组中最早的时间加保留时长计算过期时间
  • 索引字段不包含时间类型数据不会自动删除,不包含索引字段的文档也不会自动删除
  • 保留时长修改需要使用collMod
  • 不能在已有单列索引的字段上创建TTL index

创建索引

db.eventlog.createIndex( { “lastModifiedDate”: 1 }, { expireAfterSeconds: 3600 } )

修改时间

修改索引的时间为10s

db.runCommand({collMod: “mycoll”,index: {keyPattern: {“borthday”: 1},expireAfterSeconds: 10}})

capped collection

capped collection 是一种固定大小的集合,数据按照插入顺序存储,数据插入时,自动覆盖最旧的数据。类似滚动日志的写入方式。

capped collection 具有以下特点:

  • 固定大小,自动删除数据
  • 默认只有_id字段的索引,插入速度快
  • update 操作不能修改文档的大小
  • 可以通过索引加快查询速度
  • 不能使用remove/delete*操作

创建capped collection

db.createCollection( "log", { capped: true, size: 100000 } )

指定size又指定集合数

db.createCollection("log", { capped : true, size : 5242880, max : 5000 } )

drop

针对不再使用的集合,可以使用drop直接删除,该操作会删除集合、数据和索引。

该操作会在操作的数据库上获取一个block lock,操作期间会阻塞其他操作。
导出数据–rename 集合–导入数据–drop原来的集合,通过此方式清理数据可以达到回收空间的目的。

remove

remove 操纵时mongo shell 的操作命令,使用的时delete 命令操作数据。
remove有两种操作方式

db.collection.remove(
   <query>,
   <justOne>
)
或
db.collection.remove(
   <query>,
   {
     justOne: <boolean>,
     writeConcern: <document>,
     collation: <document>
   }
)

删除所有数据
db.bios.remove( { } )
根据查询删除数据
db.products.remove( { qty: { $gt: 20 } } ) //删除所有符合条件的数据
db.products.remove( { qty: { $gt: 20 } }, true ) // 只删除一条符合条件的数据

待条件删除
db.products.remove(
    { qty: { $gt: 20 } },
    { writeConcern: { w: "majority", wtimeout: 5000 } }
)

3.4 版之后,还可以根据键盘规则和重音删除数据(不常用)
db.myColl.remove(
   { category: "cafe", status: "A" },
   { collation: { locale: "fr", strength: 1 } }
)

deleteOne/deleteMany

两个mongo shell 方法,删除一条或者多条数据,是remove 的底层函数。
可以指定条件删除数据,mongo 4.4 之后的版本可以指定hint,通过索引加快数据删除。

以下以deleteMany为例,deleteOne类似

语法:

db.collection.deleteMany(
   <filter>,
   {
      writeConcern: <document>,
      collation: <document>
   }
)

根据_id 删除过期数据

集合的_id字段是默认自动生成,类型时objectid。在数据插入时,如果不指定_id的值,该字段默认会按照时间先后顺序生成的字符串插入。默认的格式为:

4 字节 timestamp
3 字节 机器id
2 字节 执行插入的process id
3 字节的自增数字

因此,可以通过按照执行时间生成的objectid 删除数据。

function timeToObjId( time ) {
    var t = new Date(time);
    t = t.getTime()/1000; // 转换成秒数
    return t.toString(16)+'0000000000000000'; // 转换成16进制的字符串,再加补齐16个0
}

var objIdTimeTo = timeToObjId( '2020-08-25 10:42:01' );

// 查询MongoDB数据库

'''
  var objid=db.test.find({
  "\_id": {
          '$lt': ObjectId( objIdTimeto )
      },{"\_id" :1}).limit(5000);
  while (objid.hasNext()){
    var obj=objid.next().\_id;
    db.test.remove({"\_id": obj});
  }
'''

大量数据删除

在清理大量数据时,很容易造成实例性能下降或阻塞操作等问题,接下来,根据不同的场景讨论一下如何合理的清理大量数据。

只写少读或者不读的集合

这种集合一般是日志,状态等信息,一般情况下不会读取,之后在少数情况下使用,但是平时场景中,此类型数据量巨大
这种情况下,一般可以考虑使用capped 或者ttl ,对于集合中已有的数据,可以考虑rename,然后等待旧的表中的所有数据过期后,drop集合。

读写频繁的集合

这种集合是日常场景中最常见的,基本上要保持集合实时在线可访问,但该类型又可分为以下两种情况:

  • 有时间字段
    如果有时间字段,且业务要求按照时间字段清理数据,可以考虑清理一次后,创建ttl索引。

  • 无时间字段
    由于集合中没有时间字段,无法通过业务字段按照时间维度清理,此时分两种情况:

  • _id 自动生成
    参考根据_id 删除过期数据

  • _id 由程序写入
    此情况需要研发确定规则清理数据。

分批删除

在根据时间字段和_id进行删除时,需要分批进行,例如:

  • 时间字段是递增的,可以使用循环,进行分批删除

    for (var i = 1358758292145; i <= 1558758292170; i=i+158758292145){
    db.attribute_all.deleteMany( {“timestamp”: {$gte:NumberLong(“1358758292145”),$lt: NumberLong(i)} } )
    }

  • 回收空间

上面列举了不同的数据清理方法,其中TTL index 和 capped collection的方式维护成本较低,capped 是固定大小集合,不会存在收缩数据文件的情况;TTL index根据时间字段删除数据,只要数据插入时存在时间字段,数据文件的增长依然可控。同时两种方案一般不会出现短时间内大量删除数据造成性能低下的问题。

!注意: 如果在较大的集合上创建TTL index ,需要注意过期数据的大小,创建集合后,已过期的数据会被自动清理。

使用remove 和 delete类方法清理数据后,数据文件不会自动收缩,需要额外的处理来回收空间。

常用的空间回收操作有以下几种:

  • compact
  • db.repairDatabase()
  • db.copyDatabase()
  • secondary 同步
  • rename+export/import

compact

该操作会重写集合中的数据和集合上的索引数据,减少碎片,回收空间。

rs0:PRIMARY> db.test.find({ \_id:{ $lt : ObjectId("5f47766051a43a0011aa19e9")} }).count()
1061370 # 符合删除条件的文档数
rs0:PRIMARY> db.test.count()
1125827 #总的文档数

回收前,数据文件的大小

"file size in bytes" : 66174976,

-rw-r--r--. 1 root root  64M Aug 27 17:09 collection-7--5205033096240673155.wt

数据清理后,数据占用的大小为”size” : 23913583,

使用compact,回收空间:

db.runCommand({compact: "test",force:true})

回收后文件大小-rw-r–r–. 1 root root 3.5M Aug 27 17:16 collection-7–5205033096240673155.wt

特点:

  • 会在操作的数据库上加锁 ,阻塞其他操作
  • 集合较大时,执行时间长

db.repairDatabase()

repair 操作主要作用是修复逻辑损坏的数据,一般在数据库异常终止之后使用,该操作会删除损坏或不可用的数据,并重建索引。该操作直接重新整理数据,所以可以达到释放空间的效果。

使用方法:

#启动mongo 的时候使用
mongod --repair

#或使用命令
db.runCommand( { repairDatabase: 1 } )

特点:

  • 执行时获取全局锁,阻塞其他操作
  • 空间需求,集合大小+2G
  • 丢弃损坏的数据,重建索引

db.copyDatabase()

拷贝数据库,可以从一个实例拷贝指定的数据库到另外一个实例,同实例可以进行拷贝。

同实例拷贝:

db.copyDatabase('records', 'archive_records')

特点:

  • 拷贝期间不锁定目标实例,可以进行其他操作
  • 重建索引使用foreground形式创建,会阻塞所有其他操作
  • 拷贝过程中,会出现数据不一致

secondary 同步

操作步骤,参考副本重新同步

特点:

  • 需要有副本集架构
  • 不阻塞实例,但是重新同步的过程中,需要注意IO

rename+export/import

首先,rename原集合

use admin
db.runCommand({renameCollection: "test.test",to :"test.test_bak"})

然后,导出rename之后的数据

mongoexport -d test -c test_bak -o test_bak.json

导入到新集合中

mongoimport -d test -c test --file=test_bak.json

最后,删除备份的集合

db.test_bak.drop()

!注意
该操作只适合并发量较小,且允许短时间内无法读取旧数据的场景。