عملياتنا كانت فوضى: كيف أنقذنا “نمط الساجا” من جحيم المعاملات الموزعة الفاشلة؟

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

بتذكر قبل كم سنة، كنا شغالين على نظام تجارة إلكترونية جديد كليًا، مبني على معمارية الخدمات المصغرة (Microservices). الحماس كان واصل للسما، والكل كان مبسوط بفكرة تقسيم النظام الكبير لوحدات صغيرة ومستقلة. خدمة للمنتجات، خدمة للطلبات، خدمة للدفع، وخدمة للمخزون… كل شي كان ماشي زي الحلاوة، لحد ما بلّشت توصلنا الشكاوي.

في ليلة من الليالي، وأنا قاعد بشرب كاسة الشاي بالمرمية، رن تلفوني. على الخط كان مدير المشروع، صوته فيه نبرة قلق ما بتطمّن. “أبو عمر، في مصيبة! في طلبات عم تتسجل عنا، والمخزون عم ينقص، بس الدفع بيفشل والفلوس ما بتوصلنا!”. قلبي وقع في رجليّ. فتحت اللابتوب بسرعة، وإذ بالكارثة أمامي: بياناتنا في حالة فوضى عارمة. طلبات مكتملة شكليًا لكنها غير مدفوعة، ومخزون ناقص لمنتجات لم تُبع فعليًا. كنا عايشين في كابوس اسمه “انعدام اتساق البيانات” (Data Inconsistency) في عالم المعاملات الموزعة. هاي الليلة كانت طويلة، وما نمنا فيها، بس كانت هي الشرارة اللي خلتنا نكتشف الحل: نمط الساجا.

ما هو الجحيم الذي كنا نعيش فيه؟ (مشكلة المعاملات الموزعة)

في الأنظمة التقليدية المتجانسة (Monolithic)، كانت الحياة أبسط. كل عمليات الطلب (إنشاء الطلب، خصم المخزون، معالجة الدفع) تحدث داخل معاملة قاعدة بيانات واحدة (Database Transaction). هاي المعاملة كانت تضمن لنا خصائص ACID (الذرية، الاتساق، العزل، الدوام)، ومعناها ببساطة: “يا إما كل شي بصير صح، يا إما ولا شي بصير”. لو فشل الدفع، قاعدة البيانات بترجع كل شي زي ما كان تلقائيًا. سلامتك وتعيش.

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

هون بتظهر مشكلة “المعاملة الموزعة” (Distributed Transaction). حاولنا في البداية نستخدم بروتوكولات معقدة مثل (Two-Phase Commit – 2PC)، لكنها كانت بتعمل ربط قوي (tight coupling) بين الخدمات وبتبطئ النظام بشكل كبير. كانت كأنك بتحاول تجبر كل واحد في فريق يشتغل بنفس اللحظة بالضبط، ولو واحد عطس، الكل بوقف شغل. ببساطة، ما كانت حل عملي.

باختصار، المشكلة كانت: كيف نضمن نتيجة “الكل أو لا شيء” عبر خدمات متعددة ومستقلة؟

ظهور “الساجا”: المنقذ من فوضى البيانات

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

خلونا نرجع لمثال طلب الشراء:

  1. الخطوة الأولى: خدمة الطلبات (Order Service) تنشئ طلبًا جديدًا بحالة “قيد التنفيذ” (Pending). هاي معاملة محلية.
  2. الخطوة الثانية: بعد نجاح الخطوة الأولى، خدمة الطلبات ترسل أمرًا (أو حدثًا) لخدمة المخزون (Inventory Service).
  3. الخطوة الثالثة: خدمة المخزون تحجز المنتج المطلوب. هاي معاملة محلية ثانية.
  4. الخطوة الرابعة: بعد نجاح حجز المخزون، خدمة المخزون ترسل أمرًا لخدمة الدفع (Payment Service).
  5. الخطوة الخامسة: خدمة الدفع تحاول سحب المبلغ من العميل. هاي معاملة محلية ثالثة.
  6. الخطوة السادسة: إذا نجح الدفع، يتم إرسال حدث نجاح، وتقوم خدمة الطلبات بتحديث حالة الطلب إلى “مكتمل” (Completed).

