كل طلبية كانت مغامرة: كيف أنقذني نمط ‘الساجا’ (Saga Pattern) من كابوس البيانات غير المتسقة

مقدمة: ليلة إطلاق لن أنساها

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

في البداية، كانت الأجواء احتفالية. الطلبات بتوصل، والمبيعات بترتفع. لكن بعد حوالي ساعة، بدأت الكارثة. هواتف الدعم الفني لم تتوقف عن الرنين: “دفعت ثمن الطلبية بس ما إجاني تأكيد!”، “انخصم المبلغ من حسابي والطلبية ملغية!”، “المنتج اللي طلبته رجع ظهر إنه متوفر بعد ما دفعت حقه!”.

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

المشكلة: لماذا تفشل المعاملات التقليدية (ACID) في الخدمات المصغرة؟

في الأنظمة التقليدية المتجانسة (Monolithic)، نعيش في نعيم معاملات الـ ACID (Atomicity, Consistency, Isolation, Durability). لو بدك تعمل طلبية، بتفتح معاملة واحدة في قاعدة البيانات، بتحدث جدول الطلبات، وبتحدث جدول المخزون، وبتحدث جدول المدفوعات، وبعدين بتعمل COMMIT. لو أي خطوة فشلت، بتعمل ROLLBACK وبيرجع كل شي زي ما كان. بسيطة وسهلة.

لكن في عالم الخدمات المصغرة، كل خدمة إلها قاعدة بياناتها الخاصة. خدمة الطلبات ما بتقدر مباشرة تحدث قاعدة بيانات خدمة المخزون. هذا ضد المبدأ الأساسي للخدمات المصغرة (Loose Coupling & Encapsulation). إذن، كيف نضمن إن سلسلة من العمليات الموزعة على خدمات مختلفة تتم كلها بنجاح أو تفشل كلها معًا؟ هذا هو التحدي الأكبر: إدارة المعاملات الموزعة (Distributed Transactions).

نصيحة من أبو عمر: محاولة تطبيق معاملات الـ (2PC – Two-phase commit) التقليدية في بيئة الخدمات المصغرة غالبًا ما تؤدي إلى كوارث. فهي تخلق اقترانًا شديدًا (Tight Coupling) وتقلل من توافرية النظام (Availability)، لأن كل الخدمات المشاركة يجب أن تكون متاحة ومتجاوبة في نفس اللحظة.

الحل الساحر: نمط الساجا (The Saga Pattern)

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

الساجا هي سلسلة من المعاملات المحلية (Local Transactions) المترابطة. كل معاملة في السلسلة تقوم بتحديث قاعدة البيانات الخاصة بخدمتها، ثم تنشر حدثًا (Event) أو ترسل أمرًا (Command) لتشغيل المعاملة المحلية التالية في الخدمة التالية.

طيب، ماذا لو فشلت إحدى الخطوات في المنتصف؟ هنا يأتي دور المعاملات التعويضية (Compensating Transactions). لكل خطوة في الساجا، يجب أن يكون هناك خطوة معاكسة يمكنها التراجع عن تأثير الخطوة الأولى. فإذا فشلت الخطوة رقم 3، تقوم الساجا بتشغيل المعاملات التعويضية للخطوتين 2 و 1 بالترتيب العكسي لإعادة النظام إلى حالة متسقة.

هذا يقودنا إلى مفهوم الاتساق النهائي (Eventual Consistency). النظام قد يمر بحالة غير متسقة مؤقتًا، لكن الساجا تضمن أنه في النهاية سيصل إلى حالة مستقرة ومتسقة.

أنواع تطبيق نمط الساجا

هناك طريقتان رئيسيتان لتطبيق هذا النمط، ولكل منهما مزاياه وعيوبه.

