في هندسة البرمجيات الحديثة، أدى التحول من التطبيقات أحادية البنية إلى الخدمات المصغرة إلى إدخال تعقيد كبير فيما يتعلق بتوافق البيانات. عندما تمتد عملية واحدة عبر خدمات متعددة، كل منها بقاعدة بيانات خاصة به، يصبح ضمان نجاح جميع الخطوات أو فشلها جميعاً تحدياً شاملاً. هذا المفهوم، المعروف باسم المعاملة الموزعة، حاسم للحفاظ على سلامة البيانات في الأنظمة المعقدة. بدون استراتيجيات مناسبة، تخاطر بتلف البيانات، أو فقدان الطلبات، أو التناقضات المالية التي يمكن أن تؤثر بشدة على العمليات التجارية.
فهم التحديات الأساسية
قبل الغوص في الحلول، من الضروري فهم سبب صعوبة المعاملات الموزعة. في قاعدة بيانات واحدة، يتم التعامل مع خصائص ACID (الأولية، الاتساق، العزل، المتانة) بسلاسة بواسطة محرك قاعدة البيانات. ومع ذلك، في بيئة موزعة، لا تأتي هذه الخصائص مجاناً. غالباً ما تضطر إلى إجراء مقايضات تفرضها نظرية CAP، التي تنص على أن النظام الموزع يمكنه ضمان خيارين فقط من ثلاثة: الاتساق، والتوفر، وتحمل التجزئة.
علاوة على ذلك، تعني زمن استجابة الشبكة وفشل العقد المحتمل أن أمر `COMMIT` البسيط لم يعد كافياً. يجب عليك تنفيذ بروتوكولات تتعامل مع الأجزاء الجزئية من الأعطال بسلاسة. إذا نجحت الخدمة A ولكن الخدمة B فشلت، فأنت بحاجة إلى آلية لإلغاء تغييرات الخدمة A، وهي عملية تُعرف باسم المعاملات التعويضية. يؤدي تجاهل هذه التحديات إلى ظهور "أنماط مضادة موزعة" حيث تصبح البيانات غير متسقة عبر حدود نظامك.
تنفيذ نمط Saga
يعد نمط Saga أحد أكثر النهج متانة للتعامل مع المعاملات الموزعة. يكسر نمط Saga معاملة كبيرة إلى تسلسل من المعاملات المحلية، كل منها يحدث قاعدة البيانات داخل خدمة واحدة. إذا فشلت خطوة ما، ينفذ نمط Saga سلسلة من المعاملات التعويضية لتراجع التغييرات التي أجرتها الخطوات السابقة. يميل هذا النهج إلى الاتساق النهائي بدلاً من الاتساق القوي، وهو ما غالباً ما يكون أكثر عملية للخدمات المصغرة عالية التوفر.
يمكن تنفيذ نمط Saga باستخدام نهج قائم على الرقص (حيث تصدر الخدمات أحداثاً وتتفاعل معها) أو نهج قائم على التنسيق (حيث يوجه منسق مركزي التدفق). عادةً ما يكون نهج التنسيق أسهل في التصحيح والصيانة. فيما يلي مثال بلغة Python يوضح نمط Saga بسيط قائم على التنسيق لعملية طلب تجارة إلكترونية:
class OrderSaga:
def __init__(self, order_service, payment_service, inventory_service):
self.order_service = order_service
self.payment_service = payment_service
self.inventory_service = inventory_service
def execute(self, order_id, user_id, items):
try:
# الخطوة 1: إنشاء الطلب
order = self.order_service.create(order_id, user_id)
# الخطوة 2: معالجة الدفع
payment_status = self.payment_service.charge(order.amount, user_id)
# الخطوة 3: حجز المخزون
self.inventory_service.reserve(items)
# إذا نجحت جميع الخطوات، يكون الطلب مكتملاً
return order
except Exception as e:
# تنفيذ المعاملات التعويضية
self._compensate(order_id, items)
raise e
def _compensate(self, order_id, items):
# عكس الخطوات بالترتيب العكسي
self.inventory_service.release(items)
self.payment_service.refund(order_id)
self.order_service.cancel(order_id)
الاتفاق ثنائي المرحلة: النهج التقليدي
بينما يشيع نمط Saga في الخدمات المصغرة، هناك سيناريوهات يكون فيها الاتساق القوي أمراً لا يقبل المساومة. بروتوكول الاتفاق ثنائي المرحلة (2PC) هو خوارزمية إجماع موزع كلاسيكية تضمن الأولية عبر عقد متعددة. في المرحلة الأولى (التحضير)، يسأل المنسق جميع المشاركين عما إذا كانوا مستعدين للإقرار. في المرحلة الثانية (الإقرار)، إذا صوت جميع المشاركين بـ "نعم"، يرسل المنسق رسالة إقرار؛ وإلا، يرسل رسالة إلغاء.
على الرغم من أن 2PC يضمن اتساقاً قوياً، إلا أنه يعاني من عيوب كبيرة. فهو متوقف (blocking)، مما يعني أنه إذا فشل المنسق أثناء المعاملة، فقد يترك المشاركين في حالة غامضة. بالإضافة إلى ذلك، فإن جولات الشبكة المتعددة تقدم زمن استجابة عالياً، مما قد يؤثر على أداء النظام. ونتيجة لذلك، نادراً ما يُستخدم 2PC في التطبيقات الحديثة الأصلية للسحابة إلا إذا كان ضرورياً بشكل صارم لأنظمة سجلات مالية أو مخازن بيانات حرجة أخرى.
// كود زائف للاتفاق ثنائي المرحلة
function twoPhaseCommit(coordinator, participants):
// المرحلة 1: التحضير
for participant in participants:
result = participant.prepare()
if result != READY:
coordinator.send(ABORT)
return
// المرحلة 2: الإقرار
coordinator.send(COMMIT)
for participant in participants:
participant.commit()
أفضل الممارسات للتنفيذ
عند تصميم نظامك، تجنب المعاملات الموزعة إلا إذا كان ذلك ضرورياً للغاية. بدلاً من ذلك، صمم خدماتك لتكون مفككة الاعتماد واعتمد على الاتساق النهائي. استخدم البنى المعتمدة على الأحداث لنشر تغييرات الحالة بشكل غير متزامن. تأكد من أن معاملاتك التعويضية قابلة للهوية (idempotent) للتعامل مع سيناريوهات إعادة المحاولة بأمان. أخيراً، قم بتنفيذ مراقبة وإنذارات قوية للكشف عن عدم الاتساق في وقت مبكر، مما يتيح لك تشغيل وظائف تسوية يدوية إذا فشلت إعادة المحاولة التلقائية. من خلال الاختيار الدقيق بين نماذج الاتساق القوي مثل 2PC والنماذج المرنة مثل Saga، يمكنك بناء أنظمة مرنة تتوسع بفعالية.