Mô tả lỗi
MongoServerError: WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.
Hai transaction đồng thời cùng cố ghi vào document giống nhau, và transaction của bạn đã thua. MongoDB sử dụng kiểm soát đồng thời lạc quan (optimistic concurrency control) — cho phép cả hai transaction chạy song song, rồi phát hiện xung đột tại thời điểm commit và hủy một trong hai. Đây là hành vi bình thường, không phải lỗi driver. Nhiệm vụ của ứng dụng là bắt lỗi này và thực hiện retry.
Các nguyên nhân phổ biến:
- Nhiều worker xử lý đơn hàng và cập nhật cùng một document tồn kho đồng thời
- Các API request song song cập nhật cùng một tài khoản người dùng trong vài mili giây
- Các batch job chạy các transaction chồng chéo trên tập dữ liệu dùng chung
Cách khắc phục từng bước
Bước 1: Triển khai Retry Logic (Làm trước tiên)
WriteConflict là lỗi tạm thời — MongoDB yêu cầu bạn retry, và thường thành công ngay lần thử tiếp theo. Kiểm tra nhãn lỗi TransientTransactionError thay vì so khớp chuỗi thông báo lỗi. So khớp chuỗi dễ bị hỏng qua các phiên bản driver; nhãn lỗi thì ổn định hơn.
Node.js (native driver hoặc Mongoose):
async function runWithRetry(session, fn, maxRetries = 3) {
let attempt = 0;
while (attempt setTimeout(r, 50 * attempt)); // exponential backoff
continue;
}
throw err;
}
}
}
// Cách dùng
const session = await mongoose.startSession();
try {
await runWithRetry(session, async () => {
await Order.findByIdAndUpdate(orderId, { status: 'processing' }, { session });
await Inventory.findByIdAndUpdate(itemId, { $inc: { stock: -1 } }, { session });
});
} finally {
await session.endSession();
}
Python (pymongo):
from pymongo.errors import PyMongoError
import time
def run_with_retry(client, fn, max_retries=3):
for attempt in range(max_retries):
with client.start_session() as session:
try:
with session.start_transaction():
fn(session)
return # đã commit thành công
except PyMongoError as e:
if e.has_error_label("TransientTransactionError") and attempt {
const price = await fetchPriceFromExternalAPI(); // gọi mạng = không tốt
await Order.create([{ price }], { session });
});
// TỐT: fetch bên ngoài, ghi bên trong
const price = await fetchPriceFromExternalAPI();
await session.withTransaction(async () => {
await Order.create([{ price }], { session });
});
Mục tiêu thời gian transaction dưới 1 giây. Giá trị mặc định transactionLifetimeLimitSeconds của MongoDB là 60 giây, nhưng xung đột tích lũy nhanh khi có nhiều transaction đồng thời truy cập cùng document.
Bước 3: Truy cập Document theo Thứ tự Nhất quán
Khi nhiều transaction truy cập cùng một tập document, luôn truy cập chúng theo cùng thứ tự đã sắp xếp. Điều này phá vỡ chu kỳ deadlock gây ra xung đột liên tiếp.
// Sắp xếp ID document trước khi duyệt — mọi transaction đều dùng cùng thứ tự
const docIds = [id1, id2, id3].sort((a, b) => a.toString().localeCompare(b.toString()));
for (const id of docIds) {
await collection.updateOne({ _id: id }, update, { session });
}
Bước 4: Thay Transaction bằng Thao tác Atomic Đơn Document Khi Có Thể
Thao tác đơn document trong MongoDB luôn là atomic và không bao giờ gây ra lỗi WriteConflict. Nếu transaction của bạn chỉ tác động vào một document, bạn hoàn toàn không cần dùng transaction.
// Giảm atomic — không cần transaction, không thể xảy ra WriteConflict
const result = await Inventory.findOneAndUpdate(
{ _id: itemId, stock: { $gt: 0 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after' }
);
if (!result) {
throw new Error('Hết hàng');
}
Bước 5: Kiểm tra Cài đặt Transaction Lifetime
MongoDB hủy bất kỳ transaction nào vượt quá transactionLifetimeLimitSeconds. Kiểm tra giá trị hiện tại và giảm xuống nếu xung đột đang tích lũy — giới hạn ngắn hơn sẽ thất bại nhanh hơn và giải phóng khóa sớm hơn:
// Kiểm tra giá trị hiện tại
db.adminCommand({ getParameter: 1, transactionLifetimeLimitSeconds: 1 })
// Giảm xuống 30 giây để thất bại nhanh hơn và giải phóng khóa sớm hơn
db.adminCommand({ setParameter: 1, transactionLifetimeLimitSeconds: 30 })
Kiểm tra sau khi sửa
Sau khi deploy retry wrapper, xác nhận rằng xung đột đang được bắt thay vì hiện lên như lỗi ứng dụng:
# Đếm số lần xuất hiện WriteConflict trong log (định dạng log JSON)
grep -c "WriteConflict" /var/log/mongodb/mongod.log
# Kiểm tra transaction metrics qua serverStatus
db.serverStatus().metrics.operation
# Theo dõi theo thời gian thực
watch -n 2 'mongo --quiet --eval "printjson(db.serverStatus().metrics.operation)"'
Thêm instrument vào retry wrapper để theo dõi tỷ lệ retry trong môi trường production. Tỷ lệ retry vượt quá 5% tổng số lần thử transaction là dấu hiệu cảnh báo. Lúc đó, retry không còn cứu được bạn nữa — bạn đang gặp vấn đề hot document cần sửa ở tầng data model, không phải tối ưu vòng lặp.
Xung đột kéo dài: Khi Retry Không Còn Đủ
Một số document thu hút xung đột theo thiết kế — một bộ đếm toàn cục, một giỏ hàng dùng chung, một bảng xếp hạng trực tiếp nhận hàng nghìn lượt ghi mỗi giây. Retry sẽ tiếp tục va chạm. Bạn cần suy nghĩ lại về data model:
- Bucket pattern: Chia document nóng thành N shard và chọn ngẫu nhiên một shard cho mỗi lần ghi, gộp lại định kỳ
- Queue-based serialization: Định tuyến các lần ghi vào document nóng qua một worker duy nhất với job queue
- Pre-aggregated counters: Ghi các lần tăng vào document theo user hoặc session, tổng hợp khi đọc
Checklist trước khi tiếp tục
- Retry wrapper kiểm tra nhãn
TransientTransactionError(không so khớp chuỗi) - Retry dùng exponential backoff — không phải vòng lặp chặt
- Không có lệnh gọi mạng, I/O file hoặc tính toán nặng bên trong khối transaction
- Document được truy cập theo thứ tự sắp xếp nhất quán giữa các transaction đồng thời
- Cập nhật đơn document dùng toán tử atomic (
$inc,$set) thay vì read-modify-write - Tỷ lệ retry được theo dõi trong production — cảnh báo nếu vượt quá 5%

