当前位置:   article > 正文

mongodb批量删除数据效率问题_mongo大表删除数据慢

mongo大表删除数据慢

今天接到一个任务,线上的mongodb积累了大量的无用数据,导致宕机,现在对里面的数据进行批量删除。

其中库里面的一个log记录有2000w+条,他的存储字段比较少,格式如下:

{ "_id" : ObjectId("5ecb648b17bee8673ef09024"), "level" : 1, "pay" : 0, "rand" : 64090, "uid" : NumberLong(120196967) }

我们对这个表里面的删除记录就是将1年以上的数据进行清楚,根据ObjectId这个字段的生成规则,用如下的方法判断:

  1. Date date1 = new Date();
  2. System.out.println("开始统计要删除的时间 :" + date1);
  3. DBCursor dbCursor = collection.find();
  4. int count = 0;
  5. while(dbCursor.hasNext()) {
  6. DBObject object = dbCursor.next();
  7. String id = object.get("_id").toString();
  8. String time16Str = id.substring(0, 8);
  9. long createTime = str16To10(time16Str);
  10. if(createTime < lastTime) {
  11. count++;
  12. }
  13. }
  14. Date date2 = new Date();
  15. System.out.println("结束统计要删除的时间 :" + date2);
  16. System.out.println("统计花费的时间 :" + (date2.getTime()-date1.getTime()) + "ms");
  17. System.out.println("dbCursor count :" + dbCursor.count());
  18. System.out.println("need del count :" + count);

统计运行的时间如下,从下图中可以看出2000+的统计时间大约1分钟不到,这个还是还是可以接收的:

  1. 开始统计要删除的时间 :Tue May 26 18:38:07 CST 2020
  2. 结束统计要删除的时间 :Tue May 26 18:38:50 CST 2020
  3. 统计花费的时间 :43079ms
  4. dbCursor count21595385
  5. need del count16398864

然后在if中添加删除的两行代码:

  1. if(createTime < lastTime) {
  2. DBObject query = new BasicDBObject().append("_id", new ObjectId(id));
  3. WriteResult result = collection.remove(query); //1
  4. // collection.findAndModify(query, null, null, true, null, false, false); //2
  5. }

上面两行代码的执行删除的时间截然不同,remove5分59s删除了36956,而findAndModify5分3s删除了127090,如果是要删除16398864条数据的话,remove大约需要44h,而findAndModify需要10h。

