TL;DR
MongoDB Atlas의 Transaction에서는 Deadlock이 발생하지 않는다는 간단한 예제[1]
(2024.08.07, MongoDB Atlas 7.0기준)
[1] MongoDB는 Transaction을 지원합니다만, 모든 DML에 Transaction을 사용하는 것은 주의 깊은 고려가 필요합니다
IT Journeyman
MongoDB는 명시적 Transaction(Lock)을 지원합니다.
그러면 늘 따라오는 질문이 Deadlock입니다만, MongoDB Atlas에서는 별로 개의치 않으셔도 됩니다.
대부분의 NoSQL에서는 명시적(사용자 선언) Transaction을 지원하지 않고, LUW/LWW(Last Update Win/Last Write Win)을 채택하고 있습니다. 간단하게 말해서 같은 Row(Mongodb에서는 Document)를 여러 사용자가 동시에 변경하면 찰나지간으로 마지막에 변경한 내용만 반영이 되고, 다른 변경 세션에는 어떤 에러도 리턴하지 않습니다. 여기서 당황하시거나 화를 내시면 안 됩니다, 원래 대부분 NoSQL(Apache Cassandra 등)은 Dealock이나 Lock Waiting을 없애는 방향으로 설계되었기 때문입니다.
그러면 "이런 NoSQL은 리소스 경합 상황(Lock)을 어떻게 해결하느냐?"라고 물으신다면, Application Lock으로 해결하라고 합니다. 다시말해 사용자 프로그램 코드에서 처리하라고 합니다.(*2) 내가 읽은 데이터를 변경할 때, 내가 읽은 값이 변경되지 않았는지 변경을 하기 전에 다시 한 번 if문으로 체크하고 변경을 하고, if not이면 에러를 리턴하게 합니다. 아니면 상태 컬럼으로 데이터의 라이프사이클을 관리하면서 프로그램 로직을 처리하는 등의 방법으로 합니다. 예를 들면 주문의 상태 컬럼이 Booking일 때만 Canceled나 Delivering으로 변경할 수 있게 하고, Delivering일 때만 Delivered로 변경할 수 있게 하는 겁니다.
[2] 또 여기서 당황하시거나 화를 내시면 안 됩니다. NoSQL은 경제적(싼) 비용으로 대량의 데이터를 처리하도록 설계된 DB입니다. 그게 싫으시다면, 고가의 HW를 전제로 한 고가 SW 라이센스의 DB(예를 들어, Oracle)을 쓰시면 됩니다. 대부분 무상인 NoSQL에 화를 낼 일이 아닙니다. 아마도 Oracle 같은 상용DB는 같은 크기의 데이터를 처리하는 비용이 5~10배 정도 더 비쌀 겁니다.
Table of Contents
Executive Summary
● Definition of Deadlock
● MongoDB Atlas and Deadlock
0. Nodejs Sample Code
1. Run deadlockExample.js
2. Monitoring Lock
2.1 Write Operations Waiting for a Lock
2.2 Sample Output
Executive Summary
- Definition of Deadlock
- 서로 다른 세션이 서로의 자원을 기다리면서 무한정 기다리는 현상
예를 들어 Session 1이 Document 1(Row 1)을 변경하고,
Session 2이 Document 2(Row2)을 변경하고,
Session 1이 Document 2를 변경하려고 할 때 Waiting하게 되고,
Session 2가 Document 1를 변경하려고 할 때 Waiting하는 현상
- MongoDB Atlas and Deadlock
- MongoDB Atlas는 사용자가 명시적(Explicit or User) Transaction을 수행할 때, Deadlock Situation이 발생하지 않음
- 그 이유는 아래와 같은 파라미터와 내부 Timeout으로 인해 30초 이내(최대 1분) 기다리는 세션은 모두 Timeout Error로 Lock을 Release함
- 아래 파라미터는 Atlas에서는 사용자가 변경할 수 없음(설치형에서는 사용자가 변경가능)
- transactionLifetimeLimitSeconds
Specifies the lifetime of multi-document transactions. Transactions that exceed this limit are considered expired and will be aborted by a periodic cleanup process. The cleanup process runs every transactionLifetimeLimitSeconds/2 seconds or at least once every 60 seconds.
0. Nodejs Sample Code
const { MongoClient } = require('mongodb');
// Replace with your MongoDB deployment's connection string.
const uri = "mongodb+srv://it_admin:***@m0cluster.nnuhput.mongodb.net/?retryWrites=true";
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });
async function createDeadlock() {
await client.connect();
const db = client.db('testDB');
const collection = db.collection('testCollection');
// Insert initial documents
await collection.insertMany([{ _id: 1, value: 'a' }, { _id: 2, value: 'b' }]);
// Create sessions
const session1 = client.startSession();
const session2 = client.startSession();
try {
// Transaction 1
const transaction1 = async () => {
session1.startTransaction();
await collection.updateOne({ _id: 1 }, { $set: { value: 'session1' } }, { session: session1 });
console.log("Session 1: Locked document 1");
// Delay to ensure both sessions lock the first document
await new Promise(resolve => setTimeout(resolve, 20000));
await collection.updateOne({ _id: 2 }, { $set: { value: 'session1' } }, { session: session1 });
console.log("Session 1: Locked document 2");
await session1.commitTransaction();
session1.endSession();
};
// Transaction 2
const transaction2 = async () => {
session2.startTransaction();
await collection.updateOne({ _id: 2 }, { $set: { value: 'session2' } }, { session: session2 });
console.log("Session 2: Locked document 2");
// Delay to ensure both sessions lock the second document
await new Promise(resolve => setTimeout(resolve, 20000));
await collection.updateOne({ _id: 1 }, { $set: { value: 'session2' } }, { session: session2 });
console.log("Session 2: Locked document 1");
await session2.commitTransaction();
session2.endSession();
};
// Run transactions concurrently to cause a deadlock
await Promise.all([transaction1(), transaction2()]);
} catch (e) {
console.error("Error during transaction:", e);
await session1.abortTransaction();
await session2.abortTransaction();
session1.endSession();
session2.endSession();
} finally {
await client.close();
}
}
createDeadlock().catch(console.dir);
1. Run deadlockExample.js
프로그램 수행 후 27초 후 관련 에러메시지를 리턴하고 종료됨
kyle@Deadlock % time node deadlockExample.js
Session 1: Locked document 1
Session 2: Locked document 2
Error during transaction: MongoServerError: Caused by :: Write conflict during plan execution and yielding is disabled. :: Please retry your operation or multi-document transaction.
... 중간 생략 ...
at async transaction1 (/Users/kyle/Downloads/Deadlock/deadlockExample.js:30:13) {
errorResponse: {
errorLabels: [ 'TransientTransactionError' ],
ok: 0,
errmsg: 'Caused by :: Write conflict during plan execution and yielding is disabled. :: Please retry your operation or multi-document transaction.',
code: 112,
codeName: 'WriteConflict',
'$clusterTime': {
clusterTime: new Timestamp({ t: 1723014974, i: 8 }),
signature: [Object]
},
operationTime: new Timestamp({ t: 1723014974, i: 8 })
},
... 중간 생략 ...
node deadlockExample.js 0.35s user 0.10s system 1% cpu 27.064 total
2. Monitoring Lock
2.1 Write Operations Waiting for a Lock
아래처럼 Lock을 기다리는 세션을 조회하는 명령이 있으나 일반적인 상황에서는 조회되는 경우가 드물어서 Sample Output으로 대체함.
db.currentOp() - MongoDB Manual v7.0
db.currentOp(
{
"waitingForLock" : true,
$or: [
{ "op" : { "$in" : [ "insert", "update", "remove" ] } },
{ "query.findandmodify": { $exists: true } }
]
}
)
2.2 Sample Output

'T. > MongoDB' 카테고리의 다른 글
| [AI]Enable Natural Language Querying (1) | 2024.09.25 |
|---|---|
| [DB Internel]killSessions, 누가 이거 돌렸어 ! (0) | 2024.08.16 |
| [Data Lake]Atlas Data Federation (0) | 2024.07.30 |
| [Sizing]Extended Storage Sizes (0) | 2024.07.27 |
| [Performance]readPreference Test (2) | 2024.07.24 |