mongodb批量删除数据效率问题

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

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

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

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

Date date1 = new Date();
System.out.println("开始统计要删除的时间 :" + date1);
DBCursor dbCursor = collection.find();
int count = 0;
while(dbCursor.hasNext()) {
    DBObject object = dbCursor.next();
    String id = object.get("_id").toString();
    String time16Str = id.substring(0, 8);
    long createTime = str16To10(time16Str);
    if(createTime < lastTime) {
        count++;
    }
}
Date date2 = new Date();
System.out.println("结束统计要删除的时间 :" + date2);
System.out.println("统计花费的时间 :" + (date2.getTime()-date1.getTime()) + "ms");
System.out.println("dbCursor count :" + dbCursor.count());
System.out.println("need del  count :" + count);

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

开始统计要删除的时间 :Tue May 26 18:38:07 CST 2020
结束统计要删除的时间 :Tue May 26 18:38:50 CST 2020
统计花费的时间 :43079ms
dbCursor count :21595385
need del  count :16398864

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

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

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

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

public Long clearUserRecommandOldData() {
    // 根据objectId清理两年之前的老数据
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.YEAR, -2);
    final long lastTime = calendar.getTime().getTime();
    final OpLogService service = this.opLogService;

    Long result = new MongoExecutor<Long>() {
        @Override
        protected Long doInMongo(MongoClient client) {
            ExecutorService executorService = Executors.newFixedThreadPool(2);

            DB database = client.getDB(ConfigManager.get(GeoMongodbConfig.class).getServer().getDbName());
            DBCollection collection = database.getCollection(COLLECTION_NAME);

            DBObject fields = new BasicDBObject();
            fields.put("_id" , true);
            // 游标每次从库里面一次拿出5w条数据,避免出现SocketTimeOut异常
            DBCursor dbCursor = collection.find(new BasicDBObject(), fields).batchSize(CloverConstants.DBCUSTOR_BATCH_SIZE);
            long count = 0;
            List<Object> objectIds = new ArrayList<>();

            Date date1 = new Date();
            logger.info("user_recommand开始统计, date:" + date1 + ", dbCursor count:" + dbCursor.count());

            while(dbCursor.hasNext()) {
                DBObject object = dbCursor.next();
                String id = object.get("_id").toString();
                String time16Str = id.substring(0, 8);  // 获取objectId中的16进制的创建时间
                long createTime = Long.valueOf(time16Str, 16) * 1000;
                if (createTime < lastTime) {
                    count++;
                    objectIds.add(object.get("_id"));

                    // 每次取出100条数据就另外开启一个线程来执行删除任务
                    if(count % CloverConstants.DBCUSTOR_BATCH_SIZE == 0) {
                        final List<Object> delObjectIds = new ArrayList<>(objectIds);
                        objectIds.clear();
                        threadPoolHandler(executorService, delObjectIds);
                    }
                }
            }
            dbCursor.close();
            Date date2 = new Date();
            logger.info("user_recommand结束统计, date:" + date2 + ", cost time:" + (date2.getTime()-date1.getTime()) + "ms");
            threadPoolHandler(executorService, objectIds);
            return count;
        }
    }.execute();
    if(result == null) {
        result = 0l;
    }
    return result;
}

private void threadPoolHandler(final ExecutorService executorService, final List<Object> objects) {
    new MongoExecutor<Long>() {
        @Override
        protected Long doInMongo(MongoClient client) {
            DB database = client.getDB(ConfigManager.get(GeoMongodbConfig.class).getServer().getDbName());
            final DBCollection collection = database.getCollection(COLLECTION_NAME);
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    Date date1 = new Date();
                    logger.info("user_recommand开始删除, date:" + date1 + ", count:" + objects.size());
                    for(Object objectId : objects) {
                        DBObject query = new BasicDBObject().append("_id", objectId);
                        // 这个删除方法效率是remove的至少10倍
                        collection.findAndModify(query, null, null, true, null, false, false);
                    }
                    Date date2 = new Date();
                    logger.info("user_recommand结束删除, date:" + date2 + ", cost time:" + (date2.getTime()-date1.getTime()) + "ms");
                }
            });
            return 0l;
        }
    }.execute();
}

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

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条,现在我做的删除具体代码如下:

