MongoDB: معالجة المعاملات: ACID واتساق المستندات المتعددة
آخر تحديث: 2026-08-26
تضمن المعاملات «الوحدوية» للعمليات التي تشمل مستندات متعددة — وإتقان هذا المفهوم يتيح تطوير أنظمة مالية وأنظمة طلبات موثوقة.
1. ما ستتعلمه
- المعاملات متعددة المستندات في MongoDB 4.0 وما فوق
- session.startTransaction / commitTransaction
- خصائص الحمض
- readConcern / writeConcern / readPreference
- الاتساق السببي
- أغلفة المعاملات في Mongoose
sequenceDiagram
participant App as Applications
participant DB as MongoDB<br/>Dungeon Collection
participant Log as Log
App->>DB: startTransaction()
activate DB
DB-->>App: session
App->>DB: Deduct $100
DB-->>App: OK
App->>DB: Add $100
DB-->>App: OK
App->>DB: Create a Transaction Log
DB-->>App: OK
alt All successful
App->>DB: commitTransaction()
DB-->>App: ✅ Committed
DB->>Log: Persistence
else Any failure
App->>DB: abortTransaction()
DB-->>App: ❌ Rollback
Note over DB: All changes have been reversed<br/>Data is rolled back to the state before the transaction
end
deactivate DB
2. لماذا تعتبر المعاملات ضرورية؟
شرح المفهوم: المعاملة هي وحدة منطقية لعمليات قاعدة البيانات تضمن أن جميع العمليات التي تندرج ضمنها إما تنجح بالكامل أو يتم التراجع عنها بالكامل. عند الكتابة إلى مستندات أو مجموعات متعددة (مثل عمليات تحويل الأموال أو تقديم الطلبات)، لا يمكن ضمان اتساق البيانات بدون المعاملات — فالنجاح الجزئي والفشل الجزئي من شأنهما أن يؤديا إلى وجود بيانات غير صحيحة.
كيفية العمل: يدعم MongoDB 4.0 والإصدارات الأحدث المعاملات ACID متعددة المستندات، والتي يتم تنفيذها باستخدام عزل اللقطات (snapshot isolation) استنادًا إلى محرك WiredTiger. عند بدء المعاملة، يتم إنشاء لقطة، وتُنفَّذ جميع عمليات القراءة والكتابة بالنسبة لتلك اللقطة؛ وعند التثبيت (commit)، تُطبَّق التغييرات بشكل متكامل على ملفات البيانات، وعند التراجع (rollback)، يتم تجاهل جميع التغييرات. في الخلفية، تعتمد المعاملات على سجل عمليات (oplog) مجموعة النسخ المتماثلة لضمان الاستمرارية والنسخ المتماثل.
شرح مفصل لمبادئ ACID:
| الميزة | المعنى | تطبيق MongoDB | المبدأ |
|---|---|---|---|
| الوحدانية | المعاملات إما تنجح بالكامل أو تفشل بالكامل | التثبيت / الإلغاء | يضمن WiredTiger ذلك من خلال سجل التراجع: عمليات الكتابة تكون متجانسة أثناء التثبيت، والاستعادة تتم باستخدام سجل التراجع أثناء التراجع |
| الاتساق | تظل قيود سلامة البيانات دون تغيير | التحقق من صحة المخطط + قيود المعاملات | يتم فحص جميع القيود قبل تثبيت المعاملة؛ وفي حالة انتهاك أي منها، تُرفض المعاملة |
| العزل | لا تتداخل المعاملات المتزامنة مع بعضها البعض | عزل اللقطة | يتم التقاط لقطة للبيانات عند بدء المعاملة؛ وتستند جميع عمليات القراءة والكتابة إلى تلك اللقطة طوال فترة المعاملة، ولا تتأثر المعاملة بالمعاملات الأخرى |
| المتانة | يتم تخزينها بشكل دائم بعد إتمام المعاملة | دفتر اليومية + سجل عمليات (oplog) لمجموعة النسخ المتماثلة | يتم تسجيل عمليات الكتابة أولاً في دفتر اليومية (WAL)، ثم يتم نسخها إلى غالبية العقد في مجموعة النسخ المتماثلة |
آلية MVCC: تُنفِّذ MongoDB عزل اللقطات من خلال آلية التحكم في التزامن متعدد الإصدارات (MVCC). يحتفظ كل مستند بعدة إصدارات سابقة؛ حيث تقرأ المعاملات البيانات من الإصدار المطابق لطابع الوقت الخاص ببدايتها، بينما تنشئ عمليات الكتابة إصدارات جديدة دون الكتابة فوق الإصدارات القديمة. ولا تصبح النسخة الجديدة مرئية للمعاملات الأخرى إلا عند إتمام العملية.
graph TB
subgraph "MVCC Multiple Versions"
D1["Document v1<br/>balance: 1000"]
D2["Document v2<br/>balance: 900<br/>(Transaction A Edit)"]
D3["Document v3<br/>balance: 1100<br/>(Transaction B Edit)"]
end
subgraph "Transaction Snapshot Read"
T1["TransactionsA (t1)<br/>Read v1"] --> R1["balance: 1000"]
T2["TransactionsB (t2)<br/>Read v1"] --> R2["balance: 1000"]
end
D1 --> D2
D1 --> D3
style D2 fill:#cce5ff
style D3 fill:#d4edda
حالات الاستخدام:
- التحويلات المالية (يجب أن تكون عمليات السحب والإيداع متزامنة)
- تقديم طلبات التجارة الإلكترونية (الطلب + خصم المخزون + خصم الرصيد + السجل)
- تحديث الارتباط بين الجداول المتعددة (مزامنة جداول المستخدمين والأدوار)
- غير مناسب لـ: العمليات التي تتعلق بوثيقة واحدة (الوثائق الفردية في MongoDB هي عمليات ذرية بطبيعتها)، والمعاملات القصيرة عالية التكرار (تتسبب المعاملات في أعباء إضافية كبيرة)
// ❌ Counterexample: Transfer without a transaction
async function transfer(fromUserId, toUserId, amount) {
await User.updateOne({ _id: fromUserId }, { $inc: { balance: -amount } });
// System Crash!
await User.updateOne({ _id: toUserId }, { $inc: { balance: amount } });
// The user's balance was deducted, but the recipient did not receive the payment
}
3. الاستخدامات الأساسية للمعاملات
شرح المفهوم: تُدار معاملات MongoDB من خلال كائنات الجلسة (Session) — حيث يقوم startSession() بإنشاء جلسة، وstartTransaction() ببدء معاملة، وcommitTransaction() بتثبيت التغييرات، وabortTransaction() بالتراجع عن التغييرات. ويجب تمرير جميع العمليات التي تتم ضمن المعاملة كمعلمات لـ { session }.
كيفية العمل: تتمثل دورة حياة المعاملة الكاملة فيما يلي: بدء الجلسة → بدء المعاملة → تنفيذ العمليات (باستخدام معلمات الجلسة) → التثبيت/التراجع → إنهاء الجلسة. عند التثبيت، يقوم WiredTiger بكتابة جميع التغييرات في دفتر اليومية بشكل متكامل؛ وعند التراجع، يعيد جميع التغييرات إلى حالتها السابقة باستخدام سجل التراجع. المدة الافتراضية لانتهاء صلاحية المعاملة هي 60 ثانية؛ ويتم التراجع عن المعاملات تلقائيًا في حالة انتهاء هذه المدة.
دورة حياة المعاملة:
stateDiagram-v2
[*] --> StartSession: startSession()
StartSession --> Active: startTransaction()
Active --> Active: Perform an action (with session)
Active --> Committed: commitTransaction()
Active --> Aborted: abortTransaction()
Committed --> [*]: endSession()
Aborted --> [*]: endSession()
note right of Active: Default 60s automatic rollback on timeout
note right of Committed: Persist changes to journal
قواعد النحو:
| الخطوة | الطريقة | الوصف |
|---|---|---|
| 1 | startSession() |
إنشاء جلسة |
| 2 | startTransaction() |
بدء المعاملة |
| 3 | العملية + {session} |
يجب أن تمر جميع عمليات القراءة والكتابة عبر الجلسة |
| 4a | commitTransaction() |
جميعها ناجحة → إرسال |
| 4b | abortTransaction() |
أي فشل → التراجع |
| 5 | endSession() |
تحرير موارد الجلسة |
// === MongoDB 4.0+ Multi-document transactions ===
const session = db.getMongo().startSession();
session.startTransaction();
try {
// 1. Deduct
db.users.updateOne(
{ _id: fromUserId },
{ $inc: { balance: -amount } },
{ session }
);
// 2. Add
db.users.updateOne(
{ _id: toUserId },
{ $inc: { balance: amount } },
{ session }
);
// 3. Commit Transaction
await session.commitTransaction();
} catch (err) {
// 4. Rollback
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}
تحليل النقاط الرئيسية:
- عملية نسيان إرسال
{ session }ليست جزءًا من معاملة ولا تخضع لحماية تلك المعاملة. commitTransactionوabortTransactionهما عمليتان متجانستان؛ ولن يؤدي تكرار استدعائهما إلى حدوث خطأ.- يتم التراجع عن المعاملات تلقائيًا عند انتهاء المهلة؛ وينبغي أن تحدد طبقة التطبيق مهلة معقولة وأن تنفذ آلية إعادة المحاولة.
4. خصائص ACID
نظرة عامة على المفهوم: يشير مصطلح ACID إلى الضمانات الأساسية الأربع لمعاملات قواعد البيانات، وهي: الترابطية (Atomicity)، والاتساق (Consistency)، والعزل (Isolation)، والاستمرارية (Durability). ويُعد فهم كيفية تطبيق مبدأ ACID في MongoDB الأساس لتصميم نظام معاملات موثوق.
شرح مفصل لمستويات العزل: يدعم MongoDB ثلاثة مستويات لعزل القراءة، يتم التحكم فيها عبر readConcern:
| مستوى العزل | مستوى الاهتمام بالقراءة | السلوك | حالة الاستخدام |
|---|---|---|---|
| قراءة البيانات غير المرسلة | local |
قراءة أحدث البيانات المحلية (قد يتم التراجع عن ذلك) | الإعداد الافتراضي، الأولوية للأداء |
| قراءة البيانات المُدرجة | majority |
قراءة البيانات التي تم تأكيدها من قبل الأغلبية | متطلبات الاتساق القوي |
| عزل اللقطة | snapshot |
لقطة متسقة مع القراءة داخل المعاملة | الإعداد الافتراضي داخل المعاملة |
sequenceDiagram
participant T1 as Transactions1
participant T2 as Transactions2
participant DB as MongoDB
Note over DB: Initial balance=1000
T1->>DB: startTransaction(readConcern: snapshot)
T1->>DB: Read balance → 1000
T2->>DB: startTransaction()
T2->>DB: balance -100 → Write 900
T2->>DB: commitTransaction()
T1->>DB: Read balance → 1000 (Snapshot isolation, can't see T2 changes)
Note over T1: Snapshots ensure intra-transaction consistency
T1->>DB: commitTransaction()
Note over DB: Conflict detection → If T1 also modifies balance, an error will be reported
| الميزة | المعنى | تطبيق MongoDB |
|---|---|---|
| الوحدة | إما أن تنجح المعاملة بالكامل أو تفشل بالكامل | التثبيت / الإلغاء |
| الاتساق | قيود سلامة البيانات | التحقق من صحة المخطط + المعاملات |
| العزل | لا تتداخل المعاملات المتزامنة مع بعضها البعض | عزل اللقطة |
| المتانة | يتم تخزينها بشكل دائم بعد إتمام المعاملة | دفتر اليومية + مجموعة النسخ المتماثلة |
5. readConcern / writeConcern / readPreference
شرح المفهوم: تشكل هذه الإعدادات الثلاثة «الثالوث» الخاص بالتحكم في اتساق المعاملات في MongoDB، حيث تنظم اتساق القراءة، وديمومة الكتابة، وسياسات توجيه القراءة، على التوالي. وهي تحدد التوازن بين الاتساق والأداء فيما يتعلق بالمعاملات.
كيف يعمل:
- writeConcern: عدد العقد التي يجب أن تؤكد عملية الكتابة حتى تُعتبر ناجحة.
w: majorityيضمن عدم ضياع عمليات الكتابة - readConcern: إصدار البيانات الذي تراه عملية القراءة.
majorityيضمن تثبيت عمليات القراءة؛snapshotيضمن الاتساق داخل المعاملة. - readPreference: العقدة التي يتم توجيه عمليات القراءة إليها.
primaryالتناسق الأقوى،secondaryيقلل من الحمل على العقدة الأساسية
(1) موضوع الرسالة
المبدأ: بعد تسجيل عملية الكتابة على الخادم الأساسي، يجب أن تنتظر عددًا محددًا من عمليات التأكيد من الخادم الثانوي تفيد بأن البيانات قد تم نسخها قبل إرجاع إشارة النجاح.
| المعلمة | القيمة | السلوك | الاتساق | الأداء |
|---|---|---|---|---|
w |
1 | التأكيد الأولي فقط | منخفض | الأسرع |
w |
الأغلبية | تم تأكيده من قبل أغلبية العقد | عالي | بطيء |
j |
صحيح | الكتابة إلى سجل القرص | الأسرع | الأبطأ |
wtimeout |
ms | انتهت مهلة الانتظار | — | خطأ في مهلة الانتظار |
session.startTransaction({
writeConcern: {
w: 'majority', // Confirmed by a majority of nodes
j: true, // Write to disk journal
wtimeout: 5000 // 5 Timeout in seconds
}
});
(2) قراءة «كونسيرن»
المبدأ: يحدد إصدارات البيانات التي يمكن لعملية القراءة الوصول إليها — سواءً كان ذلك أحدث إصدار محلي (والذي قد لا يكون قد تم تثبيته بعد) أو الإصدار الذي يحتوي على أكبر عدد من عمليات التأكيد (والذي تم تثبيته بالفعل).
| المستوى | الوصف | السيناريوهات التي ينطبق عليها |
|---|---|---|
local |
قراءة أحدث البيانات المحلية (الافتراضي) | موجهة نحو الأداء؛ تسمح بقراءة البيانات غير الملتزم بها |
majority |
قراءة البيانات التي تم تأكيدها في الغالب | التناسق القوي |
snapshot |
عزل اللقطة (داخل المعاملة فقط) | الإعداد الافتراضي للمعاملات؛ يمنع عمليات القراءة الوهمية |
session.startTransaction({
readConcern: {
level: 'majority' // Read Submitted Data
},
writeConcern: { w: 'majority' }
});
(3) قراءة التفضيلات
المبدأ: يحدد ما إذا كانت عمليات القراءة ستُوجَّه إلى العقدة الأساسية أم الثانوية، مما يؤدي إلى فصل عمليات القراءة عن عمليات الكتابة.
| النمط | السلوك | السيناريوهات التي ينطبق عليها |
|---|---|---|
primary |
العقدة الرئيسية للقراءة فقط | المعاملات ذات الاتساق القوي |
primaryPreferred |
الأولي أولاً؛ إذا لم يكن متاحًا، فالتحول إلى الثانوي | السيناريوهات العامة |
secondary |
عقدة ثانوية للقراءة فقط | إعداد التقارير/التحليل، وتخفيف العبء عن العقدة الرئيسية |
secondaryPreferred |
القراءة أولاً، واللجوء إلى القرص الأساسي في حالة عدم توفره | عمليات قراءة مكثفة، وعمليات كتابة قليلة |
nearest |
أدنى زمن انتقال للشبكة | مجموعة موزعة جغرافيًا |
session.startTransaction({
readPreference: 'primary' // Read-Only Primary Node
});
session.startTransaction({
readPreference: 'secondary' // Read from a child node
});
session.startTransaction({
readPreference: 'secondaryPreferred' // Priority Node
});
مجموعات من ثلاث قطع موصى بها:
| السيناريو | مستوى الاهتمام بالكتابة | مستوى الاهتمام بالقراءة | تفضيل القراءة |
|---|---|---|---|
| المعاملات المالية | majority + j:true | لقطة | أساسي |
| المعاملات العامة | الأغلبية | الأغلبية | الأساسية |
| تحليل التقرير | — | محلي | ثانوي |
| التطوير والاختبار | w:1 | محلي | المفضل الأساسي |
6. تغليف المعاملات في Mongoose
شرح المفهوم: توفر Mongoose واجهة برمجة تطبيقات (API) أكثر أناقة لإدارة المعاملات — وهي المعلمات startSession() وsession. ومع ذلك، فإن الكود النمطي «try-catch-commit-abort» المستخدم لإدارة المعاملات يدويًّا يتسم بالإسهاب؛ لذا فإن تغليفه في الدالة المساعدة withTransaction يمكن أن يبسط كود منطق الأعمال بشكل كبير.
كيفية العمل: يعمل غلاف withTransaction الخاص بـ Mongoose على أتمتة إدارة دورة حياة الجلسة (البدء → التثبيت/الإلغاء → الإنهاء)، مما يتيح للوظائف التجارية التركيز حصريًّا على المنطق الأساسي. كما يدعم Mongoose تمرير معلمات { session } إلى عمليات النموذج (findById، create، updateOne)، مما يتيح التكامل السلس لعمليات CRUD ضمن المعاملات.
مقارنة بين أنماط تغليف المعاملات:
| النمط | حجم الكود | معالجة الأخطاء | دعم إعادة المحاولة | حالات الاستخدام |
|---|---|---|---|---|
| try-catch يدوي | متعدد | يدوي | لا شيء | سيناريوهات بسيطة |
| تغليف مع المعاملة | قليل | تلقائي | يمكن إضافته | موصى به للاستخدام في بيئة الإنتاج |
| mongoose.connection.transaction | الحد الأدنى | تلقائي | مدمج | Mongoose 6+ |
// === mongoose Transaction Encapsulation ===
async function withTransaction(callback) {
const session = await mongoose.startSession();
session.startTransaction();
try {
const result = await callback(session);
await session.commitTransaction();
return result;
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}
}
// === Usage: Transfer ===
async function transfer(fromUserId, toUserId, amount) {
return withTransaction(async (session) => {
const fromUser = await User.findById(fromUserId).session(session);
if (fromUser.balance < amount) {
throw new Error('Insufficient balance');
}
await User.updateOne(
{ _id: fromUserId },
{ $inc: { balance: -amount } },
{ session }
);
await User.updateOne(
{ _id: toUserId },
{ $inc: { balance: amount } },
{ session }
);
await TransactionLog.create([{
fromUserId,
toUserId,
amount,
createdAt: new Date()
}], { session });
return { success: true };
});
}
▶ المثال 2: تغليف إعادة محاولة المعاملات في Mongoose
// Alice's ShopHub Financial System: Transaction encountered WriteConflict automatic retry
async function withRetryTransaction(callback, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
const session = await mongoose.startSession();
session.startTransaction({
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' }
});
try {
const result = await callback(session);
await session.commitTransaction();
return result;
} catch (err) {
await session.abortTransaction();
lastError = err;
if (err.errorLabels && err.errorLabels.includes('TransientTransactionError')) {
console.log(`Retry ${i + 1}/${maxRetries} due to WriteConflict`);
continue;
}
throw err;
} finally {
session.endSession();
}
}
throw lastError;
}
// Usage
await withRetryTransaction(async (session) => {
await User.updateOne({ _id: fromId }, { $inc: { balance: -100 } }, { session });
await User.updateOne({ _id: toId }, { $inc: { balance: 100 } }, { session });
});
الإخراج:
TEXT 📖 للعرض فقط{ success: true }
7. حدود المعاملات
شرح المفهوم: تتميز معاملات MongoDB بحدود استخدام واضحة — حيث يجب تنفيذها داخل مجموعة النسخ المتماثلة، وتخضع لقيود الحجم، ولا تدعم بعض العمليات. ويُعد فهم هذه القيود أمرًا أساسيًّا لتجنب وقوع حوادث في بيئة الإنتاج.
شرح مفصل للقيود:
| القيد | الوصف | السبب | استراتيجية التخفيف |
|---|---|---|---|
| مجموعات النسخ المتماثلة إلزامية | لا يدعم MongoDB المستقل المعاملات | تعتمد المعاملات على سجل oplog للتخزين الدائم | يُسمح باستخدام مجموعة نسخ متماثلة ذات عقدة واحدة في بيئات التطوير |
| مستند بحجم 16 ميغابايت | إجمالي جميع العمليات ضمن معاملة واحدة | الحد الأقصى لعدد المستندات في WiredTiger | تقسيم المعاملات الكبيرة إلى معاملات أصغر |
| المهلة الافتراضية البالغة 60 ثانية | maxTransactionLockRequestTimeoutMillis | منع المعاملات الطويلة من الاحتفاظ بالمقفل | ضبط معلمة المهلة |
| يتعذر إجراء عمليات على المجموعة المحدودة | قيود جزئية | لا يتم دعم عمليات التراجع للمجموعات المحدودة | تجنب إجراء عمليات على المجموعات المحدودة ضمن المعاملات |
| يتعذر إنشاء مجموعات داخل معاملة | تقييد جزئي (تم تخفيفه في الإصدار 4.4 وما بعده) | تعارض بين لغة تعريف البيانات (DDL) والمعاملات | إنشاء المجموعات قبل بدء المعاملة |
| تعارضات الكتابة | التعديلات المتزامنة على نفس المستند | آلية القفل المتفائل | إعادة المحاولة التلقائية في حالة حدوث خطأ «TransientTransactionError» |
| انتظار القفل | المعاملات الطويلة تعيق العمليات الأخرى | قفل «نية الكتابة» | تقصير مدة المعاملات لتجنب العمليات التي تستغرق وقتًا طويلاً |
التأثير على أداء المعاملات:
| العملية | غير متعلقة بالمعاملات | ضمن معاملة | سبب النفقات الإضافية |
|---|---|---|---|
| الكتابة على مستند واحد | اختبار الأداء | +30–50% | صيانة اللقطات + إدارة القفل |
| كتابة مستندات متعددة | N عملية إدخال/إخراج مستقلة | عملية تثبيت واحدة | قد يكون دمج المعاملات في عملية إدخال/إخراج واحدة أسرع في الواقع |
| القراءة | القيمة الأساسية | +10–20% | تكلفة إضافية لقراءة اللقطات |
| إرسال | — | 5–50 مللي ثانية | fsync للمجلة + كتابة سجل العمليات |
أفضل الممارسات في المعاملات:
| التمرين | الوصف |
|---|---|
| اجعل المعاملات قصيرة قدر الإمكان | تجنب المعاملات الطويلة التي تفرض قيودًا؛ واجعل مدتها أقل من 100 مللي ثانية |
| تجنب إجراء العمليات الحسابية داخل المعاملات | قم بإجراء العمليات الحسابية المعقدة خارج المعاملات؛ واقتصر في المعاملات على عمليات القراءة والكتابة فقط |
| إعادة محاولة معالجة تعارضات الكتابة | يوفر MongoDB 4.0+ errorLabels لتحديد الأخطاء القابلة لإعادة المحاولة |
| إعطاء الأولوية للعمليات الذرية التي تشمل مستندًا واحدًا | updateOne + $inc عملية ذرية في حد ذاتها؛ ولا تتطلب أي معاملة |
8. الاتساق السببي
شرح المفهوم: التناسق السببي هو نموذج تناسق أقل صرامة من التناسق القوي — فهو لا يضمن أن تكون جميع العمليات مرتبة ترتيبًا شاملاً، لكنه يضمن أن العمليات التي تربطها علاقات سببية تُنفَّذ بالترتيب الصحيح. على سبيل المثال، «قراءة الرصيد أولاً، ثم خصم المبلغ» — يجب أن تستند عملية الخصم إلى آخر رصيد تمت قراءته؛ وهذه علاقة تبعية سببية.
كيفية العمل: يحقق MongoDB الاتساق السببي من خلال operationTime وclusterTime. تحمل كل عملية ضمن الجلسة logicalTime العملية السابقة، ويضمن الخادم أن ترى العمليات اللاحقة نتائج العمليات السابقة. ويتطلب تمكين الاتساق السببي readConcern: majority + writeConcern: majority.
الاتساق السببي مقابل نماذج الاتساق الأخرى:
| الطراز | الضمان | الأداء | مجالات الاستخدام |
|---|---|---|---|
| التناسق القوي (القابل للتخطيط) | مرتبة عالميًا | الأبطأ | النواة المالية |
| الاتساق السببي | الترتيب السببي | السرعة النسبية | العمليات متعددة الخطوات |
| الاتساق النهائي | غير مرتب | الأسرع | السجلات والإشعارات |
| اقرأ منشوراتي | منشوراتي مرئية | سريع | تجربة المستخدم |
sequenceDiagram
participant A as Alice
participant P as Primary
participant S as Secondary
A->>P: Read Balance (readConcern: majority)
P-->>A: balance=1000, clusterTime=t1
A->>P: Deduct $100 (writeConcern: majority)
Note over A,P: Carry afterClusterTime=t1
P->>S: Copy oplog
S-->>P: Confirm
P-->>A: OK, clusterTime=t2
A->>P: View Transaction History (readConcern: majority)
Note over A,P: Carry afterClusterTime=t2
P-->>A: Includes records of payments that have just been deducted ✅
Note over A,P: Consistency of Cause and Effect:If you read it, you're sure to recognize your own previous writing.
// === Causal Consistency:Ensure the Correct Order of Operations ===
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: 'majority' },
writeConcern: { w: 'majority' }
});
// Operation 1:Read the current balance
const account = db.accounts.findOne({ userId: 'user_001' }, { session });
// Operation 2:Write Based on Read Results
db.accounts.updateOne(
{ userId: 'user_001' },
{ $set: { balance: account.balance - 100 } },
{ session }
);
// Guarantee:Operation 2 What you see is the operation 1 Subsequent Status
تحليل النقاط الرئيسية:
- لضمان الاتساق السببي، يجب عليك استخدام جلسة، كما يجب ضبط كل من
readConcernوwriteConcernعلىmajority. - عند القراءة عبر العقد (تفضيل القراءة: ثانوي)، يضمن الاتساق السببي أن تعكس عملية القراءة عملية الكتابة التي أجرتها العقدة المحلية.
- التناسق السببي هو الآلية الأساسية التي تقوم عليها المعاملات متعددة المستندات و«تدفقات التغييرات» (Change Streams) في MongoDB.
▶ مثال: دليل عملي شامل لمعاملات طلبات التجارة الإلكترونية
// Scene: Order placement process (Order + Inventory Deduction + Wallet Deduction + Logging), Fully Atomic
// Introduction: A replica set is required. Transactions must be in progress
// Initialize Data
db.products.insertOne({ sku: 'PHONE-001', stock: 10, price: 599 });
db.users.insertOne({ _id: 'user_001', balance: 1000 });
db.transaction_logs.createIndex({ userId: 1, createdAt: -1 });
// Complete Transaction Functions
async function placeOrder(userId, items) {
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' }
});
try {
// 1. Calculate the total amount + Check Inventory (Atomic Read)
let total = 0;
for (const item of items) {
const product = db.products.findOne(
{ sku: item.sku, stock: { $gte: item.qty } },
{ session }
);
if (!product) {
throw new Error(`Out of Stock: ${item.sku}`);
}
total += product.price * item.qty;
}
// 2. Check User Balance
const user = db.users.findOne({ _id: userId }, { session });
if (user.balance < total) {
throw new Error('Insufficient balance');
}
// 3. Inventory Deduction (Conditional, Preventing Overselling)
for (const item of items) {
const result = db.products.updateOne(
{ sku: item.sku, stock: { $gte: item.qty } },
{ $inc: { stock: -item.qty } },
{ session }
);
if (result.modifiedCount === 0) {
throw new Error(`Inventory deduction failed: ${item.sku}`);
}
}
// 4. Deduct from the user's balance
db.users.updateOne(
{ _id: userId, balance: { $gte: total } },
{ $inc: { balance: -total } },
{ session }
);
// 5. Create an Order
const orderResult = db.orders.insertOne({
userId,
items,
total,
status: 'paid',
createdAt: new Date()
}, { session });
// 6. Record the transaction log
db.transaction_logs.insertOne({
userId,
orderId: orderResult.insertedId,
amount: total,
type: 'purchase',
createdAt: new Date()
}, { session });
// 7. Commit Transaction
session.commitTransaction();
return { success: true, orderId: orderResult.insertedId };
} catch (err) {
// Any failure → Roll Back All
session.abortTransaction();
return { success: false, error: err.message };
} finally {
session.endSession();
}
}
// Execute: Place an Order
placeOrder('user_001', [
{ sku: 'PHONE-001', qty: 1 }
]);
// Testing Rollback Scenarios: Deliberately Creating Errors
placeOrder('user_001', [
{ sku: 'NONEXIST', qty: 1 } // The product does not exist.
]);
// Throw an exception → Transaction Rollback → Inventory, balance, order, all logs remain unchanged
// Verifying Atomicity:
// db.products.findOne({ sku: 'PHONE-001' }) → stock: 10 (Not deducted)
// db.users.findOne({ _id: 'user_001' }) → balance: 1000 (Not deducted)
النتيجة: عندما تنجح المعاملة، يتم تثبيت جميع التغييرات دفعة واحدة؛ وعندما تفشل المعاملة، يتم التراجع عن جميع التغييرات، مما يضمن اتساق البيانات.
▶ المثال 3:نظام معاملات التجارة الإلكترونية مع إعادة المحاولة(الصعوبة ⭐⭐⭐)
// Scene:ShopHub E-commerce transaction system with automatic retry
const mongoose = require('mongoose');
// غلاف معاملات مع إعادة المحاولة التلقائية
async function withTransactionRetry(callback, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const session = await mongoose.startSession();
session.startTransaction({
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority', j: true }
});
try {
const result = await callback(session);
await session.commitTransaction();
return result;
} catch (err) {
await session.abortTransaction();
lastError = err;
// إعادة المحاولة فقط لأخطاء TransientTransactionError
if (err.errorLabels && err.errorLabels.includes('TransientTransactionError')) {
console.log(`إعادة المحاولة ${attempt}/${maxRetries} بسبب تعارض الكتابة`);
continue;
}
throw err;
} finally {
session.endSession();
}
}
throw lastError;
}
// معاملة طلب كاملة
async function placeOrder(userId, items) {
return withTransactionRetry(async (session) => {
let total = 0;
// 1. التحقق من المخزون وحساب الإجمالي
for (const item of items) {
const product = await Product.findOne(
{ _id: item.productId, stock: { $gte: item.quantity } },
null,
{ session }
);
if (!product) {
throw new Error(`المخزون غير كافٍ: ${item.productId}`);
}
total += product.price * item.quantity;
}
// 2. التحقق من رصيد المستخدم
const user = await User.findById(userId).session(session);
if (user.balance < total) {
throw new Error('الرصيد غير كافٍ');
}
// 3. خصم المخزون
for (const item of items) {
await Product.updateOne(
{ _id: item.productId, stock: { $gte: item.quantity } },
{ $inc: { stock: -item.quantity } },
{ session }
);
}
// 4. خصم الرصيد
await User.updateOne(
{ _id: userId },
{ $inc: { balance: -total } },
{ session }
);
// 5. إنشاء الطلب
const order = await Order.create([{
userId,
items: items.map(i => ({ productId: i.productId, quantity: i.quantity })),
total,
status: 'paid',
createdAt: new Date()
}], { session });
// 6. إنشاء سجل المعاملة
await TransactionLog.create([{
userId,
orderId: order[0]._id,
amount: total,
type: 'purchase'
}], { session });
return { success: true, orderId: order[0]._id, total };
});
}
// استخدام النظام
try {
const result = await placeOrder('user_001', [
{ productId: 'prod_001', quantity: 2 },
{ productId: 'prod_002', quantity: 1 }
]);
console.log('تم الطلب بنجاح:', result);
} catch (err) {
console.error('فشل الطلب:', err.message);
}
الإخراج:
TEXT 📖 للعرض فقطتم الطلب بنجاح: {success: true, orderId: '...', total: 1497}
❓ أسئلة شائعة
📖 ملخص
- تتطلب المعاملات متعددة المستندات في MongoDB 4.0 وما فوق وجود مجموعة نسخ متماثلة
- session.startTransaction / commit / abort
- خصائص ACID: الترابطية / الاتساق / العزل / الاستمرارية
- الثالوث: readConcern / writeConcern / readPreference
- الاتساق السببي
- أغلفة المعاملات في Mongoose
📝 تمارين
- السؤال الأساسي (⭐): قم بتنفيذ معاملة تحويل باستخدام mongosh (بما في ذلك آلية try-catch وrollback).
- المشكلة الأساسية (⭐): استخدم Mongoose لتغليف الدالة المساعدة
withTransaction. - تمرين متقدم (⭐⭐): تنفيذ معاملة طلب (تقديم طلب + خصم المخزون + إنشاء طلب + مسح سلة التسوق — جميعها عمليات متكاملة).
- تمرين متقدم (⭐⭐): اختبر التراجع عن المعاملة في حالة الفشل (قم بإحداث خطأ متعمد للتحقق من الترابطية).
- التحدي (⭐⭐⭐): قم ببناء نظام معاملات تجارة إلكترونية متكامل (الطلبات + المخزون + المحفظة + السجلات) يدعم التراجع الموزع.