Cơn ác mộng On-Call lúc 2 giờ sángBạn đang theo dõi log thì đột nhiên một loạt cảnh báo VersionError xuất hiện. Hệ thống xử lý đơn hàng của bạn, vốn hoạt động hoàn hảo trong môi trường staging với lưu lượng thấp, hiện đang gặp lỗi dưới áp lực từ người dùng thực tế. Các bản log cho thấy một thủ phạm lặp đi lặp lại:
VersionError: No matching document found for id "65f3a2..." version 0 modifiedPaths "status"
Đây không phải là một lỗi ngẫu nhiên. Đó là triệu chứng của các tình huống lưu lượng truy cập cao, chẳng hạn như chương trình flash sale thương mại điện tử nơi có hơn 100 yêu cầu mỗi giây cố gắng cập nhật cùng một bản ghi kho hàng. Trong khi môi trường local của bạn có vẻ ổn định, các điều kiện tranh chấp (race conditions) trong môi trường production đang khiến Mongoose từ chối các cập nhật hợp lệ.
Nguyên nhân gốc rễ: Optimistic ConcurrencyMongoose quản lý tính nhất quán của document bằng một thuộc tính gọi là __v (version key). Điều này thực thi Optimistic Concurrency Control (Kiểm soát tranh chấp lạc quan). Khi bạn lấy một document bằng findById, Mongoose ghi lại phiên bản hiện tại của nó, ví dụ: __v: 0. Cuối cùng khi bạn gọi doc.save(), Mongoose không chỉ lọc theo ID. Nó gửi một truy vấn cụ thể đến MongoDB:
db.collection.updateOne(
{ _id: "65f3a2...", __v: 0 },
{ $set: { status: "shipped" }, $inc: { __v: 1 } }
)
Nếu một tiến trình riêng biệt đã cập nhật cùng document đó chỉ 10 mili giây trước đó, __v trong database hiện tại là 1. Truy vấn cập nhật của bạn, vẫn đang tìm kiếm __v: 0, sẽ không tìm thấy kết quả khớp. Mongoose phát hiện ra rằng không có document nào được sửa đổi và ném ra lỗi VersionError để ngăn bạn ghi đè lên dữ liệu mà bạn chưa nhìn thấy.
Xác định mô hình xung độtHãy kiểm tra tầng service của bạn để tìm trình tự cụ thể này:
- Lấy dữ liệu (Fetch):
const doc = await Model.findById(id);- Logic: Thực hiện các tác vụ tiêu tốn CPU hoặc chờ các lệnh gọi API khác.- Thay đổi (Mutate):doc.status = 'processed';- Lưu (Save):await doc.save();Khoảng cách giữa bước 1 và bước 4 càng dài thì rủi ro càng cao. Nếu hai yêu cầu bắt đầu bước 1 cùng lúc, cả hai đều giữ__v: 0. Yêu cầu nào về đích trước sẽ thắng; yêu cầu thứ hai sẽ bị lỗi.
Các chiến lược hiệu quả để khắc phục VersionError### Tùy chọn 1: Sử dụng Atomic Updates cho các thay đổi đơn giảnNếu bạn không phụ thuộc vào các hook pre('save') hoặc validation schema phức tạp, hãy bỏ qua chu trình fetch-then-save (lấy rồi lưu). Hãy sử dụng findOneAndUpdate hoặc updateOne. Các lệnh này thực thi trực tiếp trên database và mặc định bỏ qua cơ chế phiên bản nội bộ của Mongoose.
// Nhanh chóng và chống xung đột
await Model.updateOne(
{ _id: id },
{ $set: { status: 'shipped' } }
);
Tại sao cách này hiệu quả: Nó loại bỏ chu trình "đọc-sửa-ghi". Đây là một thao tác đơn lẻ thành công bất kể điều gì đã xảy ra một mili giây trước đó.
Tùy chọn 2: Triển khai một Retry WrapperKhi logic nghiệp vụ của bạn quá phức tạp để tách khỏi .save(), bạn phải xử lý xung đột một cách khéo léo. Hãy bắt lỗi, làm mới dữ liệu và thử lại. Một vòng lặp thử lại 3 lần đơn giản thường giải quyết được 99% các đợt tăng đột biến về tranh chấp.
async function safeUpdate(id, newStatus, retries = 3) {
for (let attempt = 1; attempt setTimeout(res, 50 * attempt));
continue;
}
throw error;
}
}
}
Tùy chọn 3: Bật tùy chọn 'optimisticConcurrency'Mongoose 5.10+ đã giới thiệu một cách tích hợp sẵn để xử lý vấn đề này ở cấp độ schema. Bằng cách thiết lập optimisticConcurrency: true, Mongoose sẽ tự động thêm kiểm tra phiên bản vào tất cả các thao tác save, nhưng bạn vẫn cần bắt và xử lý các lỗi phát sinh trong mã ứng dụng của mình.
Kiểm tra bản sửa lỗiBạn có thể mô phỏng điều kiện tranh chấp tại máy cục bộ bằng cách sử dụng Promise.all để kích hoạt các cập nhật đồng thời trên cùng một ID:
const doc = await Order.create({ status: 'pending' });
// Kích hoạt hai cập nhật cùng lúc
try {
await Promise.all([
safeUpdate(doc._id, 'shipped'),
safeUpdate(doc._id, 'delivered')
]);
console.log('Xử lý cập nhật đồng thời thành công.');
} catch (err) {
console.error('Cập nhật thất bại:', err.message);
}