1. التنسيق الراقص (Choreography)

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

  • كيف يعمل: الخدمة الأولى تنفذ معاملتها المحلية ثم تنشر حدثًا (e.g., OrderCreated) عبر وسيط رسائل (Message Broker) مثل RabbitMQ أو Kafka. الخدمات الأخرى المهتمة بهذا الحدث (مثل خدمة الدفع) تستمع له، وتنفذ معاملاتها الخاصة، ثم تنشر أحداثها الخاصة (e.g., PaymentProcessed)، وهكذا.
  • المزايا: بسيط في المفاهيم، لا يوجد نقطة فشل مركزية (No Single Point of Failure)، الخدمات منفصلة تمامًا (Loosely Coupled).
  • العيوب: يصعب تتبع سير عمل الساجا ككل، خاصة مع زيادة عدد الخدمات. يمكن أن يؤدي إلى تبعيات دورية (Cyclic Dependencies) إذا لم يتم تصميمه بحذر.

مثال كود (Choreography – باستخدام Node.js و RabbitMQ كمثال توضيحي)


// OrderService.js
async function createOrder(orderData) {
  // 1. Save order to local DB in 'PENDING' state
  const order = await db.orders.save({ ...orderData, status: 'PENDING' });

  // 2. Publish an event
  const event = { type: 'OrderCreated', payload: { orderId: order.id, amount: order.total } };
  await messageBroker.publish('order_events', event);
  
  return order;
}

// PaymentService.js - Listens to 'order_events'
messageBroker.subscribe('order_events', async (event) => {
  if (event.type === 'OrderCreated') {
    try {
      // 1. Process payment
      await paymentGateway.charge(event.payload.amount);

      // 2. Publish success event
      const newEvent = { type: 'PaymentSuccessful', payload: { orderId: event.payload.orderId } };
      await messageBroker.publish('payment_events', newEvent);
    } catch (error) {
      // Payment failed, publish a failure event to trigger compensation
      const newEvent = { type: 'PaymentFailed', payload: { orderId: event.payload.orderId } };
      await messageBroker.publish('payment_events', newEvent);
    }
  }
});

2. التنسيق المركزي (Orchestration)

في هذا النهج، يوجد “مايسترو” أو منسق مركزي (Orchestrator) يدير العملية بأكملها. المنسق هو المسؤول عن إخبار كل خدمة بما يجب أن تفعله ومتى. إذا فشلت إحدى الخطوات، فإن المنسق هو المسؤول عن استدعاء المعاملات التعويضية بالترتيب الصحيح.

  • كيف يعمل: العميل يرسل طلبًا إلى المنسق. المنسق بدوره يرسل أوامر (Commands) بشكل متسلسل إلى الخدمات المشاركة. ينتظر المنسق ردًا من كل خدمة قبل أن يرسل الأمر التالي.
  • المزايا: منطق سير العمل مركزي وواضح، أسهل في الفهم والتصحيح (Debugging)، لا توجد تبعيات دورية بين الخدمات.
  • العيوب: المنسق يمكن أن يصبح نقطة فشل مركزية (Single Point of Failure)، ويضيف درجة من الاقتران (Coupling) حيث يجب على الخدمات أن تتوافق مع واجهة برمجة تطبيقات المنسق.

مثال كود (Orchestration – Pseudo-code)


class OrderSagaOrchestrator {
  
  constructor(orderService, paymentService, inventoryService) {
    this.orderService = orderService;
    this.paymentService = paymentService;
    this.inventoryService = inventoryService;
  }

  async execute(orderData) {
    const executedSteps = [];

    try {
      // Step 1: Create Order
      const order = await this.orderService.createOrder(orderData);
      executedSteps.push('CreateOrder');

      // Step 2: Process Payment
      await this.paymentService.processPayment(order.id, order.amount);
      executedSteps.push('ProcessPayment');

      // Step 3: Reserve Inventory
      await this.inventoryService.reserveInventory(order.id, order.items);
      executedSteps.push('ReserveInventory');
      
      // All successful, finalize the order
      await this.orderService.approveOrder(order.id);
      
    } catch (error) {
      // Something went wrong, start compensation
      this.compensate(executedSteps, orderData);
    }
  }

  async compensate(steps, orderData) {
    // Reverse the steps and compensate
    for (const step of steps.reverse()) {
      switch (step) {
        case 'ReserveInventory':
          await this.inventoryService.releaseInventory(orderData.id, orderData.items);
          break;
        case 'ProcessPayment':
          await this.paymentService.refundPayment(orderData.id);
          break;
        case 'CreateOrder':
          await this.orderService.cancelOrder(orderData.id);
          break;
      }
    }
  }
}

