MongoDB 中的鎖分析

ZhanLi發表於2023-10-27

MongoDB 中的鎖

前言

MongoDB 是一種常見的文件型資料庫,因為其高效能、高可用、高擴充套件性等特點,被廣泛應用於各種場景。

在多執行緒的訪問下,可能會出現多執行緒同時操作一個集合的情況,進而出現資料衝突的情況,為了保證資料的一致性,MongoDB 採用了鎖機制來保證資料的一致性。

下面來看看 MongoDB 中的鎖機制。

MongoDB 中鎖的型別

MongoDB 中使用多粒度鎖定,它允許操作鎖定在全域性,資料庫或集合級別,同時允許各個儲存引擎在集合級別一下實現自己的併發控制(例如,WiredTiger 中的文件級別)。

MongoDB 中使用一個 readers-writer 鎖,它允許併發多個讀操作訪問資料庫,但是隻提供唯一寫操作訪問。

當一個讀鎖存在時,其它的讀操作可以繼續,不會被阻塞,如果一個寫鎖佔有這個資源的時候,其它所有的讀操作和寫操作都會被阻塞。也就是讀讀不阻塞,讀寫阻塞,寫寫阻塞。

MongoDB 中的鎖首先提供了讀寫鎖,即共享鎖(Shared, S)(讀鎖)以及排他鎖(Exclusive, X)(寫鎖),同時,為了解決多層級資源之間的互斥關係,提高多層級資源請求的效率,還在此基礎上提供了意向鎖(Intent Lock)。即鎖可以劃分為4種型別:

1、共享鎖,讀鎖(S),允許多個執行緒同時讀取一個集合,讀讀不互斥;

2、排他鎖,寫鎖(X),允許一個執行緒寫入資料,寫寫互斥,讀寫互斥;

3、意向共享鎖,IS,表示意向讀取;

4、意向排他鎖,IX,表示意向寫入;

什麼是意向鎖呢?

如果另一個任務企圖在某表級別上應用共享或排他鎖,則受由第一個任務控制的表級別意向鎖的阻塞,第二個任務在鎖定該表前不需要檢查各個頁或行鎖,而只需檢查表上的意向鎖。

簡單的講就是意向鎖是為了快速判斷,表裡面是否有記錄被加鎖。如果沒有意向鎖,大更新操作要判斷是否有更小的操作在進行,

意向鎖之間是不會產生衝突的,也不和 AUTO_INC 表鎖衝突,它只會阻塞表級讀鎖或表級寫鎖,另外,意向鎖也不會和行鎖衝突,行鎖只會和行鎖衝突。

例如,當以寫入模式(X模式)鎖定集合時,相應的資料庫鎖和全域性鎖都必須以意向獨佔(IX)模式鎖定。一個資料庫可以同時以IS和IX模式進行鎖定,但獨佔(X)鎖無法與其他模式並存,共享(S)鎖只能與意向共享(IS)鎖並存。

MongoDB 中的鎖是公平的,所有的請求都會排隊獲取相應的鎖。但是 MongoDB 為了最佳化吞吐量,在執行某個請求的時候,會同時執行和它相容的其它請求。比如一個佇列一個請求佇列需要的鎖如下,執行IS請求的同時,會同時執行和它相容的其他S和IS請求。等這一批請求的S鎖釋放後,再執行X鎖的請求。

IS → IS → X → X → S → IS

這種處理機制保證了在相對公平的前提下,提高了吞吐量,不會讓某一類的請求長時間的等待。

對於長時間的讀或者寫操作,某些條件下,mongodb 會臨時的 讓渡 鎖,以防止長時間的阻塞。

鎖的讓渡釋放

對於大多數讀取和寫入操作,WiredTiger 使用樂觀併發控制。WiredTiger 僅在全域性、資料庫和集合級別使用意向鎖。當儲存引擎檢測到兩個操作之間的衝突時,其中一個將導致寫衝突,從而使 MongoDB 可以在不可見的情況下重新嘗試該操作。

在某些情況下,讀寫操作可以釋放它們持有的鎖。以防止長時間的阻塞。

長時間執行的讀取和寫入操作,比如查詢、更新和刪除,在許多情況下都會釋放鎖。MongoDB操作也可以在影響多個文件的寫入操作中,在單個文件修改之間釋放鎖。

對於支援文件級併發控制的儲存引擎,比如 WiredTiger,在訪問儲存時通常不需要釋放鎖,因為在全域性、資料庫和集合級別保持的意向鎖不會阻塞其他讀取者和寫入者。然而,操作會定期釋放鎖,以便:

1、避免長時間儲存事務,因為這些事務可能需要在記憶體中儲存大量資料;

2、充當中斷點,以便可以終止長時間執行的操作;

3、允許需要對集合進行獨佔訪問的操作,比如索引/集合的刪除和建立。

常見操作使用的鎖型別

