MongoDB批量快速插入100万数据

文章目录

      • 三、小试牛刀露锋芒
      • 一、黑云压城城欲摧
      • 二、山重水复疑无路
      • 四、回首向来萧瑟处,归去,也无风雨也无晴
      • 五、按下葫芦浮起瓢
      • 六、不计较一城一池之得失

三、小试牛刀露锋芒

现网GhsHis表有几百万数据,但是测试环境只有几万数据,想要模拟现网数据量进行测试。

叮嘱测试用js脚本往数据库插入,结果她还是调了接口进行插入。虽然测试环境MongoDB部署的还是分片集群,但是,还是把测试环境搞挂了。

关键时刻,还得开发上场。

有了测试同事的教训,为了研发环境的安全,我用js脚本先插入了一万条数据小试牛刀。结果呢,执行得很慢。考虑到还有主从复制,从
库查询压力也很大。

肯定不能一条一条插。那就批量插入。刚好MongoDB有支持批量插入的命令insertMany,于是试了一下,果真批量插入,速度不同凡
响,快的不是一星半点。

理论上可行,但实践起来还有很多细节要考虑。

要插入一百万数据,肯定不能一次性插入,我们一次插入一万,分一百次插入。写两层for循环轻松搞定。

插入的数据不能一模一样,比如创建时间和进入历史表的时间不能都一样,所以需要动态设置。

尝试插入了二十条数据,虽然动态设置了时间,但是发现最终所有数据都跟最后一条数据时间一毛一样。

我一个写Java的,为啥要让我写Js脚本?我感觉你这是在为难我胖虎。

钱难挣,屎难吃,工资也不是那么好拿的。于是,我又开始了面向百度编程。

很明显是由于对Js语法不了解导致的。百度了两行代码,试了一下可以。

var b = {};
Object.assign(b, a);
b.createDate = NumberLong(new Date().getTime() - Math.round(Math.random() * 10000) + 10000);

之前写的是

var b;
b.createDate = NumberLong(new Date().getTime() - Math.round(Math.random() * 10000) + 10000);

然后就是往数组里面插入数据,使用push方法即可。

统计一下执行脚本的耗时,java的sout使用的是加号拼接,Js使用的是逗号拼接。

var time = new Date().getTime();
print("执行耗时:",new Date().getTime()-time);

让时间具有随机性,调用Js数学类库函数。

NumberLong(new Date().getTime() - Math.round(Math.random() * 10000) + 10000);

最终脚本

var a = {"customerId": "123456789","username": "ghs@qq.com","pkgId": "66666666","state": "USE_END","price": 1.0,"createDate": NumberLong(1666341382443),"orgId": "963852741","deptId": "147258369","remark": "666","inHisTime": NumberLong(1666346588556)
};var time = new Date().getTime();
for (j = 1; j <= 100; j++) {var arr = [];for (i = 1; i <= 10000; i++) {var b = {};Object.assign(b, a);if (i % 3 == 0) {b.state = "USE_END";} else if (i % 3 == 1) {b.state = "EXPIRE";} else {b.state = "TRANSFER";}b.createDate = NumberLong(new Date().getTime() - Math.round(Math.random() * 10000) + 10000);b.inHisTime = NumberLong(b.createDate + Math.round(Math.random() * 10000) + 100);arr.push(b);}db.GhsHis.insertMany(arr);
}
print("执行耗时:",new Date().getTime()-time);

今天是程序员节,祝大家节日快乐!!!

大功告成???

不,这才是万里长征第一步。革命尚未成功,同志仍需努力!!!

数据是构造好了,有了跟现网差不多的数据量级,但是现网问题的复现、测试还没开始呢!!!

一、黑云压城城欲摧

故事背景:现网导出接口导出Excel数据出现了Id重复,几乎是必现。

测试环境不能复现,距离升级只有三天时间了。时间紧,任务重。

找业务人员要了当时导出的那份Excel,将Id列复制到D:\delete1.txt文件,准备用java代码分析一下。

代码思路,使用高速缓冲字符流一次读取一行,将读取到的Id放入List集合,然后遍历List集合,使用Set集合去重,拿到重复的Id以及下标。

