MongoDB: معالجة المعاملات: ACID واتساق المستندات المتعددة

آخر تحديث: 2026-08-26

تضمن المعاملات «الوحدوية» للعمليات التي تشمل مستندات متعددة — وإتقان هذا المفهوم يتيح تطوير أنظمة مالية وأنظمة طلبات موثوقة.

1. ما ستتعلمه


100%
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). يحتفظ كل مستند بعدة إصدارات سابقة؛ حيث تقرأ المعاملات البيانات من الإصدار المطابق لطابع الوقت الخاص ببدايتها، بينما تنشئ عمليات الكتابة إصدارات جديدة دون الكتابة فوق الإصدارات القديمة. ولا تصبح النسخة الجديدة مرئية للمعاملات الأخرى إلا عند إتمام العملية.

100%
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

حالات الاستخدام:

JAVASCRIPT
// ❌ 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 ثانية؛ ويتم التراجع عن المعاملات تلقائيًا في حالة انتهاء هذه المدة.

دورة حياة المعاملة:

100%
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() تحرير موارد الجلسة
JAVASCRIPT
// === 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();
}

تحليل النقاط الرئيسية:

  1. عملية نسيان إرسال { session } ليست جزءًا من معاملة ولا تخضع لحماية تلك المعاملة.
  2. commitTransaction وabortTransaction هما عمليتان متجانستان؛ ولن يؤدي تكرار استدعائهما إلى حدوث خطأ.
  3. يتم التراجع عن المعاملات تلقائيًا عند انتهاء المهلة؛ وينبغي أن تحدد طبقة التطبيق مهلة معقولة وأن تنفذ آلية إعادة المحاولة.


4. خصائص ACID

نظرة عامة على المفهوم: يشير مصطلح ACID إلى الضمانات الأساسية الأربع لمعاملات قواعد البيانات، وهي: الترابطية (Atomicity)، والاتساق (Consistency)، والعزل (Isolation)، والاستمرارية (Durability). ويُعد فهم كيفية تطبيق مبدأ ACID في MongoDB الأساس لتصميم نظام معاملات موثوق.

شرح مفصل لمستويات العزل: يدعم MongoDB ثلاثة مستويات لعزل القراءة، يتم التحكم فيها عبر readConcern:

مستوى العزل مستوى الاهتمام بالقراءة السلوك حالة الاستخدام
قراءة البيانات غير المرسلة local قراءة أحدث البيانات المحلية (قد يتم التراجع عن ذلك) الإعداد الافتراضي، الأولوية للأداء
قراءة البيانات المُدرجة majority قراءة البيانات التي تم تأكيدها من قبل الأغلبية متطلبات الاتساق القوي
عزل اللقطة snapshot لقطة متسقة مع القراءة داخل المعاملة الإعداد الافتراضي داخل المعاملة
100%
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، حيث تنظم اتساق القراءة، وديمومة الكتابة، وسياسات توجيه القراءة، على التوالي. وهي تحدد التوازن بين الاتساق والأداء فيما يتعلق بالمعاملات.

كيف يعمل:

(1) موضوع الرسالة

المبدأ: بعد تسجيل عملية الكتابة على الخادم الأساسي، يجب أن تنتظر عددًا محددًا من عمليات التأكيد من الخادم الثانوي تفيد بأن البيانات قد تم نسخها قبل إرجاع إشارة النجاح.

المعلمة القيمة السلوك الاتساق الأداء
w 1 التأكيد الأولي فقط منخفض الأسرع
w الأغلبية تم تأكيده من قبل أغلبية العقد عالي بطيء
j صحيح الكتابة إلى سجل القرص الأسرع الأبطأ
wtimeout ms انتهت مهلة الانتظار خطأ في مهلة الانتظار
JAVASCRIPT
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 عزل اللقطة (داخل المعاملة فقط) الإعداد الافتراضي للمعاملات؛ يمنع عمليات القراءة الوهمية
JAVASCRIPT
session.startTransaction({
  readConcern: {
    level: 'majority'      // Read Submitted Data
  },
  writeConcern: { w: 'majority' }
});

(3) قراءة التفضيلات