لهون الأمور تمام. لكن القوة الحقيقية للساجا بتظهر لما “تخرب الطبخة”… لما تفشل إحدى الخطوات.

“وإذا خربت الطبخة؟” – التعامل مع الفشل في نمط الساجا

هون بيجي دور “المعاملات التعويضية” (Compensating Transactions). لكل خطوة في الساجا، لازم نجهز خطوة عكسية تمامًا. هاي الخطوة العكسية وظيفتها إنها تلغي أثر الخطوة الأصلية في حال فشل السلسلة.

لنفرض إن الدفع في الخطوة الخامسة فشل. شو بصير؟

  1. فشل الدفع (الخطوة 5): خدمة الدفع تعلن عن فشل عمليتها.
  2. بدء التراجع: الساجا تبدأ بتنفيذ المعاملات التعويضية بالترتيب العكسي.
  3. إلغاء حجز المخزون (تعويض الخطوة 3): يتم إرسال أمر لخدمة المخزون لإلغاء الحجز وإرجاع المنتج للمخزون المتاح.
  4. تحديث حالة الطلب (تعويض الخطوة 1): يتم إرسال أمر لخدمة الطلبات لتحديث حالة الطلب إلى “فشل” (Failed) أو “ملغي” (Cancelled).

بهالطريقة، رجعنا النظام لحالة متسقة. صحيح الطلب فشل، لكن ما تركنا وراه بيانات “يتيمة” أو فوضى. المخزون رجع صحيح، وحالة الطلب واضحة. هذا ما يسمى بـ “الاتساق النهائي” (Eventual Consistency).

كيف نطبق الساجا؟ طريقتان لا ثالث لهما

في طريقتين مشهورات لتنسيق خطوات الساجا:

الطريقة الأولى: الكوريغرافيا (Choreography) – الرقصة المنظمة

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

  • الميزات: بسيطة، لا مركزية، الخدمات منفصلة تمامًا (loosely coupled).
  • العيوب: صعبة التتبع والمراقبة. لما يكون عندك 10 خدمات، من الصعب تعرف وين وصلت الساجا بالضبط أو ليش فشلت. ممكن تصير عندك تبعيات دائرية (cyclic dependencies) إذا ما كنت حذر.

بتكون مناسبة للعمليات البسيطة اللي فيها خطوتين أو ثلاث خدمات بس.

الطريقة الثانية: الأوركسترا (Orchestration) – المايسترو المتحكم

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

السيناريو بكون كالتالي:

  1. العميل يرسل طلب الشراء إلى المنسق.
  2. المنسق يطلب من خدمة الطلبات إنشاء طلب.
  3. خدمة الطلبات ترد على المنسق بالنجاح.
  4. المنسق يطلب من خدمة المخزون حجز المنتج.
  5. خدمة المخزون ترد على المنسق بالنجاح.
  6. المنسق يطلب من خدمة الدفع معالجة الدفع.
  7. إذا فشل الدفع: خدمة الدفع تخبر المنسق بالفشل. المنسق الآن يعرف بالضبط أين فشلت العملية، ويبدأ بإرسال الأوامر التعويضية (اطلب من المخزون إلغاء الحجز، اطلب من الطلبات تحديث الحالة إلى فشل).
  • الميزات: منطق العملية مركزي وواضح، سهلة التتبع والمراقبة، ما في تبعيات دائرية بين الخدمات.
  • العيوب: المنسق ممكن يصير نقطة فشل مركزية (single point of failure)، ويضيف تعقيدًا بوجود خدمة جديدة لازم إدارتها.

مثال كود بسيط لمنسق (Orchestrator) باستخدام Pseudocode


class OrderSagaOrchestrator {

