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而言更加復雜難懂,后續有時間再做深入研究學習,今日尚且淺嘗輒止!
總結
以上是生活随笔為你收集整理的MongoDB批量快速插入100万数据的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 新买的路由器怎么用手机设置 新路由器如何
- 下一篇: WenetSpeech数据集的处理和使用