针对这种批量删除,设计的方案如下:

  1. public Long clearUserRecommandOldData() {
  2. // 根据objectId清理两年之前的老数据
  3. Calendar calendar = Calendar.getInstance();
  4. calendar.add(Calendar.YEAR, -2);
  5. final long lastTime = calendar.getTime().getTime();
  6. final OpLogService service = this.opLogService;
  7. Long result = new MongoExecutor<Long>() {
  8. @Override
  9. protected Long doInMongo(MongoClient client) {
  10. ExecutorService executorService = Executors.newFixedThreadPool(2);
  11. DB database = client.getDB(ConfigManager.get(GeoMongodbConfig.class).getServer().getDbName());
  12. DBCollection collection = database.getCollection(COLLECTION_NAME);
  13. DBObject fields = new BasicDBObject();
  14. fields.put("_id" , true);
  15. // 游标每次从库里面一次拿出5w条数据,避免出现SocketTimeOut异常
  16. DBCursor dbCursor = collection.find(new BasicDBObject(), fields).batchSize(CloverConstants.DBCUSTOR_BATCH_SIZE);
  17. long count = 0;
  18. List<Object> objectIds = new ArrayList<>();
  19. Date date1 = new Date();
  20. logger.info("user_recommand开始统计, date:" + date1 + ", dbCursor count:" + dbCursor.count());
  21. while(dbCursor.hasNext()) {
  22. DBObject object = dbCursor.next();
  23. String id = object.get("_id").toString();
  24. String time16Str = id.substring(0, 8); // 获取objectId中的16进制的创建时间
  25. long createTime = Long.valueOf(time16Str, 16) * 1000;
  26. if (createTime < lastTime) {
  27. count++;
  28. objectIds.add(object.get("_id"));
  29. // 每次取出100条数据就另外开启一个线程来执行删除任务
  30. if(count % CloverConstants.DBCUSTOR_BATCH_SIZE == 0) {
  31. final List<Object> delObjectIds = new ArrayList<>(objectIds);
  32. objectIds.clear();
  33. threadPoolHandler(executorService, delObjectIds);
  34. }
  35. }
  36. }
  37. dbCursor.close();
  38. Date date2 = new Date();
  39. logger.info("user_recommand结束统计, date:" + date2 + ", cost time:" + (date2.getTime()-date1.getTime()) + "ms");
  40. threadPoolHandler(executorService, objectIds);
  41. return count;
  42. }
  43. }.execute();
  44. if(result == null) {
  45. result = 0l;
  46. }
  47. return result;
  48. }
  49. private void threadPoolHandler(final ExecutorService executorService, final List<Object> objects) {
  50. new MongoExecutor<Long>() {
  51. @Override
  52. protected Long doInMongo(MongoClient client) {
  53. DB database = client.getDB(ConfigManager.get(GeoMongodbConfig.class).getServer().getDbName());
  54. final DBCollection collection = database.getCollection(COLLECTION_NAME);
  55. executorService.execute(new Runnable() {
  56. @Override
  57. public void run() {
  58. Date date1 = new Date();
  59. logger.info("user_recommand开始删除, date:" + date1 + ", count:" + objects.size());
  60. for(Object objectId : objects) {
  61. DBObject query = new BasicDBObject().append("_id", objectId);
  62. // 这个删除方法效率是remove的至少10
  63. collection.findAndModify(query, null, null, true, null, false, false);
  64. }
  65. Date date2 = new Date();
  66. logger.info("user_recommand结束删除, date:" + date2 + ", cost time:" + (date2.getTime()-date1.getTime()) + "ms");
  67. }
  68. });
  69. return 0l;
  70. }
  71. }.execute();
  72. }

这个方案的重要一点这这一行代码:

DBCursor dbCursor = collection.find(newBasicDBObject(), fields).batchSize(CloverConstants.DBCUSTOR_BATCH_SIZE);

find()中的两个参数,第一个表示查询条件,我这里是什么查询条件也没有,第二个表示查询返回的字段,这里一定要写,如果你只需要几个查询字段,一定要带上这个,上面的一条log记录它的json结构很小,如果你的json结构很大的话,每次查询返回一个很大的DBObject,会造成系统频繁的gc。

batchSize()中是这个数字我写的是100,表示游标每次从数据库一次拿100条数据到内存,这个值,不能设置的太大,也不要设置太小。

batchSize设置太大,会出现SocketTimeOut超时问题,设置太小,游标每次从数据库拿一条效率也很低。

现在的mongodb启动时设置的SocketTimeOut时间时5s,batchSize时100,这个也是测试设置了好几种才决定的。

代码里面还有一个很重要的点,就是下面这行:

// 每次取出100条数据就另外开启一个线程来执行删除任务
if(count % CloverConstants.DBCUSTOR_BATCH_SIZE == 0) {
final List<Object> delObjectIds = new ArrayList<>(objectIds);
objectIds.clear();
threadPoolHandler(executorService, delObjectIds);
}

每收集100条可删除的数据,然后就放到线程池里面去执行,线程池初始化设置了2个,

当前条件改成删除两年前的老数据,库里面一共时2100+的数据,删除数据有500+万条数据,耗时15min左右就删干净了,看了一下数据库里面增加的不是很频繁,在系统里面添加了一个定时器,每两个星期清理一次Mongodb中的数据。

补充:

上面这个方案弊端是一边遍历一边删除,dbCustor的游标一直在不停的变化,很可能在删除的时候会出现超时的异常,具体如下图:

上面这个就是某些数据删除过慢,在日志中打印出来的,之前用remove()做删除这个时间消耗的有2s-20s左右的都有。但是如果删除不是那么频繁的话就不会出现这个问题。

 

