人妖在线一区,国产日韩欧美一区二区综合在线,国产啪精品视频网站免费,欧美内射深插日本少妇

新聞動態(tài)

MongoDB中優(yōu)雅刪除大量數(shù)據(jù)的三種方式

發(fā)布日期:2022-01-28 11:09 | 文章來源:CSDN

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

為什么要“瘦身”呢?

1、表的數(shù)據(jù)量到達一定量級后,數(shù)據(jù)量越大,表的查詢性能會越差。

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

2、表的數(shù)據(jù)有冷熱之分,將很多無用或很少用到的數(shù)據(jù)存儲在數(shù)據(jù)庫中會消耗數(shù)據(jù)庫的資源。

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

所以,對于那些無用的數(shù)據(jù),我們會定期刪除。

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

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

對于這類刪除操作,很多開發(fā)童鞋的實現(xiàn)就是一個簡單的DELETE操作??瓷先ィ唵蚊髁?,干凈利落。

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

以 MySQL 為例:

  • 會造成大事務(wù)
    大事務(wù)會導致主從延遲,而主從延遲又會影響數(shù)據(jù)庫的高可用切換。
  • 回滾表空間會不斷膨脹
    在MySQL 8.0之前,回滾表空間默認是放到系統(tǒng)表空間中,而系統(tǒng)表空間一旦”膨脹“,就不會收縮。
  • 鎖定的記錄多
    相對而言,更容易導致鎖等待。

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

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

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

本文主要包括以下四部分內(nèi)容。

  1. MongoDB中刪除數(shù)據(jù)的三種方式。
  2. 三種方式的執(zhí)行效率對比。
  3. 通過Write Concern規(guī)避主從延遲。
  4. 刪除過程中碰到的Bug。

MongoDB中刪除數(shù)據(jù)的三種方式

在MongoDB中刪除數(shù)據(jù),可通過以下三種方式:

db.collection.remove()

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

db.collection.deleteMany()

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

db.collection.bulkWrite()

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

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

三種方式的執(zhí)行效率對比

環(huán)境:MongoDB 3.4.4,副本集。

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

1. remove

// delete_date是刪除條件
var delete_date = new Date("2021-01-01T00:00:00.000Z");
// 獲取程序開始時間
var start_time = new Date();
// 獲取滿足刪除條件的記錄數(shù)
rows = db.test_collection.find({"createtime": {$lt: delete_date}}).count()
print("total rows:", rows);
// 定義每批需要刪除的記錄數(shù)
var batch_num = 5000;
while (rows > 0) {
    // rows也可理解為剩余記錄數(shù)
    // 如果剩余記錄數(shù)小于batch_num,則將剩余記錄數(shù)賦值給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"
        })
    });
}
// 獲取程序結(jié)束時間
var end_time = new Date();
// 兩者的差值,即為程序執(zhí)行時長
print((end_time - start_time) / 1000);

2. deleteMany

實例思路同remove類似,只不過會將待刪除的_id放到一個數(shù)組中,最后再通過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"]);
    });
    // 通過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

實現(xiàn)思路同deleteMany類似,也是將待刪除的_id放到一個數(shù)組中,最后再調(diào)用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);

接下來,看看三者的執(zhí)行效率。

刪除方式 平均執(zhí)行時間(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ù),可以看出,

  1. 執(zhí)行最慢的是remove,執(zhí)行最快的是bulkWrite,前者差不多是后者的 2.79 倍。
  2. deleteMany 和 bulkWrite 的執(zhí)行效率差不多,但就語法而言,前者比后者簡潔。

所以線上如果要刪除大量數(shù)據(jù),推薦使用 deleteMany + ObjectID 進行批量刪除。

通過 Write Concern 規(guī)避主從延遲

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

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

看下面這個原理圖。

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

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

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

Write Concern的完整語法如下,

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

其中,

w:指定節(jié)點數(shù)或tags。其有如下取值:

  • <number>:顯式指定節(jié)點數(shù)量。

設(shè)置為0,無需Server端反饋。

設(shè)置為1,只需Primary節(jié)點反饋。

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

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

一般來說,設(shè)置的節(jié)點數(shù)越多,數(shù)據(jù)越安全,寫入的效率也會越低。

  • majority:副本集大多數(shù)節(jié)點。

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

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

tag,顧名思義,是給節(jié)點打標簽。常用于多數(shù)據(jù)中心部署場景。

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

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

具體使用,可參考:https://docs.mongodb.com/manual/tutorial/configure-replica-set-tag-sets/#configure-custom-write-concern。

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

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

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

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

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

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

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

  • Write Concern 設(shè)置了 j: true。
  • 每100ms。

由 storage.journal.commitIntervalMs 參數(shù)指定。

  • 創(chuàng)建新的 journal 文件時。

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

wtimeout:超時時長,單位ms。

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

如果設(shè)置了時間,命令在這個時間內(nèi)沒有執(zhí)行成功,則會超時報錯,具體報錯信息如下:

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

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

  1. 不用額外獲取需要刪除的記錄數(shù)。
  2. batch_num在整個執(zhí)行過程中也是不變的。

但用這個版本在線上刪除數(shù)據(jù)時,發(fā)現(xiàn)了一個問題。

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

  • 最后一批的文檔數(shù)小于batch_num時,會出現(xiàn)這個問題。

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

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

  • cursor只是一個迭代對象,并不是查詢結(jié)果。基于cursor可以分批返回記錄,類似于Python中的迭代器。

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

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

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

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

如果刪除的數(shù)據(jù)量較小,使不使用sort則沒多大區(qū)別。

總結(jié)

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

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

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

到此這篇關(guān)于MongoDB中優(yōu)雅刪除大量數(shù)據(jù)的文章就介紹到這了,更多相關(guān)MongoDB刪除大量數(shù)據(jù)內(nèi)容請搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!

參考

[1] Journaling

[2] Write Concern

海外服務(wù)器租用

版權(quán)聲明:本站文章來源標注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請保持原文完整并注明來源及原文鏈接。禁止復制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學習參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請聯(lián)系alex-e#qq.com處理。

實時開通

自選配置、實時開通

免備案

全球線路精選!

全天候客戶服務(wù)

7x24全年不間斷在線

專屬顧問服務(wù)

1對1客戶咨詢顧問

在線
客服

在線客服:7*24小時在線

客服
熱線

400-630-3752
7*24小時客服服務(wù)熱線

關(guān)注
微信

關(guān)注官方微信
頂部