نصائح من خبرة أبو عمر

بعد سنوات من التعامل مع الساجا، تعلمت كم درس “على جلدي” كما نقول. إليكم الزبدة:

  1. اجعل عملياتك قابلة للتكرار (Idempotent): هذه أهم نصيحة. في الأنظمة الموزعة، قد تصل الرسالة أو يستدعى الإجراء أكثر من مرة. يجب أن تصمم خدماتك ومعاملاتك التعويضية بحيث لو تم استدعاؤها 5 مرات يكون لها نفس تأثير استدعائها مرة واحدة. تخيل لو تم خصم المبلغ من الزبون مرتين! استخدم معرفات فريدة (Unique IDs) لكل عملية لتجنب هذا.
  2. المعاملة التعويضية ليست مجرد “تراجع”: التراجع عن عملية دفع ليس حذف سجل الدفع، بل هو إنشاء معاملة “استرداد” (Refund). التراجع عن حجز مخزون ليس حذف الحجز، بل هو “تحرير” المخزون ليعود متاحًا. فكر جيدًا في المنطق التجاري لكل خطوة تعويضية.
  3. متى تختار Choreography ومتى Orchestration؟ قاعدتي الشخصية:
    • استخدم Choreography للعمليات البسيطة التي تشمل 2-3 خدمات. هي أسرع في التطوير وأكثر مرونة.
    • استخدم Orchestration عندما يتجاوز عدد الخدمات 4 أو عندما يكون سير العمل معقدًا ويحتاج إلى منطق شروطي (Conditional Logic) واضح. الرؤية المركزية هنا لا تقدر بثمن.
  4. الرصد والتتبع هما عيناك في الظلام: بدون وجود معرف تتبع موحد (Correlation ID) يمر عبر كل الخدمات المشاركة في الساجا، فإن تصحيح الأخطاء سيكون جحيمًا. استثمر في أدوات الرصد (Monitoring) والتتبع الموزع (Distributed Tracing) مثل Jaeger أو Zipkin.

الخلاصة: الساجا ليست رفاهية، بل ضرورة 🚀

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

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

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

أبو عمر

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

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

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

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

آخر المدونات

الحوسبة السحابية

كانت الأحمال المفاجئة تكسر ظهرنا: كيف أنقذتنا الحوسبة بدون خوادم (Serverless) من جحيم التوسع اليدوي؟

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

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

كنت أعرف الإجابة التقنية ولكني أرسب: كيف أنقذتني طريقة ‘STAR’ من جحيم المقابلات السلوكية؟

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

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

كان فشل خدمة واحدة ينهار النظام بأكمله: كيف أنقذنا نمط ‘قاطع الدائرة’ من جحيم الفشل المتتالي؟

أشارككم قصة حقيقية عن ليلة كاد فيها تطبيقنا أن ينهار بالكامل بسبب فشل خدمة صغيرة. اكتشفوا كيف أنقذنا نمط تصميم "قاطع الدائرة" (Circuit Breaker) من...

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

من كوابيس التحقق اليدوي إلى ثورة الـ eKYC: قصتي مع إنقاذ الشركات من الاحتيال والتأخير

أتذكر جيداً أكوام الورق التي كادت أن تدفننا في أحد المشاريع الناشئة. في هذه المقالة، أشارككم قصة تحولنا من جحيم التحقق اليدوي لهوية العملاء إلى...

29 أبريل، 2026 قراءة المزيد
ادارة الفرق والتنمية البشرية

اجتماعاتنا كانت حقل ألغام: كيف أنقذتنا ‘السلامة النفسية’ من جحيم الصمت والخوف من الخطأ؟

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

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

كان كل تحديث CSS مغامرة مرعبة: كيف أنقذنا ‘الاختبار البصري التراجعي’ من جحيم ‘لقد بدت أفضل على جهازي’؟

أشارككم قصة حقيقية عن كارثة تسببت بها بضعة أسطر من CSS، وكيف وجدنا الخلاص في تقنية "الاختبار البصري التراجعي" (Visual Regression Testing). مقالة عملية للمطورين...

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