نمط الساجا (Saga Pattern): كيف أنقذنا من جحيم البيانات غير المتسقة في الخدمات المصغرة

يا جماعة الخير، السلام عليكم ورحمة الله وبركاته. معكم أخوكم أبو عمر.

بتذكر هذيك الليلة، كانت الساعة حوالي 2 بعد منتصف الليل، وكنا بنحضر لإطلاق ميزة جديدة في منصة تجارة إلكترونية كنا شغالين عليها. الأجواء كانت متوترة ومผสมة بالحماس، قهوة ورا قهوة، والكل مركز في شاشته. الميزة كانت عبارة عن نظام طلبات متكامل: المستخدم بيعمل طلب، الدفع بيتم، المخزون بيتحدث، وشركة الشحن بتاخد إشعار. كل خطوة من هدول كانت عبارة عن “خدمة مصغرة” (Microservice) منفصلة.

أطلقنا الميزة… وفي الدقائق الأولى، كل شيء كان يبدو تمام. لكن فجأة، بدأت توصلنا بلاغات غريبة. مستخدم بحكي “دفعت الفلوس وانسحبت من حسابي، بس الطلب عندي حالته ‘ملغي’!”. ومستخدم ثاني بحكي “وصلني إيميل تأكيد شحن لمنتج أنا ما دفعته أصلاً!”.

دخلنا على قواعد البيانات، وهون كانت الصدمة. خدمة الدفع (Payment Service) سجلت عملية دفع ناجحة، لكن خدمة المخزون (Inventory Service) فشلت في حجز المنتج لأنه كان آخر قطعة وخدمة تانية حجزته بنفس الثانية. النتيجة؟ بيانات غير متسقة بالمرة. خدمة بتحكي “تم”، وخدمة تانية بتحكي “لأ فشل”. كانت فوضى عارمة، وشغلة بتجلط بمعنى الكلمة. قضينا باقي الليلة بنصلح البيانات يدوياً ونعتذر للعملاء. في هذيك اللحظة أدركت إننا وقعنا في الفخ الكلاسيكي للأنظمة الموزعة: فشل المعاملات الموزعة (Distributed Transactions). ومن هنا بدأت رحلتنا مع نمط الساجا.

ما هي مشكلة المعاملات في الخدمات المصغرة؟

في النظام التقليدي المتجانس (Monolith)، لما بدك تعمل عملية مكونة من عدة خطوات (زي تسجيل مستخدم جديد وإرسال إيميل ترحيبي)، كنت بتحطهم كلهم داخل “معاملة” (Transaction) واحدة في قاعدة البيانات. لو أي خطوة فشلت، قاعدة البيانات بتعمل “Rollback” تلقائياً وبترجع كل شيء زي ما كان. سهلة وبسيطة.

لكن في عالم الخدمات المصغرة، كل خدمة إلها قاعدة بياناتها الخاصة. خدمة الطلبات (Orders) ما بتقدر تبدأ معاملة تشمل قاعدة بيانات خدمة الدفع (Payments) وقاعدة بيانات خدمة المخزون (Inventory). هذا مستحيل تقنياً، لأنه بخالف مبدأ استقلالية الخدمات. طيب شو الحل؟

الحل التقليدي القديم كان اسمه “Two-Phase Commit (2PC)”، لكنه حل معقد جداً وبخلق ترابط قوي (tight coupling) بين الخدمات، وهذا عكس الهدف من الخدمات المصغرة أساساً. لهيك، أغلب المعماريين اليوم بتجنبوه.

بطل الحكاية: نمط الساجا (The Saga Pattern)

نمط الساجا هو ببساطة أسلوب لإدارة اتساق البيانات عبر خدمات متعددة بدون الحاجة لمعاملات موزعة. الفكرة بسيطة: بدل ما نحاول نعمل معاملة واحدة كبيرة، بنقسم العملية لسلسلة من المعاملات المحلية (Local Transactions) الصغيرة، كل واحدة بتصير داخل خدمة واحدة.

