في عالم التطبيقات الضخمة (Monolithic)، يعد الحفاظ على اتساق البيانات أمراً بسيطاً. تضمن معاملة قاعدة البيانات الواحدة نجاح جميع خطوات العملية التجارية أو فشلها جميعاً. ومع ذلك، مع انتقالنا نحو بنية الخدمات المصغرة، تختفي هذه البساطة. تملك كل خدمة قاعدة بياناتها الخاصة، مما يجعل معاملات ACID التقليدية مستحيلة عبر حدود الخدمات. هنا يأتي دور نمط Saga كأداة أساسية لمهندس البرمجيات الحديث.
المشكلة: اتساق البيانات الموزع
عندما تمتد عملية تجارية عبر خدمات متعددة، مثل تدفق طلبات التجارة الإلكترونية الذي يتضمن خدمة المخزون، وخدمة الدفع، وخدمة الشحن، نواجه تحدي المعاملات الموزعة. لا يمكننا استخدام بروتوكول الالتزام ثنائي المرحلة (2PC) القياسي في العديد من البيئات الحديثة بسبب زمن الاستجابة العالي (Latency)، والارتباط الوثيق، وإمكانية حدوث نقاط فشل واحدة. بدلاً من ذلك، نحتاج إلى نمط يسمح للمعاملات طويلة الأمد بالحفاظ على الاتساق من خلال سلسلة من المعاملات المحلية، يكون لكل منها إجراء تعويضي مقابله.
كيف يعمل نمط Saga
Saga هي سلسلة من المعاملات المحلية. كل معاملة محلية تحدث قاعدة البيانات وتنشر حدثاً أو رسالة لتشغيل المعاملة المحلية التالية في الـ Saga. إذا فشل خطوة ما، ينفذ الـ Saga سلسلة من المعاملات التعويضية لإلغاء التغييرات التي أجرتها الخطوات السابقة. هناك طريقتان رئيسيتان لتنفيذ الـ Sagas:
- الرقص (Choreography): تتواصل الخدمات عبر الأحداث دون منسق مركزي.
- التنسيق (Orchestration): يقوم منسق مركزي بتوجيه تدفق الـ Saga.
الرقص مقابل التنسيق
بينما يكون الرقص أبسط للأنظمة الصغيرة، فقد يصبح من الصعب تتبعه وتصحيح الأخطاء فيه مع زيادة التعقيد. يوفر التنسيق، باستخدام منسق مثل AWS Step Functions أو Temporal، رؤية وتحكماً أفضل، ولكنه يقدم نقطة فشل جديدة إذا لم يتم تصميمه بعناية.
مثال عملي على الكود: نهج الرقص
لنلقِ نظرة على مثال مبسط بلغة Node.js لـ Saga تعتمد على الرقص لعملية "تقديم الطلب". هنا، نفترض استخدام حافلة أحداث مثل RabbitMQ أو Kafka.
// الخطوة 1: إنشاء الطلب (معاملة محلية)
async function handleOrderCreated(event) {
try {
// حجز المخزون محلياً
await inventoryService.reserve(itemIds, quantity);
// نشر الحدث لتشغيل الدفع
eventBus.publish('ItemsReservedEvent', { orderId: event.orderId });
} catch (error) {
// التعويض: إذا فشل الحجز، إشعار بإلغاء الطلب
eventBus.publish('OrderFailedEvent', { orderId: event.orderId });
}
}
// الخطوة 2: معالجة الدفع (مفعلة بواسطة ItemsReservedEvent)
async function handleItemsReserved(event) {
try {
await paymentService.charge(event.orderId, amount);
// النجاح: المضي قدماً إلى الشحن
eventBus.publish('PaymentCompletedEvent', { orderId: event.orderId });
} catch (error) {
// التعويض: إلغاء الطلب (مما سيؤدي إلى تحرير المخزون)
eventBus.publish('OrderFailedEvent', { orderId: event.orderId });
}
}
// الخطوة 3: إلغاء الطلب (إجراء تعويضي)
async function handleOrderFailed(event) {
// تحرير المخزون
await inventoryService.release(event.orderId);
// استرداد المبلغ إذا تم الخصم بالفعل
await paymentService.refund(event.orderId);
}
التحديات والاعتبارات الرئيسية
إن تنفيذ الـ Sagas ليس خالياً من المزالق. يجب على المطورين التعامل بعناية مع الإعادة القابلة للتكرار (Idempotency)، حيث قد تؤدي إعادة محاولة الشبكة إلى تنفيذ نفس الخطوة عدة مرات. بالإضافة إلى ذلك، يعد القابلية للمراقبة (Observability) أمراً حاسماً؛ فبدون تتبع موزع، قد يكون تصحيح الأخطاء في Saga فاشلة عبر خمس خدمات كابوساً. وأخيراً، فكر في مهلات الوقت (Timeouts)؛ إذا أصبحت الخدمة غير مستجيبة، يجب أن يكون الـ Saga قادراً على اكتشاف ذلك وبدء التعويض بدلاً من التعليق إلى الأبد.
الخاتمة
يُعد نمط Saga حلاً متيناً لإدارة المعاملات الموزعة في بنية الخدمات المصغرة. من خلال استبدال معاملات ACID الصلبة بعمليات عمل مرنة وقابلة للتعويض، يسمح للنظم بالبقاء منفصلة بشكل فضفاض مع الحفاظ على اتساق البيانات. سواء اخترت الرقص من أجل البساطة أو التنسيق من أجل التحكم، فإن فهم نمط Saga إلزامي لأي مطور يبني أنظمة موزعة قابلة للتوسع وقوية.