كل طلبية كانت مغامرة: كيف أنقذني نمط ‘الساجا’ (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) لها بداية ووسط ونهاية واضحة، حتى لو تخللتها بعض المنعطفات غير المتوقعة.

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

أبو عمر

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

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

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

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

آخر المدونات

تجربة المستخدم والابداع البصري

تطبيقي كان جميلاً… لكنه كان عدواً لقارئات الشاشة: كيف أنقذتني ‘إمكانية الوصول الرقمية’ (a11y) من بناء جدران أمام المستخدمين؟

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

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

كنت أستجدي التحديثات كل دقيقة: كيف أنقذتني الـ Webhooks من جحيم الاستقصاء (Polling)؟

أشارككم قصة حقيقية عن معاناة واجهتها مع استقصاء (Polling) خدمات الطرف الثالث، وكيف غيّرت الـ Webhooks طريقة بناء تطبيقاتي بالكامل. سنتعمق في الفروقات بين التقنيتين،...

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

تطبيقي كان خاملاً 99% من الوقت… لكنني كنت أدفع ثمن سيرفر كامل: كيف أنقذتني الحوسبة الخادومية (Serverless) من هدر الموارد؟

كنت أدفع شهرياً ثمن سيرفر يعمل 24/7، بينما تطبيقي الصغير لا يستقبل سوى بضع طلبات في اليوم. في هذه المقالة، أشارككم قصتي مع هذا الهدر...

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

طلبات المستخدمين كانت تضيع أثناء الذروة: كيف أنقذتني طوابير الرسائل (Message Queues) من فقدان البيانات؟

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

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

نظام مكافحة غسيل الأموال كان يطلق إنذارات على كل فنجان قهوة: كيف استخدمتُ نماذج الكشف عن الشذوذ (Anomaly Detection) للتركيز على المخاطر الحقيقية؟

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

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

وظائف Cron كانت شبكة عنكبوت صامتة: كيف أنقذتني محركات تنسيق سير العمل من فوضى المهام المجدولة؟

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

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