الفكرة الجوهرية في الساجا هي: لكل خطوة (معاملة محلية) تنجح، لازم يكون في خطوة عكسية (Compensating Transaction) تقدر تلغي أثرها في حال فشلت خطوة لاحقة في السلسلة.

خلونا نرجع لمثالنا، عملية الطلب:

  1. خدمة الطلبات: تنشئ طلب بحالة “قيد الانتظار” (Pending). (معاملة محلية 1)
  2. خدمة الدفع: تعالج الدفع. (معاملة محلية 2)
  3. خدمة المخزون: تحجز المنتج من المخزون. (معاملة محلية 3)
  4. خدمة الشحن: تنشئ طلب شحن. (معاملة محلية 4)

طيب، ماذا لو فشلت الخطوة 3 (خدمة المخزون)؟ هنا يأتي دور المعاملات العكسية:

  • خدمة المخزون فشلت، فما في داعي لعملية عكسية لها.
  • الساجا تبدأ بتشغيل العمليات العكسية للخطوات الناجحة السابقة:
    • خدمة الدفع: تنفذ معاملة عكسية وهي “إرجاع المبلغ” (Refund).
    • خدمة الطلبات: تنفذ معاملة عكسية وهي “تغيير حالة الطلب إلى ملغي” (Cancel Order).

بهذه الطريقة، حتى لو فشلت العملية في المنتصف، النظام برجع لحالة متسقة وصحيحة. ما في فلوس مسحوبة بدون طلب، ولا طلب مؤكد بدون منتج محجوز.

طرق تطبيق نمط الساجا

الحكي بينا، في طريقتين مشهورات لتطبيق الساجا، وكل وحدة إلها ميزاتها وعيوبها.

1. الكوريغرافيا (Choreography)

في هذا النموذج، ما في قائد أو منسق مركزي. كل خدمة بعد ما تخلص شغلها، بتنشر “حدث” (Event) معين. والخدمات الأخرى المهتمة بتسمع لهذا الحدث وبتنفذ شغلها بناءً عليه.

مثال (سيناريو الطلب):

  1. OrderService تنشئ الطلب وتنشر حدث OrderCreated.
  2. PaymentService تستمع لحدث OrderCreated، تعالج الدفع، ثم تنشر حدث PaymentProcessed.
  3. InventoryService تستمع لحدث PaymentProcessed، تحجز المخزون، ثم تنشر حدث InventoryUpdated.
  4. … وهكذا.

ماذا لو فشل الدفع؟

PaymentService ستنشر حدث PaymentFailed. خدمة OrderService (اللي بدأت كل شيء) رح تكون بتستمع لهذا الحدث، ولما يوصلها، بتنفذ المعاملة العكسية وبتلغي الطلب.


// OrderService
function createOrder(data) {
  const order = db.orders.create({ ...data, status: 'PENDING' });
  eventBus.publish('OrderCreated', { orderId: order.id, amount: data.amount });
  return order;
}

// PaymentService
eventBus.subscribe('OrderCreated', async (event) => {
  try {
    await paymentGateway.charge(event.amount);
    eventBus.publish('PaymentProcessed', { orderId: event.orderId });
  } catch (error) {
    eventBus.publish('PaymentFailed', { orderId: event.orderId, reason: 'Insufficient funds' });
  }
});

// OrderService (again, listening for failure)
eventBus.subscribe('PaymentFailed', async (event) => {
  const order = db.orders.findById(event.orderId);
  order.status = 'CANCELLED';
  await order.save();
  // Maybe notify the user
});
  • الميزات: بسيط، لا يوجد نقطة فشل مركزية (no single point of failure)، الخدمات منفصلة تماماً (loosely coupled).
  • العيوب: صعب تتبع سير العملية ككل. لما يزيد عدد الخدمات، بتصير زي حفلة فوضوية ما بتعرف مين بحكي مع مين.

2. الأوركسترا (Orchestration)

هنا، يوجد “مايسترو” أو منسق (Orchestrator). هو عبارة عن خدمة مخصصة وظيفتها الوحيدة هي تنسيق سير عمل الساجا. هو اللي بحكي لكل خدمة شو تعمل ومتى.