  function executeOrderSaga(orderData) {
    try {
      // Step 1: Create Order
      let orderResult = OrderService.create(orderData);
      
      // Step 2: Reserve Inventory
      let inventoryResult = InventoryService.reserve(orderData.items);

      // Step 3: Process Payment
      let paymentResult = PaymentService.process(orderData.paymentDetails);

      // If all successful, finalize
      OrderService.updateStatus(orderResult.id, "COMPLETED");
      return "Saga Completed Successfully";

    } catch (error) {
      // If any step fails, start compensation
      console.log("Saga failed at step: " + error.step);
      this.compensate(error.step, orderData);
      return "Saga Failed and Rolled Back";
    }
  }

  function compensate(failedStep, orderData) {
    if (failedStep === "Payment") {
      // Compensate for inventory reservation
      InventoryService.release(orderData.items);
    }
    if (failedStep === "Payment" || failedStep === "Inventory") {
      // Compensate for order creation
      OrderService.updateStatus(orderData.id, "FAILED");
    }
  }
}

نصائح من “الختيار” أبو عمر

بعد ما عكّينا وتعلّمنا، هاي شوية نصائح من خبرتي المتواضعة:

  • اجعل عملياتك قابلة للتكرار (Idempotent): هاي أهم نصيحة. لازم تكون المعاملة العادية والمعاملة التعويضية مصممة بحيث لو تنفذت أكثر من مرة بالخطأ (بسبب مشكلة في الشبكة مثلاً)، ما تعمل مصيبة. يعني لو وصل أمر “إلغاء حجز المنتج” مرتين، لازم المرة التانية ما تعمل شي لأن الحجز أُلغي أصلاً.
  • اختر الطريقة الصح: لا تنجر وراء التعقيد. إذا كانت عمليتك بسيطة (2-3 خطوات)، الكوريغرافيا (Choreography) ممتازة وخفيفة. للعمليات المعقدة اللي فيها منطق وقرارات، الأوركسترا (Orchestration) بتعطيك تحكم ووضوح أكبر.
  • المراقبة والتعقب (Observability): بدون أدوات تسجيل (logging) وتتبع (tracing) جيدة، الساجا بتصير صندوق أسود. لازم تستثمر في أدوات تخليك تشوف كل خطوة في الساجا، وين وصلت، وليش فشلت.
  • فكر في فشل التعويض: ماذا لو فشلت المعاملة التعويضية نفسها؟ (مثلاً، خدمة المخزون ما كانت شغالة لما حاولت ترجع المنتج). هذا سيناريو معقد، وحله غالبًا بكون إما بإعادة المحاولة (retry mechanism) مع تأخير، أو بتنبيه فريق الدعم للتدخل اليدوي. لازم تكون مستعد لهي الحالات.

الخلاصة: من الفوضى إلى النظام 🚀

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

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

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

والله ولي التوفيق.

أبو عمر

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

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

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

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

آخر المدونات

أتمتة العمليات

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

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

8 أبريل، 2026 قراءة المزيد
نصائح برمجية

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

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

8 أبريل، 2026 قراءة المزيد
خوارزميات

البحث في قوائمي المرتبة كان يزحف: كيف أنقذني ‘البحث الثنائي’ من جحيم البطء الخطي؟

أشارككم قصة حقيقية من مسيرتي كمبرمج، عندما كاد تطبيق بطيء أن يكلفني مشروعاً كاملاً. اكتشفوا معي كيف أنقذتني خوارزمية "البحث الثنائي" البسيطة، وحولت تطبيقاً يزحف...

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

ميزانيتنا كانت تحترق: كيف أنقذتنا ‘نماذج الإحالة’ (Attribution Models) من جحيم تخمين القنوات الرابحة؟

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

8 أبريل، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

من فوضى المكونات إلى نظام التصميم المتكامل: قصتنا لإنقاذ واجهات المستخدم من جحيم التضارب

أشارككم تجربتي كـ "أبو عمر" في الانتقال من واجهات فوضوية ومكررة إلى بيئة عمل منظمة بفضل "نظام التصميم". سنغوص في رحلتنا لبناء هذا النظام من...

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