日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

MongoDB中如何优雅地删除大量数据

發布時間:2025/1/21 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 MongoDB中如何优雅地删除大量数据 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

刪除大量數據,無論是在哪種數據庫中,都是一個普遍性的需求。除了正常的業務需求,我們需要通過這種方式來為數據庫“瘦身”。

為什么要“瘦身”呢?

  • 表的數據量到達一定量級后,數據量越大,表的查詢性能會越差。

    畢竟數據量越大,B+樹的層級會越高,需要的IO也會越多。

  • 表的數據有冷熱之分,將很多無用或很少用到的數據存儲在數據庫中會消耗數據庫的資源。

    譬如會占用緩存;會增加備份集的大小,進而影響備份的恢復時間等。

  • 所以,對于那些無用的數據,我們會定期刪除。

    對于那些很少用到的數據,則會定期歸檔。歸檔,一般是將數據寫入到歸檔實例或抽取到大數據組件中。歸檔完畢后,會將對應的數據從原實例中刪除。

    一般來說,這種刪除操作涉及的數據量都比較大。

    對于這類刪除操作,很多開發童鞋的實現就是一個簡單的DELETE操作??瓷先?#xff0c;簡單明了,干凈利落。

    但是,這種方式,危害性卻極大。

    以 MySQL 為例:

    • 會造成大事務

      大事務會導致主從延遲,而主從延遲又會影響數據庫的高可用切換。

    • 回滾表空間會不斷膨脹

      在MySQL 8.0之前,回滾表空間默認是放到系統表空間中,而系統表空間一旦”膨脹“,就不會收縮。

    • 鎖定的記錄多

      相對而言,更容易導致鎖等待。

    即使是分布式數據庫,如TiDB,如果一次刪除了大量數據,這批數據在進行Compaction時有可能會觸發流控。

    所以,對于線上的大規模刪除操作,建議分而治之。具體來說,就是批量刪除,每次只刪除一部分數據,分多次執行。

    就如何刪除大量數據,接下來我們看看MongoDB中的落地方案。

    本文主要包括以下四部分內容。

  • MongoDB中刪除數據的三種方式。
  • 三種方式的執行效率對比。
  • 通過Write Concern規避主從延遲。
  • 刪除過程中碰到的Bug。
  • MongoDB中刪除數據的三種方式

    在MongoDB中刪除數據,可通過以下三種方式:

    • db.collection.remove()

      刪除單個文檔或滿足條件的所有文檔。

    • db.collection.deleteMany()

      刪除滿足條件的所有文檔。

    • db.collection.bulkWrite()

      批量操作接口,可執行批量插入、更新、刪除操作。

    接下來,對比下這三種方式的執行效率。

    三種方式的執行效率對比

    環境:MongoDB 3.4.4,副本集。

    測試思路:分別使用 remove、deleteMany、bulkWrite 刪除 10w 條記錄(每批刪除 5000 條),交叉執行 5 次。

    1. remove

    //?delete_date是刪除條件 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)?{//?rows也可理解為剩余記錄數//?如果剩余記錄數小于batch_num,則將剩余記錄數賦值給batch_num//?為什么要怎么做,后面會提到。if?(rows?<?batch_num)?{batch_num?=?rows;}//?獲取滿足刪除條件的最小的5000個_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)?{//?通過remove刪除記錄,這里指定了"justOne":?true,每次只能刪除一條記錄。//?為了避免誤刪除,這里同時指定了主鍵和刪除條件。db.test_collection.remove({'_id':?each_row["_id"],?"createtime":?{'$lt':?delete_date}},?{"justOne":?true,w:?"majority"})}); } //?獲取程序結束時間 var?end_time?=?new?Date(); //?兩者的差值,即為程序執行時長 print((end_time?-?start_time)?/?1000);

    2. deleteMany

    實例思路同remove類似,只不過會將待刪除的_id放到一個數組中,最后再通過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?=?[];//?將滿足條件的主鍵值放入到數組中。cursor.forEach(function?(each_row)?{delete_ids.push(each_row["_id"]);});//?通過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

    實現思路同deleteMany類似,也是將待刪除的_id放到一個數組中,最后再調用bulkWrite進行刪除。

    具體代碼如下:

    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);

    接下來,看看三者的執行效率。

    刪除方式平均執行時間(s)第一次第二次第三次第四次第五次
    remove47.34149.60648.48749.31447.57241.727
    deleteMany16.95116.56618.66917.93218.6612.928
    bulkWrite16.47617.24714.18116.15118.40316.397

    結合表中的數據,可以看出,

  • 執行最慢的是remove,執行最快的是bulkWrite,前者差不多是后者的 2.79 倍。
  • deleteMany 和 bulkWrite 的執行效率差不多,但就語法而言,前者比后者簡潔。
  • 所以線上如果要刪除大量數據,推薦使用 deleteMany + ObjectID 進行批量刪除。

    通過 Write Concern 規避主從延遲

    雖然是批量刪除,但在MySQL中,如果沒控制好節奏,還是很容易導致主從延遲。在MongoDB中,其實也有類似的擔憂,不過我們可以通過 Write Concern 進行規避。

    Write Concern,可理解為寫安全策略,簡單來說,它定義了一個寫操作,需要在幾個節點上應用(Apply)完,才會給客戶端反饋。

    看下面這個原理圖。

    圖中是一個一主兩從的副本集,設置了w: "majority",代表一個寫操作,需要等待副本集中絕大多數節點(本例中是兩個)應用完,才能給客戶端反饋。

    在前面的代碼中,無論是remove,deleteMany還是bulkWrite方法,都設置了w: "majority"。

    之所以這樣設置,一方面是為了保證數據的安全性,畢竟刪除操作能在多個節點落盤,另一方面,還能有效降低批量操作可能導致的主從延遲風險。

    Write Concern的完整語法如下,

    {?w:?<value>,?j:?<boolean>,?wtimeout:?<number>?}

    其中,

    w:指定節點數或tags。其有如下取值:

    • <number>:顯式指定節點數量。

      設置為0,無需Server端反饋。

      設置為1,只需Primary節點反饋。

      設置為2,在副本集中,需要一個Primary節點(Primary節點必需)和一個Secondary節點反饋。

      需要注意的是,這里的Secondary節點必須是數據節點,可以是隱藏節點、延遲節點或Priority為 0 的節點,但仲裁節點(Arbiter)絕對不行。

      一般來說,設置的節點數越多,數據越安全,寫入的效率也會越低。

    • majority:副本集大多數節點。

      與上面不一樣的是,這里的Secondary節點不僅要求是數據節點,它的votes(members[n].votes)還必須大于0。

    • <custom write concern name>:指定tags。

      tag,顧名思義,是給節點打標簽。常用于多數據中心部署場景。

      如一個集群,有5個節點,跨機房部署。其中3個節點在A機房,另外2個節點在B機房,因為對數據的安全性、一致性要求很高,我們希望寫操作至少能在A機房的2個節點落盤,B機房的1個節點落盤。

      對于這種個性化的需求,只有通過tags才能實現。

      具體使用,可參考:Configure Replica Set Tag Sets — MongoDB Manual。

    j:是否需要等待對應操作的日志持久化到磁盤中。

    在MongoDB中,一個寫操作會涉及到三個動作:更新數據,更新索引,寫入oplog,這三個動作要么全部成功,要么全部失敗,這也是MongoDB單行事務的由來。

    對于每個寫操作,WiredTiger都會記錄一條日志到 journal 中。

    日志在寫入journal之前,會首先寫入到 journal buffer(最大128KB)中。

    Journal buffer會在以下場景持久化到 journal 文件中:

    • 副本集中,當有操作等待oplog時。

      這類操作包括:針對oplog最新位置點的掃描查詢;Causally consistent session中的讀操作;對于Secondary節點,每次批量應用oplog后。

    • Write Concern 設置了 j: true。

    • 每100ms。

      由 storage.journal.commitIntervalMs 參數指定。

    • 創建新的 journal 文件時。

      當 journal 文件的大小達到100MB時會自動創建一個新的journal 文件。

    wtimeout:超時時長,單位ms。

    不設置或設置為0,命令在執行的過程中,如果遇到了鎖等待或節點數不滿足要求,會一直阻塞。

    如果設置了時間,命令在這個時間內沒有執行成功,則會超時報錯,具體報錯信息如下:

    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"} })

    刪除過程中遇到的Bug

    其實,最開始的刪除程序是下面這個版本。

    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);

    相對于效率對比章節的版本,這個版本的代碼簡潔不少。

  • 不用額外獲取需要刪除的記錄數。
  • batch_num在整個執行過程中也是不變的。
  • 但用這個版本在線上刪除數據時,發現了一個問題。

    在刪除到最后一批時,程序會hang在那里。重試了多次依然如此。分析如下:

    • 最后一批的文檔數小于batch_num時,會出現這個問題。

      刪除同實例下另外一個集合,也出現了類似的問題。

      但在測試環境,刪除一個簡單的集合卻沒有復現出來,懷疑這個Bug與線上集合的記錄過長有關。

    • cursor只是一個迭代對象,并不是查詢結果?;赾ursor可以分批返回記錄,類似于Python中的迭代器。

      最后一批也不是完全沒有返回,而是在返回100條之后才hang在那里。

    • 不使用sort沒有這個問題。

      為什么要使用sort呢?這樣可保證得到的id是有序且在物理上的存儲是相鄰的。這樣,在執行批量刪除操作時,效率也會相對較高。

      經過實際測試,當要刪除的數據量較大時,使用sort的效率確實比不使用的要高。

      如果刪除的數據量較小,使不使用sort則沒多大區別。

    總結

    從最佳實踐的角度出發,無論是在哪種數據庫中,如果都刪除(更新)大量數據,都建議分而治之,分批執行。

    在MongoDB中,如果要刪除大量數據,推薦使用deleteMany + ObjectID進行批量刪除。

    為了保證操作的安全性及規避批量操作帶來的主從延遲風險,建議在執行刪除操作時,將Write Concern設置為w: "majority"。?

    總結

    以上是生活随笔為你收集整理的MongoDB中如何优雅地删除大量数据的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。