المبدأ: يحدد ما إذا كانت عمليات القراءة ستُوجَّه إلى العقدة الأساسية أم الثانوية، مما يؤدي إلى فصل عمليات القراءة عن عمليات الكتابة.

النمط السلوك السيناريوهات التي ينطبق عليها
primary العقدة الرئيسية للقراءة فقط المعاملات ذات الاتساق القوي
primaryPreferred الأولي أولاً؛ إذا لم يكن متاحًا، فالتحول إلى الثانوي السيناريوهات العامة
secondary عقدة ثانوية للقراءة فقط إعداد التقارير/التحليل، وتخفيف العبء عن العقدة الرئيسية
secondaryPreferred القراءة أولاً، واللجوء إلى القرص الأساسي في حالة عدم توفره عمليات قراءة مكثفة، وعمليات كتابة قليلة
nearest أدنى زمن انتقال للشبكة مجموعة موزعة جغرافيًا
JAVASCRIPT
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+
JAVASCRIPT
// === 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

JAVASCRIPT
// 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.

الاتساق السببي مقابل نماذج الاتساق الأخرى:

الطراز الضمان الأداء مجالات الاستخدام
التناسق القوي (القابل للتخطيط) مرتبة عالميًا الأبطأ النواة المالية
الاتساق السببي الترتيب السببي السرعة النسبية العمليات متعددة الخطوات
الاتساق النهائي غير مرتب الأسرع السجلات والإشعارات
اقرأ منشوراتي منشوراتي مرئية سريع تجربة المستخدم
100%
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.
JAVASCRIPT
// === 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

تحليل النقاط الرئيسية:

  1. لضمان الاتساق السببي، يجب عليك استخدام جلسة، كما يجب ضبط كل من readConcern وwriteConcern على majority.
  2. عند القراءة عبر العقد (تفضيل القراءة: ثانوي)، يضمن الاتساق السببي أن تعكس عملية القراءة عملية الكتابة التي أجرتها العقدة المحلية.
  3. التناسق السببي هو الآلية الأساسية التي تقوم عليها المعاملات متعددة المستندات و«تدفقات التغييرات» (Change Streams) في MongoDB.

▶ مثال: دليل عملي شامل لمعاملات طلبات التجارة الإلكترونية

JAVASCRIPT
// 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:نظام معاملات التجارة الإلكترونية مع إعادة المحاولة(الصعوبة ⭐⭐⭐)

JAVASCRIPT
// 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}

❓ أسئلة شائعة

س هل يمكن للمعاملة أن تضمن اتساقًا قويًّا للبيانات؟
ج نعم، وذلك باستخدام مجموعة النسخ المتماثلة + writeConcern بالأغلبية + readConcern بالأغلبية + readPreference على الخادم الأساسي.
س ما مدى انخفاض أداء المعاملات؟
ج إنها أبطأ بنسبة 30–50% مقارنة بالعمليات غير المتعلقة بالمعاملات. تتضمن المعاملات عمليات قفل ولقطات.
س هل يمكن لمثيل MongoDB ذي العقدة الواحدة استخدام المعاملات؟
ج لا. يجب أن يكون مجموعة نسخ متماثلة أو مجموعة مقسمة.

📖 ملخص


📝 تمارين

  1. السؤال الأساسي (⭐): قم بتنفيذ معاملة تحويل باستخدام mongosh (بما في ذلك آلية try-catch وrollback).
  2. المشكلة الأساسية (⭐): استخدم Mongoose لتغليف الدالة المساعدة withTransaction.
  3. تمرين متقدم (⭐⭐): تنفيذ معاملة طلب (تقديم طلب + خصم المخزون + إنشاء طلب + مسح سلة التسوق — جميعها عمليات متكاملة).
  4. تمرين متقدم (⭐⭐): اختبر التراجع عن المعاملة في حالة الفشل (قم بإحداث خطأ متعمد للتحقق من الترابطية).
  5. التحدي (⭐⭐⭐): قم ببناء نظام معاملات تجارة إلكترونية متكامل (الطلبات + المخزون + المحفظة + السجلات) يدعم التراجع الموزع.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%