public class IOReader {public static void main(String[] args) throws IOException {BufferedReader bis = null;List<String> list = new ArrayList<>();try {bis = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\delete1.txt")));String str = null;while ((str = bis.readLine()) != null){list.add(str);}} catch (FileNotFoundException e) {e.printStackTrace();} finally {bis.close();}System.out.println(list.size());Set<String> set = new HashSet<>();for(int i=0;i<list.size();i++){if(set.contains(list.get(i))){System.out.println("重复数据:"+list.get(i)+",下标"+i);}else{set.add(list.get(i));}}}
}

通过代码分析发现,确实存在重复Id。因为Id是唯一的,出现重复有悖常理。因为有现表和历史表,表结构大差不差。导出又可以同时导出历史表和现表。查看代码发现,在将现表数据移入历史表的时候,先插入的历史表,然后删除的现表,这样在极端情况下,是可能出现导出重复Id的。

事情会这么简单吗?很明显不会。

二、山重水复疑无路

随着代码的继续深入,我发现对于导出所有的情况,代码已经做了去重处理,并无明显Bug。

带着内心的疑问,我又找到了业务人员。仔细询问了他操作的细节。

操作流水号?操作Id?很明显,业务人员并不关注这些,只是丢给了我一个用户名。

我进入用户表一查,好家伙,一个用户名对应十几个用户。

我又问了他操作时所属的部门,这才将将初步定位了嫌疑人。

案发时间在昨天,还好时间不久,一切证据还未被抹除。

现网Kabana日志只展示七天,超过七天的就只能去磁盘看。需要申请一堆的权限,耗时又费力,大家一般都不愿意申请。

将时间定格在昨天,将嫌疑人锁死在刚才找到的Id。果真寻到了案发现场。

本着不放过一切蛛丝马迹的原则,我仔仔细细地查看了案发时的证据,但并未发现什么特别的有价值的证据。

导出Excel的过程是这样的,前台请求中台,中台请求后台。每次最多导出1000条,中台分次请求后台。

我看了一下每次导出的时间间隔竟然相差14秒,确实有点大。这算是案发现场唯一的收获了。

是中台设置的请求时间还是后台接口竟然如此慢?

我去测试环境试了一下,很快。那就是后台接口慢喽。

案件一下陷入了僵局,扑朔迷离的案情属实让人焦头乱额。

大脑飞速地转动着,思索着还有哪些未考虑到的场景。

我又去看了一下导出Excel,这算是直接证据了,要好好分析一下。

凭借着精湛的业务能力和三年多的工作经验,灵光一现,我机智地发现了出现重复的数据位于每一页开头的位置。

一个Idea浮现在了我的脑海。

我激动地翻找代码,想要佐证自己的想法。果真如我所想,初步定位到了问题所在。

知错,改错,验错。第一步总算是完成了。

改错也很简单,几秒钟搞定。知错几小时,改错几秒钟。

接力棒交接到了测试同事的手中,我总算可以松一口气了。

四、回首向来萧瑟处,归去,也无风雨也无晴

测试按我所说,未能成功复现问题。球呀,又到了我的手里。

作为全场最靓的仔,这点事肯定难不倒英明神武的我。

现网每一次操作时间间隔有14秒,有充足的时间来进行我们想要的操作。测试环境只有三秒,时间不等人,拼的是手速。

交代一下我定位出来的问题:我怀疑是排序字段选择不当造成导出Id重复。

啥?排序字段还能引起导出Id重复?我只是工作了三年的实习生,你别蒙我!

别急,听我细细道来。因为导出是既可以选择现表,也可以选择历史表,它们的排序规则都是一样的:创建时间逆序。问题就出在这里,现表使用创建时间没有问题,但是历史表就有问题了,创建时间早的不一定进入历史表的时间就早。

举个例子,导出的第二页,第1001条到第2000条数据。在正导出第二页的时候,有一个创建时间恰好位于第1001条到第2000条之间的数据被插入,那么根据创建时间逆序排序,原来第2000条数据就会被排到第2001条数据的位置,表现出来就是第2001条数据Id重复。

找出数据量在10000条以上的部门,然后选一个一万条左右的部门导出。

db.GhsHis.aggregate([{$group:{_id:"$deptId",total:{$sum:1}}},{$match:{total:{$gt:10000}}}],{allowDiskUse: true})

管道有100M内存限制。设置allowDiskUse:true,允许使用磁盘存储数据。

由于测试环境数据量用户量不足,一瞬间没有那么多数据失效然后进入历史表。如此苛刻的复现条件只能是手动来提供。

三秒的操作时间,理论上是来得及操作的。但是,可能不具有普适性。测试又复现失败了。

不知道测试同事心中此时作何感想。(叼毛,按你说的方法复现不了问题?)

为了维护我在同事心中的靓仔形象。啪,很快呀,我又写了一个脚本。

var a = {"customerId": "123456789","username": "ghs@qq.com","pkgId": "66666666","state": "USE_END","price": 1.0,"createDate": NumberLong(1666341382443),"orgId": "963852741","deptId": "147258369","remark": "666","inHisTime": NumberLong(1666346588556)
};
function sleep(number){var now = new Date();var exitTime = now.getTime() + number;while (true) {now = new Date();if(now.getTime() > exitTime){return;}}
}
var timeArr=[1641864542173,1641864263779,1637305936014,1637293032528,1636098374840,1624608384592,1621040814675,1617868030123,1617866906466];
timeArr.forEach(function(t){var b = {};Object.assign(b, a);b.createDate = NumberLong(t);sleep(200);db.GhsHis.insert(b);
});

开发呀,你不讲武德,你跟我说用手操作,你竟然偷偷写脚本。

还不是为了操作的普适性,为了问题在测试环境必现,你以为我想写脚本呀?

说起来云淡风轻,实际上斩棘披荆。

老规矩:先解释一下代码思路。找出了九个时间点,这九个时间点都是位于第二页的。然后遍历,每隔200毫秒,以该时间点为创建时间,插入一条数据,代表的是此时有一条创建时间位于第二页的数据被插入历史表,以此来模拟现网操作。

Java线程睡眠一行代码就够了,Js竟然还得自己写,当然都是百度的了。

那这九个时间点是怎么找出来的?

db.GhsHis.find({"deptId" : "147258369","state":{$in:["EXPIRE","USE_END","TRANSFER"]}}).sort({"createDate":-1}).skip(1100)

skip那里改成1200,一直到1900,找出9个创建时间。

我将脚本给了测试,让他在第一页导出之后,第二页正在导出的时候,立刻执行该脚本。

结果呢,翻车了,还是没有复现。

大意呀,我没有改进入历史表的时间。因为我改了排序规则,新规则使用进入历史表的时间和创建时间两个字段来逆向排序。

b.inHisTime= NumberLong(new Date().getTime());

又把脚本给了测试,问题成功复现!靓仔的形象得到了强有力的维护!自己的形象要靠自己来维护。

五、按下葫芦浮起瓢

然后,测试升级版本,开始验错,到了校验我改错的时候了。

结果又翻车了!!!每次都能复现问题,改了跟没改一样,我怕不是改了个毛线?

这下好了,靓仔彻底变叼毛了。

细细端详代码,思忖着究竟是哪里出了问题?原来是排序字段的排序方式有问题。

之前是按照创建时间逆向排序,我想都没想,就沿用了之前的方式,根据进入历史表的时间和创建时间两个字段来逆向排序。(当进入历史表时间相同,按照创建时间来逆向排序)

我怎能重蹈前任的覆辙呢?我跟他们又有什么区别?我改错又有什么意义呢?只是从一个坑爬到了另一个坑。

大意,还是大意呀。不仅丢了燕云十八州,连荆襄九郡都丢了。

痛定思痛,痛改前非。

还是时间太紧急了,搞得我很急躁,都不能冷静思考了,犯了如此低级的错误。总得给自己找个借口安慰一下英明神武明察秋毫的自己。

改成正向排序以后,问题果然解决了。问题不会复现了。Nice !

现网问题到这里已经解决了,不会再导出重复的Id了。

六、不计较一城一池之得失

bug已经解决,但是优化永无止境。查询导出1000条耗费14秒,太慢了。

查询慢,第一反应肯定是没加索引。查看现网GhsHis表的索引。

db.GhsHis.getIndexes()

发现表里索引挺多的,但是,排序字段createDate竟然没加索引!

分析一下查询导出语句的执行计划,“stage” : “SORT”,证明排序没有使用索引,在内存中做了排序,且做了全表扫描。

db.GhsHis.find({"deptId" : "147258369","state":{$in:["EXPIRE","USE_END","TRANSFER"]}})
.sort({"createDate":-1}).skip(1000).limit(1000).explain("executionStats")

鉴于已经修改了排序规则,所以我给进入历史表时间和创建时间建了联合索引。{background:true}这句一定要加,否则会锁表。

db.GhsHis.ensureIndex({inHisTime:1,createDate:1},{background:true})

state字段与deptId字段数据区分度不高,暂时不加索引。

db.GhsHis.find({"deptId" : "147258369","state":{$in:["EXPIRE","USE_END","TRANSFER"]}}).sort({"inHisTime":1,"createDate":1}).skip(1000).limit(1000).explain("executionStats")

再次查看执行计划,“stage” : “IXSCAN”,说明使用了索引,只扫描了几千条数据,查询时间也只有几十毫秒了。

MongoDB的执行计划比起Mysql而言更加复杂难懂,后续有时间再做深入研究学习,今日尚且浅尝辄止!

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注