下面列列舉一下 MongoDB 中的常見操作對應的鎖型別

select:庫級別的意向讀鎖(r),表級別的意向讀鎖(r),文件級別的讀鎖(R);

update/insert:庫級別的意向寫鎖(w),表級別的意向寫鎖(w),文件級別的寫鎖(W);

foreground 方式建立索引:庫級別的寫鎖(W);當為一個集合建立索引時,因為是庫級別的寫鎖,這個操作將阻塞其他的所有操作,任意基於所有資料庫申請讀或寫鎖都將等待直到前臺完成索引建立操作;

background 方式建立索引:庫級別的意向寫鎖(w),表級別的意向寫鎖(w);從 MongoDB 4.2 開始,在構建過程的開始和結束時,索引構建僅獲取正在進行索引的集合的獨佔鎖,以保護後設資料更改。在建立索引的其它部分,使用鎖的讓渡行為,最大限度保證集合的讀寫訪問。

如果定位 MongoDB 中鎖操作

當查詢有慢查詢出現的時候,有時候會出現鎖的阻塞等待,緊急情況,需要我們快速定位並且結束當前操作。

使用 db.currentOp() 就能檢視當前資料庫正在執行的操作。

db.currentOp()

{
    "inprog" : [
        {
            "opid" : 6222,   #程式號
            "active" : true, #是否活動狀態
            "secs_running" : 3,#操作執行了多少秒
            "microsecs_running" : NumberLong(3662328),#操作持續時間(以微秒為單位)。MongoDB透過從操作開始時減去當前時間來計算這個值。
            "op" : "getmore",#操作型別,包括(insert/query/update/remove/getmore/command)
            "ns" : "local.oplog.rs",#名稱空間
            "query" : {#如果op是查詢操作,這裡將顯示查詢內容;也有說這裡顯示具體的操作語句的
                 
            },
            "client" : "192.168.91.132:45745",#連線的客戶端資訊
            "desc" : "conn5",#資料庫的連線資訊
            "threadId" : "0x7f1370cb4700",#執行緒ID
            "connectionId" : 5,#資料庫的連線ID
            "waitingForLock" : false,#是否等待獲取鎖
            "numYields" : 0,
            "lockStats" : {
                "timeLockedMicros" : {#持有的鎖時間微秒
                    "r" : NumberLong(141),#整個MongoDB例項的全域性讀鎖
                    "w" : NumberLong(0)#整個MongoDB例項的全域性寫鎖
                },
                "timeAcquiringMicros" : {#為了獲得鎖,等待的微秒時間
                    "r" : NumberLong(16),#整個MongoDB例項的全域性讀鎖
                    "w" : NumberLong(0)#整個MongoDB例項的全域性寫鎖
                }
            }
        }
    ]
}

來看下幾個主要的欄位含義

  • client:發起請求的客戶端;

  • opid: 操作的唯一標識;

  • secs_running:該操作已經執行的時間,單位:微妙。如果該欄位的返回值很大,就需要查詢請求是否合理;

  • op:操作型別。通常是query、insert、update、delete、command中的一種;

  • query/ns:這個欄位能看出是對哪個集合正在執行什麼操作。

當發現一個語句執行時間很久,影響到了整個資料庫的執行,這時候我們 可以考慮中斷這條語句的執行。

使用 db.killOp(opid) 命令終止該請求。

來個試驗的栗子

對錶裡面一個大表建立索引,不新增 backend。

db.notifications.createIndex({userId: -1});

1、查詢執行超過20S 的請求

$ db.currentOp({"active" : true, "secs_running":{ "$gt" : 20 }})

