MongoDB中優(yōu)雅刪除大量數(shù)據(jù)的三種方式
刪除大量數(shù)據(jù),無(wú)論是在哪種數(shù)據(jù)庫(kù)中,都是一個(gè)普遍性的需求。除了正常的業(yè)務(wù)需求,我們需要通過(guò)這種方式來(lái)為數(shù)據(jù)庫(kù)“瘦身”。
為什么要“瘦身”呢?
1、表的數(shù)據(jù)量到達(dá)一定量級(jí)后,數(shù)據(jù)量越大,表的查詢性能會(huì)越差。
畢竟數(shù)據(jù)量越大,B+樹的層級(jí)會(huì)越高,需要的IO也會(huì)越多。
2、表的數(shù)據(jù)有冷熱之分,將很多無(wú)用或很少用到的數(shù)據(jù)存儲(chǔ)在數(shù)據(jù)庫(kù)中會(huì)消耗數(shù)據(jù)庫(kù)的資源。
譬如會(huì)占用緩存;會(huì)增加備份集的大小,進(jìn)而影響備份的恢復(fù)時(shí)間等。
所以,對(duì)于那些無(wú)用的數(shù)據(jù),我們會(huì)定期刪除。
對(duì)于那些很少用到的數(shù)據(jù),則會(huì)定期歸檔。歸檔,一般是將數(shù)據(jù)寫入到歸檔實(shí)例或抽取到大數(shù)據(jù)組件中。歸檔完畢后,會(huì)將對(duì)應(yīng)的數(shù)據(jù)從原實(shí)例中刪除。
一般來(lái)說(shuō),這種刪除操作涉及的數(shù)據(jù)量都比較大。
對(duì)于這類刪除操作,很多開發(fā)童鞋的實(shí)現(xiàn)就是一個(gè)簡(jiǎn)單的DELETE操作。看上去,簡(jiǎn)單明了,干凈利落。
但是,這種方式,危害性卻極大。
以 MySQL 為例:
- 會(huì)造成大事務(wù)
大事務(wù)會(huì)導(dǎo)致主從延遲,而主從延遲又會(huì)影響數(shù)據(jù)庫(kù)的高可用切換。 - 回滾表空間會(huì)不斷膨脹
在MySQL 8.0之前,回滾表空間默認(rèn)是放到系統(tǒng)表空間中,而系統(tǒng)表空間一旦”膨脹“,就不會(huì)收縮。 - 鎖定的記錄多
相對(duì)而言,更容易導(dǎo)致鎖等待。
即使是分布式數(shù)據(jù)庫(kù),如TiDB,如果一次刪除了大量數(shù)據(jù),這批數(shù)據(jù)在進(jìn)行Compaction時(shí)有可能會(huì)觸發(fā)流控。
所以,對(duì)于線上的大規(guī)模刪除操作,建議分而治之。具體來(lái)說(shuō),就是批量刪除,每次只刪除一部分?jǐn)?shù)據(jù),分多次執(zhí)行。
就如何刪除大量數(shù)據(jù),接下來(lái)我們看看MongoDB中的落地方案。
本文主要包括以下四部分內(nèi)容。
- MongoDB中刪除數(shù)據(jù)的三種方式。
- 三種方式的執(zhí)行效率對(duì)比。
- 通過(guò)Write Concern規(guī)避主從延遲。
- 刪除過(guò)程中碰到的Bug。
MongoDB中刪除數(shù)據(jù)的三種方式
在MongoDB中刪除數(shù)據(jù),可通過(guò)以下三種方式:
db.collection.remove()
刪除單個(gè)文檔或滿足條件的所有文檔。
db.collection.deleteMany()
刪除滿足條件的所有文檔。
db.collection.bulkWrite()
批量操作接口,可執(zhí)行批量插入、更新、刪除操作。
接下來(lái),對(duì)比下這三種方式的執(zhí)行效率。
三種方式的執(zhí)行效率對(duì)比
環(huán)境:MongoDB 3.4.4,副本集。
測(cè)試思路:分別使用 remove、deleteMany、bulkWrite 刪除 10w 條記錄(每批刪除 5000 條),交叉執(zhí)行 5 次。
1. remove
// delete_date是刪除條件 var delete_date = new Date("2021-01-01T00:00:00.000Z"); // 獲取程序開始時(shí)間 var start_time = new Date(); // 獲取滿足刪除條件的記錄數(shù) rows = db.test_collection.find({"createtime": {$lt: delete_date}}).count() print("total rows:", rows); // 定義每批需要?jiǎng)h除的記錄數(shù) var batch_num = 5000; while (rows > 0) { // rows也可理解為剩余記錄數(shù) // 如果剩余記錄數(shù)小于batch_num,則將剩余記錄數(shù)賦值給batch_num // 為什么要怎么做,后面會(huì)提到。 if (rows < batch_num) { batch_num = rows; } // 獲取滿足刪除條件的最小的5000個(gè)_id(ObjectID) var cursor = db.test_collection.find({"createtime": {$lt: delete_date}}, {"_id": 1}).sort({"_id": 1}).limit(batch_num); rows = rows - batch_num; cursor.forEach(function (each_row) { // 通過(guò)remove刪除記錄,這里指定了"justOne": true,每次只能刪除一條記錄。 // 為了避免誤刪除,這里同時(shí)指定了主鍵和刪除條件。 db.test_collection.remove({'_id': each_row["_id"], "createtime": {'$lt': delete_date}}, { "justOne": true, w: "majority" }) }); } // 獲取程序結(jié)束時(shí)間 var end_time = new Date(); // 兩者的差值,即為程序執(zhí)行時(shí)長(zhǎng) print((end_time - start_time) / 1000);
2. deleteMany
實(shí)例思路同remove類似,只不過(guò)會(huì)將待刪除的_id放到一個(gè)數(shù)組中,最后再通過(guò)deleteMany一次性刪除。
具體代碼如下:
var delete_date = new Date("2021-01-01T00:00:00.000Z"); var start_time = new Date(); rows = db.test_collection.find({"createtime": {$lt: delete_date}}).count() print("total rows:", rows); var batch_num = 5000; while (rows > 0) { if (rows < batch_num) { batch_num = rows; } var cursor = db.test_collection.find({"createtime": {$lt: delete_date}}, {"_id": 1}).sort({"_id": 1}).limit(batch_num); rows = rows - batch_num; var delete_ids = []; // 將滿足條件的主鍵值放入到數(shù)組中。 cursor.forEach(function (each_row) { delete_ids.push(each_row["_id"]); }); // 通過(guò)deleteMany一次刪除5000條記錄。 db.test_collection.deleteMany({ '_id': {"$in": delete_ids}, "createTime": {'$lt': delete_date} },{w: "majority"}) } var end_time = new Date(); print((end_time - start_time) / 1000);
3. bulkWrite
實(shí)現(xiàn)思路同deleteMany類似,也是將待刪除的_id放到一個(gè)數(shù)組中,最后再調(diào)用bulkWrite進(jìn)行刪除。
具體代碼如下:
var delete_date = new Date("2021-01-01T00:00:00.000Z"); var start_time = new Date(); rows = db.test_collection.find({"createtime": {$lt: delete_date}}).count() print("total rows:", rows); var batch_num = 5000; while (rows > 0) { if (rows < batch_num) { batch_num = rows; } var cursor = db.test_collection.find({"createtime": {$lt: delete_date}}, {"_id": 1}).sort({"_id": 1}).limit(batch_num); rows = rows - batch_num; var delete_ids = []; cursor.forEach(function (each_row) { delete_ids.push(each_row["_id"]); }); db.test_collection.bulkWrite( [ { deleteMany: { "filter": { '_id': {"$in": delete_ids}, "createTime": {'$lt': delete_date} } } } ], {ordered: false}, {writeConcern: {w: "majority", wtimeout: 100}} ) } var end_time = new Date(); print((end_time - start_time) / 1000);
接下來(lái),看看三者的執(zhí)行效率。
刪除方式 | 平均執(zhí)行時(shí)間(s) | 第一次 | 第二次 | 第三次 | 第四次 | 第五次 |
---|---|---|---|---|---|---|
remove | 47.341 | 49.606 | 48.487 | 49.314 | 47.572 | 41.727 |
deleteMany | 16.951 | 16.566 | 18.669 | 17.932 | 18.66 | 12.928 |
bulkWrite | 16.476 | 17.247 | 14.181 | 16.151 | 18.403 | 16.397 |
結(jié)合表中的數(shù)據(jù),可以看出,
- 執(zhí)行最慢的是remove,執(zhí)行最快的是bulkWrite,前者差不多是后者的 2.79 倍。
- deleteMany 和 bulkWrite 的執(zhí)行效率差不多,但就語(yǔ)法而言,前者比后者簡(jiǎn)潔。
所以線上如果要?jiǎng)h除大量數(shù)據(jù),推薦使用 deleteMany + ObjectID 進(jìn)行批量刪除。
通過(guò) Write Concern 規(guī)避主從延遲
雖然是批量刪除,但在MySQL中,如果沒(méi)控制好節(jié)奏,還是很容易導(dǎo)致主從延遲。在MongoDB中,其實(shí)也有類似的擔(dān)憂,不過(guò)我們可以通過(guò) Write Concern 進(jìn)行規(guī)避。
Write Concern,可理解為寫安全策略,簡(jiǎn)單來(lái)說(shuō),它定義了一個(gè)寫操作,需要在幾個(gè)節(jié)點(diǎn)上應(yīng)用(Apply)完,才會(huì)給客戶端反饋。
看下面這個(gè)原理圖。
圖中是一個(gè)一主兩從的副本集,設(shè)置了w: "majority",代表一個(gè)寫操作,需要等待副本集中絕大多數(shù)節(jié)點(diǎn)(本例中是兩個(gè))應(yīng)用完,才能給客戶端反饋。
在前面的代碼中,無(wú)論是remove,deleteMany還是bulkWrite方法,都設(shè)置了w: "majority"。
之所以這樣設(shè)置,一方面是為了保證數(shù)據(jù)的安全性,畢竟刪除操作能在多個(gè)節(jié)點(diǎn)落盤,另一方面,還能有效降低批量操作可能導(dǎo)致的主從延遲風(fēng)險(xiǎn)。
Write Concern的完整語(yǔ)法如下,
{ w: <value>, j: <boolean>, wtimeout: <number> }
其中,
w:指定節(jié)點(diǎn)數(shù)或tags。其有如下取值:
- <number>:顯式指定節(jié)點(diǎn)數(shù)量。
設(shè)置為0,無(wú)需Server端反饋。
設(shè)置為1,只需Primary節(jié)點(diǎn)反饋。
設(shè)置為2,在副本集中,需要一個(gè)Primary節(jié)點(diǎn)(Primary節(jié)點(diǎn)必需)和一個(gè)Secondary節(jié)點(diǎn)反饋。
需要注意的是,這里的Secondary節(jié)點(diǎn)必須是數(shù)據(jù)節(jié)點(diǎn),可以是隱藏節(jié)點(diǎn)、延遲節(jié)點(diǎn)或Priority為 0 的節(jié)點(diǎn),但仲裁節(jié)點(diǎn)(Arbiter)絕對(duì)不行。
一般來(lái)說(shuō),設(shè)置的節(jié)點(diǎn)數(shù)越多,數(shù)據(jù)越安全,寫入的效率也會(huì)越低。
- majority:副本集大多數(shù)節(jié)點(diǎn)。
與上面不一樣的是,這里的Secondary節(jié)點(diǎn)不僅要求是數(shù)據(jù)節(jié)點(diǎn),它的votes(members[n].votes)還必須大于0。
- <custom write concern name>:指定tags。
tag,顧名思義,是給節(jié)點(diǎn)打標(biāo)簽。常用于多數(shù)據(jù)中心部署場(chǎng)景。
如一個(gè)集群,有5個(gè)節(jié)點(diǎn),跨機(jī)房部署。其中3個(gè)節(jié)點(diǎn)在A機(jī)房,另外2個(gè)節(jié)點(diǎn)在B機(jī)房,因?yàn)閷?duì)數(shù)據(jù)的安全性、一致性要求很高,我們希望寫操作至少能在A機(jī)房的2個(gè)節(jié)點(diǎn)落盤,B機(jī)房的1個(gè)節(jié)點(diǎn)落盤。
對(duì)于這種個(gè)性化的需求,只有通過(guò)tags才能實(shí)現(xiàn)。
j:是否需要等待對(duì)應(yīng)操作的日志持久化到磁盤中。
在MongoDB中,一個(gè)寫操作會(huì)涉及到三個(gè)動(dòng)作:更新數(shù)據(jù),更新索引,寫入oplog,這三個(gè)動(dòng)作要么全部成功,要么全部失敗,這也是MongoDB單行事務(wù)的由來(lái)。
對(duì)于每個(gè)寫操作,WiredTiger都會(huì)記錄一條日志到 journal 中。
日志在寫入journal之前,會(huì)首先寫入到 journal buffer(最大128KB)中。
Journal buffer會(huì)在以下場(chǎng)景持久化到 journal 文件中:
- 副本集中,當(dāng)有操作等待oplog時(shí)。
這類操作包括:針對(duì)oplog最新位置點(diǎn)的掃描查詢;Causally consistent session中的讀操作;對(duì)于Secondary節(jié)點(diǎn),每次批量應(yīng)用oplog后。
- Write Concern 設(shè)置了 j: true。
- 每100ms。
由 storage.journal.commitIntervalMs 參數(shù)指定。
- 創(chuàng)建新的 journal 文件時(shí)。
當(dāng) journal 文件的大小達(dá)到100MB時(shí)會(huì)自動(dòng)創(chuàng)建一個(gè)新的journal 文件。
wtimeout:超時(shí)時(shí)長(zhǎng),單位ms。
不設(shè)置或設(shè)置為0,命令在執(zhí)行的過(guò)程中,如果遇到了鎖等待或節(jié)點(diǎn)數(shù)不滿足要求,會(huì)一直阻塞。
如果設(shè)置了時(shí)間,命令在這個(gè)時(shí)間內(nèi)沒(méi)有執(zhí)行成功,則會(huì)超時(shí)報(bào)錯(cuò),具體報(bào)錯(cuò)信息如下:
rs:PRIMARY> db.test.insert({"a": 1}, {writeConcern: {w: "majority", wtimeout: 100}}) WriteResult({ "nInserted": 1, "writeConcernError": { "code": 64, "codeName": "WriteConcernFailed", "errInfo": { "wtimeout": true }, "errmsg": "waiting for replication timed out" } })
刪除過(guò)程中遇到的Bug
其實(shí),最開始的刪除程序是下面這個(gè)版本。
var delete_date = new Date("2021-01-01T00:00:00.000Z"); var start_time = new Date(); var batch_num = 5000; while (1 == 1) { var cursor = db.test_collection.find({"createtime": {$lt: delete_date}}, {"_id": 1}).sort({"_id": 1}).limit(batch_num); delete_ids = [] cursor.forEach(function (each_row) { delete_ids.push(each_row["_id"]) }); if (delete_ids.length == 0) { break; } db.test_collection.deleteMany({ '_id': {"$in": delete_ids}, "createtime": {'$lt': delete_date} }, {w: "majority"}) } var end_time = new Date(); print((end_time - start_time) / 1000);
相對(duì)于效率對(duì)比章節(jié)的版本,這個(gè)版本的代碼簡(jiǎn)潔不少。
- 不用額外獲取需要?jiǎng)h除的記錄數(shù)。
- batch_num在整個(gè)執(zhí)行過(guò)程中也是不變的。
但用這個(gè)版本在線上刪除數(shù)據(jù)時(shí),發(fā)現(xiàn)了一個(gè)問(wèn)題。
在刪除到最后一批時(shí),程序會(huì)hang在那里。重試了多次依然如此。分析如下:
- 最后一批的文檔數(shù)小于batch_num時(shí),會(huì)出現(xiàn)這個(gè)問(wèn)題。
刪除同實(shí)例下另外一個(gè)集合,也出現(xiàn)了類似的問(wèn)題。
但在測(cè)試環(huán)境,刪除一個(gè)簡(jiǎn)單的集合卻沒(méi)有復(fù)現(xiàn)出來(lái),懷疑這個(gè)Bug與線上集合的記錄過(guò)長(zhǎng)有關(guān)。
- cursor只是一個(gè)迭代對(duì)象,并不是查詢結(jié)果?;赾ursor可以分批返回記錄,類似于Python中的迭代器。
最后一批也不是完全沒(méi)有返回,而是在返回100條之后才hang在那里。
- 不使用sort沒(méi)有這個(gè)問(wèn)題。
為什么要使用sort呢?這樣可保證得到的id是有序且在物理上的存儲(chǔ)是相鄰的。這樣,在執(zhí)行批量刪除操作時(shí),效率也會(huì)相對(duì)較高。
經(jīng)過(guò)實(shí)際測(cè)試,當(dāng)要?jiǎng)h除的數(shù)據(jù)量較大時(shí),使用sort的效率確實(shí)比不使用的要高。
如果刪除的數(shù)據(jù)量較小,使不使用sort則沒(méi)多大區(qū)別。
總結(jié)
從最佳實(shí)踐的角度出發(fā),無(wú)論是在哪種數(shù)據(jù)庫(kù)中,如果都刪除(更新)大量數(shù)據(jù),都建議分而治之,分批執(zhí)行。
在MongoDB中,如果要?jiǎng)h除大量數(shù)據(jù),推薦使用deleteMany + ObjectID進(jìn)行批量刪除。
為了保證操作的安全性及規(guī)避批量操作帶來(lái)的主從延遲風(fēng)險(xiǎn),建議在執(zhí)行刪除操作時(shí),將Write Concern設(shè)置為w: "majority"。
到此這篇關(guān)于MongoDB中優(yōu)雅刪除大量數(shù)據(jù)的文章就介紹到這了,更多相關(guān)MongoDB刪除大量數(shù)據(jù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
參考
[1] Journaling
[2] Write Concern
相關(guān)文章
window平臺(tái)安裝MongoDB數(shù)據(jù)庫(kù)圖文詳解
本篇文章主要介紹了window平臺(tái)安裝MongoDB數(shù)據(jù)庫(kù)圖文詳解,主要介紹window下面安裝mogod的步驟和使用細(xì)節(jié)。感興趣的小伙伴們可以參考一下。2016-11-11MongoDB最大連接數(shù)設(shè)置失效的異常分析過(guò)程與解決方法
mongodb最大連接數(shù)是20000。所以業(yè)界流傳一段話,千萬(wàn)級(jí)以下的用mysql、千萬(wàn)級(jí)以上的用mongodb,億級(jí)以上的用hadoop。下面這篇文章主要給大家介紹了關(guān)于MongoDB最大連接數(shù)設(shè)置失效的異常分析過(guò)程,需要的朋友可以參考下2018-09-09mongodb+php實(shí)現(xiàn)簡(jiǎn)單的增刪改查
這篇文章主要介紹了mongodb+php實(shí)現(xiàn)簡(jiǎn)單的增刪改查的相關(guān)資料,需要的朋友可以參考下2016-07-07SpringBoot?集成MongoDB實(shí)現(xiàn)文件上傳功能
這篇文章主要介紹了SpringBoot?集成MongoDB實(shí)現(xiàn)文件上傳,主要通過(guò)示例代碼記錄文件上傳的步驟,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04mongodb數(shù)據(jù)庫(kù)入門之CURD簡(jiǎn)單操作示例
這篇文章主要介紹了mongodb數(shù)據(jù)庫(kù)入門之CURD簡(jiǎn)單操作,結(jié)合簡(jiǎn)單示例形式分析了MongoDB數(shù)據(jù)庫(kù)基本的CURD增刪改查相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2019-10-10MongoDB查詢之高級(jí)操作詳解(多條件查詢、正則匹配查詢等)
這篇文章主要給大家介紹了關(guān)于MongoDB查詢之高級(jí)操作(多條件查詢、正則匹配查詢等)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10