مثال (سيناريو الطلب):

الـ OrderOrchestrator يبدأ العمل:

  1. يرسل أمر لـ OrderService: “أنشئ طلب”. ينتظر الرد.
  2. إذا نجح، يرسل أمر لـ PaymentService: “اخصم المبلغ”. ينتظر الرد.
  3. إذا نجح، يرسل أمر لـ InventoryService: “احجز المنتج”. ينتظر الرد.
  4. وهكذا…

ماذا لو فشل الدفع؟

الـ Orchestrator سيكتشف الفشل (لأن PaymentService سترجع خطأ). عندها، سيبدأ هو بإرسال أوامر العمليات العكسية بالترتيب المعاكس:

  1. يرسل أمر لـ OrderService: “ألغِ الطلب رقم X”.

// OrderOrchestrator
async function executeOrderSaga(data) {
  let orderId;
  try {
    // Step 1: Create Order
    const orderResponse = await orderService.createOrder(data);
    orderId = orderResponse.id;

    // Step 2: Process Payment
    await paymentService.processPayment({ orderId, amount: data.amount });

    // Step 3: Update Inventory
    await inventoryService.updateInventory({ orderId, items: data.items });

    // If all successful, mark saga as complete
    await sagaRepo.update(orderId, { status: 'COMPLETED' });

  } catch (error) {
    // Saga failed, start compensation
    console.error(`Saga for order ${orderId} failed. Starting compensation.`);
    await compensate(orderId, error.step);
  }
}

async function compensate(orderId, failedStep) {
  if (failedStep === 'Payment' || failedStep === 'Inventory') {
    // Compensate order creation
    await orderService.cancelOrder({ orderId });
  }
  if (failedStep === 'Inventory') {
    // Compensate payment
    await paymentService.refundPayment({ orderId });
  }
}
  • الميزات: منطق العملية كله مركزي وواضح، أسهل في التتبع والمراقبة، إدارة الأخطاء أبسط.
  • العيوب: المنسق نفسه ممكن يصير نقطة فشل مركزية. خطر أن يصبح هذا المنسق “خدمة إله” (God Service) تعرف كل شيء عن كل الخدمات الأخرى، مما يزيد الترابط.

نصائح أبو عمر العملية من قلب المعركة

بعد ما تبنينا نمط الساجا، تعلمنا كم درس مهم “بالطريقة الصعبة”. خذوا هالنصائح من الآخر:

  • اجعل عملياتك العكسية بسيطة ومضمونة النجاح: عملية “إرجاع المبلغ” (Refund) يجب أن تكون مصممة بحيث لا تفشل أبداً (مثلاً، لا تعتمد على خدمة خارجية قد تكون معطلة). يجب أن تكون عملية موثوقة جداً.
  • الـ Idempotency هي مفتاح النجاة: تأكد أن عملياتك (العادية والعكسية) تكون “Idempotent”. يعني لو تم استدعاؤها أكثر من مرة بنفس المدخلات، النتيجة تكون واحدة. مثلاً، لو وصل أمر “اخصم 100 دولار للطلب رقم 123” مرتين بسبب مشكلة في الشبكة، يجب أن يتم الخصم مرة واحدة فقط. هذا يمنع الكوارث.
  • المراقبة والتتبع (Observability) ليست رفاهية: بدون أدوات تتبع (Tracing) وسجلات (Logs) واضحة، تتبع فشل ساجا مكونة من 10 خطوات هو كابوس حقيقي. لازم يكون عندك طريقة تشوف فيها كل خطوة في الساجا، وين نجحت ووين فشلت.
  • متى تختار كوريغرافيا ومتى أوركسترا؟ نصيحتي: إذا كانت العملية بسيطة (2-4 خدمات)، ابدأ بالكوريغرافيا (Choreography) فهي أسرع وأبسط. إذا كانت العملية معقدة، أو تحتاج منطق تفريع (if/else)، أو تشمل عدداً كبيراً من الخدمات، فالأوركسترا (Orchestration) هي خيارك الأفضل لأنها تمنحك تحكماً ورؤية أوضح.