{
	"inprog" : [
		{
			"host" : "host-192-168-61-214:27017",
			"desc" : "conn50774156",
			"connectionId" : 50774156,
			"client" : "172.18.91.66:52088",
			"appName" : "Navicat",
			"clientMetadata" : {
				"application" : {
					"name" : "Navicat"
				},
				"driver" : {
					"name" : "mongoc",
					"version" : "1.16.2"
				},
				"os" : {
					"type" : "Darwin",
					"name" : "macOS",
					"version" : "20.6.0",
					"architecture" : "x86_64"
				},
				"platform" : "cfg=0x0000d6a0e9 posix=200112 stdc=201112 CC=clang 8.0.0 (clang-800.0.42.1) CFLAGS=\"\" LDFLAGS=\"\""
			},
			"active" : true,
			"currentOpTime" : "2023-10-24T01:32:00.615+0000",
			"opid" : -1782291565,
			"lsid" : {
				"id" : UUID("fff3c45d-b6ac-4a30-b83f-5a565ba166ef"),
				"uid" : BinData(0,"EJF4gS8MLpU7cuurTHswrdjF5hInXITH3796necT7PU=")
			},
			"secs_running" : NumberLong(103),
			"microsecs_running" : NumberLong(103025729),
			"op" : "command",
			"ns" : "gleeman.$cmd",
			"command" : {
				"createIndexes" : "notifications",
				"indexes" : [
					{
						"key" : {
							"userId" : -1
						},
						"name" : "userId_-1"
					}
				],
				"$db" : "gleeman",
				"lsid" : {
					"id" : UUID("fff3c45d-b6ac-4a30-b83f-5a565ba166ef")
				},
				"$clusterTime" : {
					"clusterTime" : Timestamp(1698111011, 1),
					"signature" : {
						"hash" : BinData(0,"iKilM1hvvIJC4hrTgu3FebYNhEw="),
						"keyId" : NumberLong("7233287468395528194")
					}
				}
			},
			"msg" : "Index Build (background) Index Build (background): 27288147/34043394 80%",
			"progress" : {
				"done" : 27288148,
				"total" : 34043394
			},
			"numYields" : 213205,
			"locks" : {
				"Global" : "w",
				"Database" : "w",
				"Collection" : "w"
			},
			"waitingForLock" : false,
			"lockStats" : {
				"Global" : {
					"acquireCount" : {
						"r" : NumberLong(213208),
						"w" : NumberLong(213208)
					}
				},
				"Database" : {
					"acquireCount" : {
						"w" : NumberLong(213209),
						"W" : NumberLong(1)
					}
				},
				"Collection" : {
					"acquireCount" : {
						"w" : NumberLong(213207)
					}
				},
				"oplog" : {
					"acquireCount" : {
						"w" : NumberLong(1)
					}
				}
			}
		}
	],
	"ok" : 1,
	"operationTime" : Timestamp(1698111118, 1),
	"$clusterTime" : {
		"clusterTime" : Timestamp(1698111118, 1),
		"signature" : {
			"hash" : BinData(0,"oLzIpVSGpZ213BW4x/jY6ESKvdA="),
			"keyId" : NumberLong("7233287468395528194")
		}
	}
}

2、批次刪除請求大於 20s 的請求

var ops = db.currentOp(
    {
        "active": true,
        "secs_running": {
            "$gt": 20
        }
    }
).inprog

for (i = 0; i < ops.length; i++) {
    ns = ops[i].ns;
    op = ops[i].op;
    if (ns.startsWith("system.") || ns.startsWith("local.oplog.") || ns.length === 0 || op == "none" || ns.command == "" || ns in["admin", "local", "config"]) {
        continue;
    }
    var opid = ops[i].opid;
    db.killOp(opid);
    print("Stopping op #" + opid)
}

3、kill 掉特定 client 端 ip 的請求

var clientIp="172.18.91.66";
var currOp = db.currentOp();

for (op in currOp.inprog) {
    if (clientIp == currOp.inprog[op].client.split(":")[0]) {
        db.killOp(currOp.inprog[op].opid)
    }
}

4、查詢所有 wait 鎖定的寫操作

db.currentOp(
   {
     "waitingForLock" : true,
     $or: [
        { "op" : { "$in" : [ "insert", "update", "remove" ] } },
        { "command.findandmodify": { $exists: true } }
    ]
   }
)

5.返回索引的建立資訊

db.adminCommand(
    {
      currentOp: true,
      $or: [
        { op: "command", "command.createIndexes": { $exists: true }  },
        { op: "none", "msg" : /^Index Build/ }
      ]
    }
)

總結

1、MongoDB 中使用一個 readers-writer 鎖,它允許併發多個讀操作訪問資料庫,但是隻提供唯一寫操作訪問;

2、MongoDB 中的鎖首先提供了讀寫鎖,即共享鎖(Shared, S)(讀鎖)以及排他鎖(Exclusive, X)(寫鎖),同時,為了解決多層級資源之間的互斥關係,提高多層級資源請求的效率,還在此基礎上提供了意向鎖(Intent Lock)。即鎖可以劃分為4中型別:

  • 1、共享鎖,讀鎖(S),允許多個執行緒同時讀取一個集合,讀讀不互斥;

  • 2、排他鎖,寫鎖(X),允許一個執行緒寫入資料,寫寫互斥,讀寫互斥;

  • 3、意向共享鎖,IS,表示意向讀取;

  • 4、意向排他鎖,IX,表示意向寫入;

3、MongoDB 中支援高併發的一個重要的點就是 MongoDB 支援鎖的讓渡,在某些情況下,讀寫操作可以讓渡它們持有的鎖。以防止長時間的阻塞後面的操作;

參考

【mongodb鎖表命令-相關文件】https://www.volcengine.com/theme/900385-M-7-1
【mongo 中的鎖】https://www.jinmuinfo.com/community/MongoDB/docs/15-faq/03-concurrency.html
【FAQ: Concurrency】https://www.mongodb.com/docs/manual/faq/concurrency/
【mongodb中的鎖】https://boilingfrog.github.io/2023/10/27/mongo中的鎖/

相關文章