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

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

أبو عمر

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

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

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

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

آخر المدونات

تسويق رقمي

ميزانية التسويق كانت ثقباً أسود: كيف أنقذتنا ‘نماذج الإحالة القائمة على البيانات’ من جحيم ‘اللمسة الأخيرة’؟

أتذكر جيداً ذلك الاجتماع العاصف الذي كاد أن يودي بآمالنا في الشركة. كنا ننفق آلاف الدولارات على التسويق، لكن العائد كان مجهولاً، والميزانية أشبه بثقب...

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

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

أشارككم قصة حقيقية عن ليلة كادت أن تدمر مشروعاً كاملاً بسبب مفتاح API منسي في الكود. سنتعلم كيف أن أدوات مثل "مدير الأسرار السحابي" (Cloud...

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

معرض أعمالي كان كارثيًا: كيف أنقذتني “دراسات الحالة” من جحيم “ماذا فعلت بالضبط هنا؟”

كنت أظن أن معرض أعمالي المليء بالروابط كافٍ، حتى واجهت سؤالًا بسيطًا دمر ثقتي: "ماذا فعلت بالضبط في هذا المشروع؟". في هذه المقالة، أشارككم كيف...

28 مايو، 2026 قراءة المزيد
التوسع والأداء العالي والأحمال

كان خادمنا الوحيد يحتضر: كيف أنقذنا ‘موازن الأحمال’ (Load Balancer) من جحيم ‘نقطة الفشل الواحدة’؟

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

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

كان المحتالون يسبقوننا بخطوة: كيف أنقذنا ‘تحليل الرسوم البيانية’ (Graph Analysis) من جحيم شبكات الاحتيال المنظمة؟

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

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

كانت بيئاتنا نسخاً مشوهة: كيف أنقذتنا ‘البنية التحتية كوداً’ (IaC) من جحيم ‘لكنها تعمل على جهازي’؟

أتذكر تلك الليلة جيداً، ليلة إطلاق الميزة التي عملنا عليها لشهور. لكن ما حدث كان كابوساً حقيقياً، والسبب؟ جملة واحدة: "لكنها تعمل على بيئة الاختبار!"....

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