我们项目中需求是定时清理过期的数据,这张表现在有2100w+的记录,删除当前时间两年前的数据大约有500w+条,线上一个星期大约增长了10w+条数据,之前在本次测试23分钟大约删除了18w条,现在我做的删除具体代码如下:

  1. public Long clearUserRecommandOldData(final boolean auto) {
  2. final TwoTuple<Long, Long> executeTime = opLogService.getExecuteTime(COLLECTION_NAME, auto);
  3. // 判断当前的任务是否在可执行的指定时间片中
  4. if (executeTime.getFirst() == 0l || executeTime.getSecond() == 0l) {
  5. return 0l;
  6. }
  7. long now = System.currentTimeMillis();
  8. if (now < executeTime.getFirst() || now > executeTime.getSecond()) {
  9. return 0l;
  10. }
  11. // 根据objectId清理两年之前的老数据
  12. Calendar calendar = Calendar.getInstance();
  13. calendar.add(Calendar.YEAR, -2);
  14. final long twoYearsAgo = calendar.getTime().getTime();
  15. final OpLogService service = this.opLogService;
  16. // 返回字段
  17. DBObject fields = new BasicDBObject();
  18. fields.put("_id", true);
  19. // 排序条件
  20. DBObject orderBy = new BasicDBObject();
  21. orderBy.put("_id", 1); // 时间升序
  22. long count = 0;
  23. try {
  24. MongoClient client = MongoManager.getInstance().getClient();
  25. DB database = client.getDB(ConfigManager.get(GeoMongodbConfig.class).getServer().getDbName());
  26. DBCollection collection = database.getCollection(COLLECTION_NAME);
  27. // 游标每次从库里面一次拿出100条数据,避免出现SocketTimeOut异常,
  28. // 按_id升序排列,时间大于lastTime就立马返回,不用遍历所有
  29. DBCursor dbCursor = collection.find(new BasicDBObject(), fields).sort(orderBy).batchSize(CloverConstants.DBCUSTOR_BATCH_SIZE);
  30. Date date1 = new Date();
  31. logger.error("user_recommand开始统计, date:" + date1 + ", dbCursor count:" + dbCursor.count());
  32. List<Object> objectIds = new ArrayList<>();
  33. while (dbCursor.hasNext()) {
  34. DBObject object = dbCursor.next();
  35. String id = object.get("_id").toString();
  36. String time16Str = id.substring(0, 8); // 获取objectId中的16进制的创建时间
  37. long createTime = Long.valueOf(time16Str, 16) * 1000;
  38. if (createTime > twoYearsAgo)
  39. break;
  40. count++;
  41. objectIds.add(object.get("_id"));
  42. // 每次取出100条数据就来执行删除任务
  43. if (count % CloverConstants.DBCUSTOR_BATCH_SIZE == 0) {
  44. final List<Object> delObjectIds = new ArrayList<>(objectIds);
  45. objectIds.clear();
  46. // 避免删除频繁,让出一些空闲时间给主程序调用mongodb
  47. try {
  48. Thread.sleep(500);
  49. } catch (Exception e) {
  50. logger.error(COLLECTION_NAME + " clear sleep error" + e);
  51. }
  52. // 继续删除
  53. service.delOldData(collection, delObjectIds, COLLECTION_NAME, 0);
  54. }
  55. // 如果划分的时间片用完,就结束执行,保证别的日志也能被清理到
  56. if (System.currentTimeMillis() >= executeTime.getSecond()) {
  57. logger.error(COLLECTION_NAME + " time period over");
  58. break;
  59. }
  60. }
  61. dbCursor.close();
  62. Date date2 = new Date();
  63. logger.error("user_recommand结束统计, date:" + date2 + ", cost time:" + (date2.getTime() - date1.getTime()) + "ms, del count:" + count);
  64. service.delOldData(collection, objectIds, COLLECTION_NAME, 0);
  65. return count;
  66. } catch (Exception e) {
  67. logger.error(COLLECTION_NAME + "execute error:" + e.getMessage());
  68. count += clearUserRecommandOldData(auto);
  69. }
  70. return count;
  71. }
  72. public void delOldData(DBCollection collection, List<Object> objectIds, String colName, int startIndex) {
  73. if (objectIds.isEmpty())
  74. return;
  75. if (startIndex >= objectIds.size())
  76. return;
  77. //Date date1 = new Date();
  78. //logger.info(colName + "开始删除, date:" + date1 + ", count:" + objectIds.size());
  79. try {
  80. for (int i = startIndex; i < objectIds.size(); i++) {
  81. DBObject query = new BasicDBObject().append("_id", objectIds.get(startIndex));
  82. // 比remove删除效率高10+
  83. collection.findAndModify(query, null, null, true, null, false, false);
  84. startIndex++;
  85. //collection.remove(query);
  86. }
  87. } catch (MongoException exception) {
  88. logger.error("MongoException error, _id:" + objectIds.get(startIndex) + "error msg:" + exception.getMessage());
  89. startIndex++;
  90. delOldData(collection, objectIds, colName, startIndex);
  91. }
  92. //Date date2 = new Date();
  93. //logger.info(colName + "结束删除, date:" + date2 + ", cost time:" + (date2.getTime() - date1.getTime()) + "ms");
  94. }