public Long clearUserRecommandOldData(final boolean auto) {
    final TwoTuple<Long, Long> executeTime = opLogService.getExecuteTime(COLLECTION_NAME, auto);
    // 判断当前的任务是否在可执行的指定时间片中
    if (executeTime.getFirst() == 0l || executeTime.getSecond() == 0l) {
        return 0l;
    }
    long now = System.currentTimeMillis();
    if (now < executeTime.getFirst() || now > executeTime.getSecond()) {
        return 0l;
    }

    // 根据objectId清理两年之前的老数据
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.YEAR, -2);
    final long twoYearsAgo = calendar.getTime().getTime();
    final OpLogService service = this.opLogService;

    // 返回字段
    DBObject fields = new BasicDBObject();
    fields.put("_id", true);

    // 排序条件
    DBObject orderBy = new BasicDBObject();
    orderBy.put("_id", 1);  // 时间升序
    long count = 0;
    try {
        MongoClient client = MongoManager.getInstance().getClient();
        DB database = client.getDB(ConfigManager.get(GeoMongodbConfig.class).getServer().getDbName());
        DBCollection collection = database.getCollection(COLLECTION_NAME);

        // 游标每次从库里面一次拿出100条数据,避免出现SocketTimeOut异常,
        // 按_id升序排列,时间大于lastTime就立马返回,不用遍历所有
        DBCursor dbCursor = collection.find(new BasicDBObject(), fields).sort(orderBy).batchSize(CloverConstants.DBCUSTOR_BATCH_SIZE);
        Date date1 = new Date();
        logger.error("user_recommand开始统计, date:" + date1 + ", dbCursor count:" + dbCursor.count());

        List<Object> objectIds = new ArrayList<>();
        while (dbCursor.hasNext()) {
            DBObject object = dbCursor.next();
            String id = object.get("_id").toString();
            String time16Str = id.substring(0, 8);  // 获取objectId中的16进制的创建时间
            long createTime = Long.valueOf(time16Str, 16) * 1000;
            if (createTime > twoYearsAgo)
                break;

            count++;
            objectIds.add(object.get("_id"));

            // 每次取出100条数据就来执行删除任务
            if (count % CloverConstants.DBCUSTOR_BATCH_SIZE == 0) {
                final List<Object> delObjectIds = new ArrayList<>(objectIds);
                objectIds.clear();
                // 避免删除频繁,让出一些空闲时间给主程序调用mongodb
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    logger.error(COLLECTION_NAME + " clear sleep error" + e);
                }
                // 继续删除
                service.delOldData(collection, delObjectIds, COLLECTION_NAME, 0);
            }

            // 如果划分的时间片用完,就结束执行,保证别的日志也能被清理到
            if (System.currentTimeMillis() >= executeTime.getSecond()) {
                logger.error(COLLECTION_NAME + " time period over");
                break;
            }
        }
        dbCursor.close();
        Date date2 = new Date();
        logger.error("user_recommand结束统计, date:" + date2 + ", cost time:" + (date2.getTime() - date1.getTime()) + "ms, del count:" + count);
        service.delOldData(collection, objectIds, COLLECTION_NAME, 0);

        return count;
    } catch (Exception e) {
        logger.error(COLLECTION_NAME + "execute error:" + e.getMessage());
        count += clearUserRecommandOldData(auto);
    }
    return count;
}

public void delOldData(DBCollection collection, List<Object> objectIds, String colName, int startIndex) {
    if (objectIds.isEmpty())
        return;
    if (startIndex >= objectIds.size())
        return;

    //Date date1 = new Date();
    //logger.info(colName + "开始删除, date:" + date1 + ", count:" + objectIds.size());

    try {
        for (int i = startIndex; i < objectIds.size(); i++) {
            DBObject query = new BasicDBObject().append("_id", objectIds.get(startIndex));
            // 比remove删除效率高10+倍
            collection.findAndModify(query, null, null, true, null, false, false);
            startIndex++;
            //collection.remove(query);
        }
    } catch (MongoException exception) {
        logger.error("MongoException error, _id:" + objectIds.get(startIndex) + "error msg:" + exception.getMessage());
        startIndex++;
        delOldData(collection, objectIds, colName, startIndex);
    }
    //Date date2 = new Date();
    //logger.info(colName + "结束删除, date:" + date2 + ", cost time:" + (date2.getTime() - date1.getTime()) + "ms");

}

具体删除策略如下:

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

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

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

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

public TwoTuple<Long, Long> getExecuteTime(String colName, boolean auto) {
        long startTime = 0l;
        long endTime = 0l;
        Calendar c = Calendar.getInstance();

        int nowHour = c.get(Calendar.HOUR_OF_DAY);
        long now = c.getTimeInMillis();

        if (auto) {
            // 定时器触发 3-7点之间,3-5执行user_recommend, 5-6执行op_log, 6-7执行op_result
            int endHour = CloverConstants.CLEAR_END_HOUR;
            if (colName.equals(FriendRecService.COLLECTION_NAME)) {
                endHour = 5;
            } else if (colName.equals(COL_OP_LOG)) {
                endHour = 6;
            }

            startTime = now;
            c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH), endHour, 0, 0);
            endTime = c.getTimeInMillis();
        } else {
            // 手动触发,如果碰上定时器的执行时间段则不执行
            startTime = now;

            long endTime1 = 0l;
            if (nowHour < CloverConstants.CLEAR_START_HOUR) {
                c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH), CloverConstants.CLEAR_START_HOUR, 0, 0);
                endTime1 = c.getTimeInMillis() - 5 * HeConsts.MILLISECONDS_PER_MINUTE;
                if (startTime > endTime1) {
                    return Tuple.tuple(0l, 0l);
                }
            } else if (nowHour >= CloverConstants.CLEAR_END_HOUR) {
                c.add(Calendar.DAY_OF_MONTH, 1);
                c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH), CloverConstants.CLEAR_START_HOUR, 0, 0);
                endTime1 = c.getTimeInMillis() - 5 * HeConsts.MILLISECONDS_PER_MINUTE;
            } else {
                return Tuple.tuple(0l, 0l);
            }
            // 手动触发,每个表删除时间分配2小时,保证手动执行每个表也有时间去删到
            endTime = startTime + HeConsts.MILLISECONDS_PER_HOUR * 2;
            //endTime = startTime + 5*60*1000;  // for test
            if (endTime > endTime1) {
                endTime = endTime1;
            }
        }
        return Tuple.tuple(startTime, endTime);
    }
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱吃肉的鱼儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值