البيانات الموزّعة والاتساق
البيانات الموزّعة والاتساق
في التطبيق المتراص (Monolith) تقع كل عملية كتابة في قاعدة بيانات واحدة وتحصل على ضمانات ACID مجانًا. أما في منظومة الخدمات المصغّرة، فكل خدمة تمتلك بياناتها الخاصة (الدرس 4)، لذا قد تستلزم عملية تجارية واحدة — "تسجيل طلب" — الكتابة في ثلاث قواعد بيانات تملكها ثلاث خدمات منفصلة. لا يوجد مدير معاملات موزّعة يلفّها جميعًا. يشرح هذا الدرس نموذج الاتساق الذي يجب أن تصمّم وفقه، ويعرّف بنمط Saga — الأداة الأكثر عملية لتنسيق عمليات الكتابة متعددة الخدمات بشكل موثوق.
لماذا لا تتوسّع المعاملات الموزّعة
الحل الكلاسيكي لعمليات الكتابة عبر قواعد البيانات المتعددة هو بروتوكول الإلزام ثنائي الطور (2PC): ينظّم منسّق طلبًا لجميع المشاركين للتهيئة، وينتظر أصواتهم، ثم يصدر أمر إلزام (commit) أو تراجع (rollback) شامل. يعمل هذا لكنّه ينطوي على تكاليف جسيمة في سياق الخدمات المصغّرة:
- أقفال محجوبة: بين مرحلتَي التهيئة والإلزام يحتجز كل مشارك أقفالًا على قاعدة البيانات، ما يُحوّلها إلى عنق زجاجة في أداء الإنتاجية عند التزامن العالي.
- المنسّق نقطة فشل وحيدة: إذا تعطّل المنسّق بعد التهيئة وقبل الإلزام، يظل المشاركون في حالة غير محدّدة إلى أجل غير مسمى.
- اقتران محكم: يجب أن تكون جميع الأطراف المشاركة متاحة في اللحظة ذاتها، وهذا يتعارض مع هدف القابلية للنشر المستقل في الخدمات المصغّرة.
- دعم البروتوكول: كثير من مخازن البيانات الحديثة (NoSQL وقواعد البيانات السحابية المُدارة) لا تنفّذ XA / JTA إطلاقًا.
الاتساق التدريجي
الاتساق التدريجي ضمان حيوية: إذا لم تصل كتابات جديدة، ستتقارب في نهاية المطاف جميع النسخ المتماثلة والخدمات إلى القيمة ذاتها. لا يعني ذلك أن النظام خاطئ أو فوضوي دومًا — بل يعني وجود نافذة قصيرة (ميلي ثانية إلى ثوانٍ عمليًا) قد ترى فيها خدمات مختلفة قيمًا مختلفة.
يتطلب التصميم للاتساق التدريجي التفكير في ثلاثة أمور:
- ما أسوأ نافذة للبيانات القديمة؟ (ثوانٍ، دقائق؟) وهل هذا مقبول لهذه العملية؟
- ماذا يحدث إذا فشلت خطوة في منتصف عملية كتابة متعددة الخدمات؟ كيف تعوّض؟
- كيف تتعامل واجهة المستخدم مع حالة غير متسقة مؤقتًا؟ (مثلًا عرض "طلب قيد التنفيذ" بدلًا من حالة نهائية)
نمط Saga
Saga عبارة عن تسلسل من المعاملات المحلية، معاملة واحدة لكل خدمة مشاركة. كل معاملة محلية تُحدّث قاعدة بياناتها الخاصة ثم تنشر حدثًا (أو ترسل أمرًا) لتشغيل الخطوة التالية. إذا فشلت أي خطوة، ينفّذ Saga معاملات تعويضية بترتيب عكسي للتراجع عن الخطوات المنجزة.
تخيّل الأمر باعتباره استبدال معاملة ACID ضخمة واحدة بسلسلة من المعاملات الصغيرة، مع مسار تراجع محدد لكل خطوة.
أسلوبا تنسيق Saga
هناك طريقتان لتنظيم التدفق:
- الكوريوغرافيا (Choreography): كل خدمة تستمع للأحداث وتتفاعل باستقلالية. لا منسّق مركزي. تتوسّع جيدًا لكنها تصعب تتبّعها؛ يجب عليك تتبّع السجلات عبر خدمات متعددة لفهم ما جرى.
- التوزيع المنسّق (Orchestration): خدمة منسّق Saga مخصّصة (خدمة Spring Boot أو Spring State Machine) تُخبر كل مشارك صراحةً بما يجب فعله لاحقًا. أسهل في التفكير والمراقبة؛ المنسّق نقطة اقتران محدودة لكنه ليس نقطة فشل وحيدة لأنه عديم الحالة أو يخزّن حالته في قاعدة بياناته الخاصة.
Saga لتسجيل الطلب — مثال بالكوريوغرافيا
السيناريو: تسجيل طلب يستلزم (1) حجز المخزون، (2) تحصيل الدفع، (3) إنشاء سجل شحن. تنشر كل خدمة أحداث تطبيق Spring (أو مواضيع Kafka في الإنتاج).
@TransactionalEventListener(phase = AFTER_COMMIT) وليس @EventListener عند نشر أحداث Saga. إذا نشرت داخل المعاملة ذاتها وتراجعت تلك المعاملة لاحقًا، سيُطلق الحدث بينما لم تُنجز عملية الكتابة في قاعدة البيانات — مما يجعل الخدمات التالية تتصرف بناءً على بيانات وهمية. يضمن AFTER_COMMIT أن الكتابة المحلية دائمة قبل مغادرة الحدث للعملية.
الاستقلالية (Idempotency) — المتطلب الخفي
في الأنظمة الموزّعة، يمكن تسليم الرسائل أكثر من مرة (إعادة محاولات الشبكة، وإعادة تسليم الوسيط). يجب أن يكون كل معالج خطوة في Saga استقلاليًا: معالجة الحدث ذاته مرتين يجب أن ينتج النتيجة ذاتها كمعالجته مرة واحدة.
نمط Outbox — ضمان تسليم الأحداث
إذا كتبت خدمة إلى قاعدة بياناتها ثم نشرت إلى وسيط رسائل في عمليتين منفصلتين، فإن أي تعطّل بينهما يترك قاعدة البيانات محدَّثة لكن الحدث لم يُرسل قط. يحل نمط Transactional Outbox هذه المشكلة: اكتب الحدث إلى جدول outbox في المعاملة المحلية ذاتها مع الكتابة التجارية، ثم اجعل عملية تتابع منفصلة (مثل Debezium CDC أو مستطلع @Scheduled من Spring) تُحيل صفوف Outbox إلى الوسيط وتحذفها.
مستويات الاتساق عمليًا
- اقرأ كتاباتك الخاصة: وجّه قراءات المستخدم إلى الخدمة التي معالجت كتابته للتو (التوجيه اللاصق أو رموز الإصدار) حتى لا يرى بيانات قديمة بشأن إجراءاته.
- القراءات الرتيبة: بمجرد أن يرى العميل قيمة، يجب ألا تُعيد القراءات اللاحقة قيمة أقدم. يُحقَّق ذلك بأرقام الإصدار أو الطوابع الزمنية في الردود.
- الاتساق السببي: إذا تسبّب الحدث A في الحدث B، فأي خدمة ترى B يجب أن تكون قد رأت A. يوفّر نمط Saga والترتيب في قسيمة Kafka هذا الضمان ضمن تدفق طلب واحد.
الخلاصة
تتبادل إدارة البيانات الموزّعة ضمانات ACID مقابل التوافر والقابلية للنشر المستقل. الاتساق التدريجي هو القاعدة؛ مهمتك تقليص النافذة التدريجية وإظهار مسارات الفشل بوضوح. نمط Saga — سواء بالكوريوغرافيا أو التنسيق المنسّق — يستبدل المعاملة الموزّعة بسلسلة من المعاملات المحلية مضافًا إليها إجراءات تعويضية. أضف إليها حراس الاستقلالية ونمط Outbox لتجعل تسليم الأحداث موثوقًا، وستكون قد وضعت أسس طبقة بيانات تتحمّل الأعطال الجزئية بسلاسة.