الخلاصة… ومن الآخر 💡

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

نمط الساجا (Saga Pattern) ليس حلاً سحرياً، بل هو استراتيجية قوية ومنطقية للتعامل مع هذه الفوضى. سواء اخترت بساطة الكوريغرافيا أو تحكم الأوركسترا، فإن تبني هذا النمط سيمنحك الثقة لبناء أنظمة موزعة قوية وقادرة على التعافي من الأخطاء بشكل تلقائي.

لا تخافوا من الأنظمة الموزعة، فقط افهموا تحدياتها جيداً، واستخدموا الأنماط الصحيحة للتغلب عليها. والله ولي التوفيق.

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

برمجة وقواعد بيانات

تحديثات قاعدة البيانات بدون توقف: كيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من جحيم التوقفات المجدولة؟

هل سئمت من إيقاف الخدمة مع كل تحديث لهيكلة قاعدة البيانات؟ أشارككم قصة حقيقية وكيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من ليالي النشر الطويلة والمُجهدة،...

4 يونيو، 2026 قراءة المزيد
الشبكات والـ APIs

كانت إعادة المحاولة كارثة: كيف أنقذتنا مفاتيح عدم تكرار العمليات (Idempotency Keys) من جحيم الفواتير المزدوجة؟

أشارككم قصة حقيقية من الخنادق البرمجية، يوم كاد خطأ بسيط في إعادة محاولة طلبات الدفع أن يكلفنا سمعتنا وأموال عملائنا. اكتشفوا معنا كيف كانت مفاتيح...

4 يونيو، 2026 قراءة المزيد
الحوسبة السحابية

من التوقف التام إلى النجاة: كيف أنقذتنا استراتيجية “الضوء المرشد” (Pilot Light) يوم انقطعت السحابة؟

أتذكر ذلك اليوم جيدًا، فنجان القهوة الصباحي، وصوت تنبيهات المراقبة يصرخ كأنه يوم القيامة. كانت منطقة سحابية كاملة قد توقفت عن العمل، لكن بفضل استراتيجية...

4 يونيو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

كانت مهمتي البرمجية للاختبار مجرد كود: كيف أنقذني توثيق القرارات من جحيم الصمت بعد المقابلة؟

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

4 يونيو، 2026 قراءة المزيد
التكنلوجيا المالية Fintech

من الانتظار لأيام إلى الدفع في ثوانٍ: كيف أنقذتنا شبكات الدفع الفوري من جحيم التحويلات البنكية؟

أسرد لكم من واقع تجربتي كـ "أبو عمر"، كيف عانينا من بطء وتكلفة التحويلات البنكية الدولية، وكيف جاءت شبكات الدفع الفوري ومعيار ISO 20022 لتكون...

4 يونيو، 2026 قراءة المزيد
البنية التحتية وإدارة السيرفرات

كان كل خادم لدينا ‘ندفة ثلج’ فريدة: كيف أنقذنا ‘الكود كبنية تحتية’ (IaC) من جحيم الانجراف اليدوي؟

في هذه المقالة، أشارككم قصة حقيقية من قلب المعركة التقنية مع "خوادم ندفات الثلج" الفوضوية. سنغوص في مفهوم "الكود كبنية تحتية" (IaC) وكيف أن أدوات...

4 يونيو، 2026 قراءة المزيد
اختبارات الاداء والجودة

كانت تغطية الاختبارات 100% لكن الأخطاء تتسرب: كيف أنقذنا “الاختبار الطفري” من جحيم الثقة الزائفة؟

كنا نظن أن تغطية الاختبار بنسبة 100% هي درعنا الواقي، لكن الأخطاء كانت تتسلل إلى الإنتاج كاللصوص في ليل بهيم. اكتشف كيف أنقذنا "الاختبار الطفري"...

4 يونيو، 2026 قراءة المزيد
البودكاست