具体删除策略如下:

线上的connections连接数配置的是5,所以采用的单线程删除策略;

开启一个定时器,每天3点-7点执行,每张表规定一个时间段删除,保证有限的时间内每张表都能够有时间删到;

保证你的时间段删除的数量比它每天的增长量要多,不然你怎么都删不完就尴尬了;

如果你的删除数据任务不是要求立马删完,可以删除一些数据之后,让线程休眠一段时间再继续删除,不然频繁删除不仅会一直占用着连接,线上写数据可能会有影响,而且会报SocketTimeOut异常;

  1. public TwoTuple<Long, Long> getExecuteTime(String colName, boolean auto) {
  2. long startTime = 0l;
  3. long endTime = 0l;
  4. Calendar c = Calendar.getInstance();
  5. int nowHour = c.get(Calendar.HOUR_OF_DAY);
  6. long now = c.getTimeInMillis();
  7. if (auto) {
  8. // 定时器触发 3-7点之间,3-5执行user_recommend, 5-6执行op_log, 6-7执行op_result
  9. int endHour = CloverConstants.CLEAR_END_HOUR;
  10. if (colName.equals(FriendRecService.COLLECTION_NAME)) {
  11. endHour = 5;
  12. } else if (colName.equals(COL_OP_LOG)) {
  13. endHour = 6;
  14. }
  15. startTime = now;
  16. c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH), endHour, 0, 0);
  17. endTime = c.getTimeInMillis();
  18. } else {
  19. // 手动触发,如果碰上定时器的执行时间段则不执行
  20. startTime = now;
  21. long endTime1 = 0l;
  22. if (nowHour < CloverConstants.CLEAR_START_HOUR) {
  23. c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH), CloverConstants.CLEAR_START_HOUR, 0, 0);
  24. endTime1 = c.getTimeInMillis() - 5 * HeConsts.MILLISECONDS_PER_MINUTE;
  25. if (startTime > endTime1) {
  26. return Tuple.tuple(0l, 0l);
  27. }
  28. } else if (nowHour >= CloverConstants.CLEAR_END_HOUR) {
  29. c.add(Calendar.DAY_OF_MONTH, 1);
  30. c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH), CloverConstants.CLEAR_START_HOUR, 0, 0);
  31. endTime1 = c.getTimeInMillis() - 5 * HeConsts.MILLISECONDS_PER_MINUTE;
  32. } else {
  33. return Tuple.tuple(0l, 0l);
  34. }
  35. // 手动触发,每个表删除时间分配2小时,保证手动执行每个表也有时间去删到
  36. endTime = startTime + HeConsts.MILLISECONDS_PER_HOUR * 2;
  37. //endTime = startTime + 5*60*1000; // for test
  38. if (endTime > endTime1) {
  39. endTime = endTime1;
  40. }
  41. }
  42. return Tuple.tuple(startTime, endTime);
  43. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/878395
推荐阅读
相关标签
  

闽ICP备14008679号