文章目录

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

三、小试牛刀露锋芒

现网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而言更加复杂难懂,后续有时间再做深入研究学习,今日尚且浅尝辄止!

MongoDB三分钟插入100万数据相关推荐

  1. 一分钟内向数据库中批量插入100万数据

    罗列一下三种插入方式: 1.动态SQL拼接 批量插入一万条数据:4.7s @Testpublic void dynamicSql() {List<NotifyRecordEntity> n ...

  2. 4分钟插入1000万条数据到mysql数据库表

    准备工作 我用到的数据库为,mysql数据库8.0版本的,使用的InnoDB存储引 创建测试表 CREATE TABLE `product` (`id` int NOT NULL AUTO_INCRE ...

  3. 插入100万条有随机姓名,随机电话等字段的数据最快需要几秒?

    问题描述:往数据库的teachers表里面随机插入100万条带随机姓名,随机电话等字段的数据,需要多少秒? 接下来,我将从1.需要多少秒?2.为什么会这么快(包括PreparedStatement与S ...

  4. 绝对干货,教你4分钟插入1000万条数据到mysql数据库表,快快进来

    我用到的数据库为,mysql数据库5.7版本的 1.首先自己准备好数据库表 其实我在插入1000万条数据的时候遇到了一些问题,现在先来解决他们,一开始我插入100万条数据时候报错,控制台的信息如下: ...

  5. 力控数据写入mysQL_绝对干货,教你4分钟插入1000万条数据到mysql数据库表,快快进来...

    我用到的数据库为,mysql数据库5.7版本的 1.首先自己准备好数据库表 其实我在插入1000万条数据的时候遇到了一些问题,现在先来解决他们,一开始我插入100万条数据时候报错,控制台的信息如下: ...

  6. 批量往mysql里面插入1000万条数据_绝对干货,教你4分钟插入1000万条数据到mysql数据库表,快快进来...

    我用到的数据库为,mysql数据库5.7版本的 1.首先自己准备好数据库表 其实我在插入1000万条数据的时候遇到了一些问题,现在先来解决他们,一开始我插入100万条数据时候报错,控制台的信息如下: ...

  7. MySQL索引学习(100万数据做对比)

    创建表 create table account (id int auto_incrementprimary key,name varchar(11) not null,money decimal(9 ...

  8. mysql命令行批量添加数据_mysql命令行批量插入100条数据命令

    先介绍一个关键字的使用: delimiter 定好结束符为"$$",(定义的时候需要加上一个空格) 然后最后又定义为";", MYSQL的默认结束符为" ...

  9. 为何插入10万数据只需2秒

    文章目录 一.前言 二.问题 1.url后面useServerPrepStmts是什么? 2.url后面rewriteBatchedStatements是什么? 3.这两个参数对语句执行有什么影响? ...

  10. mysql db 100万行 大小_插入100万行数据

    #创建库和表 CREATE DATABASE db1; USE db1; CREATE TABLE t1(id INT,nam VARCHAR(10),age int); #定义一个存储过程,插入数据 ...

最新文章

  1. 解决linux 系统中Mysql 进程占用CPU 300%故障
  2. redis 未授权访问利用 两种方式
  3. [每天进步一点 -- 流水账]第3周
  4. UVA10870递推关系(矩阵乘法)
  5. 反素数(luogu 1463)
  6. 微信重大更新!这特么是为上班摸鱼开发的吧.....(附内测地址)
  7. 一张图带你了解 Insider Dev Tour 2019中国技术大会
  8. Mac下关于-您不能拷贝项目“”,因为它的名称太长或包括的字符在目的宗卷上无效。-的删除...
  9. bzoj3203: [Sdoi2013]保护出题人
  10. vs2010中moc文件生成方法
  11. Frontpage网页制作软件,你们还记得吗?
  12. php post undefined index,PHP 中提示undefined index如何解决(多种方法)
  13. linux 标准函数注释,Linux 驱动程序中相关函数注释汇总(跟新中)
  14. 那桃林深处最美的风景
  15. 前端单页路由《stateman》源码解析
  16. 利用HTML+JS+CSS实现简单的网页计算器,附html所有源代码,可直接黏贴运行
  17. 使用 matlab 数字图像处理(七)—— 频率域处理
  18. 如何通过http协议知道是从那个站点过来的请求_聊聊HTTP的那些事,以及在它背后的“勾当”...
  19. 关于没有找到MFC80UD.DLL,因此这个程序未能启动.重新安装应用程序可能会修复此问题的解决方案
  20. Ubuntu串口驱动安装及串口权限设置

热门文章

  1. 百度2017春招笔试真题编程题集合 [编程题]买帽子
  2. 高德地图E/libEGL: call to OpenGL ES API with no current context (logged once per thread)
  3. Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.错误解决
  4. php like 中文,punycode和中文转换 phpThe Twitter-clone/twitter-like sites collection
  5. Unity3D的3D音效的实现
  6. 浙江大学黄杨思博计算机学院,竺可桢学院2010-2011学年荣誉称号发文名单
  7. PHP - 收藏集 - 掘金
  8. IE提示当前安全设置不允许下载该文件怎么办?
  9. 如何设置无需fn直接按F1~F10(HP Pavilion Notebook )
  10. linux 下安装redis并设置开机自启动