نمط الملحمة: المعاملات الموزعة في هندسة الخدمات الصغيرة
في التطبيقات المتجانسة التقليدية، يعد الحفاظ على تناسق البيانات عبر كيانات متعددة أمرًا بسيطًا. توفر محركات قواعد البيانات العلائقية ضمانات ACID (الذرية والاتساق والعزل والمتانة) داخل معاملات SQL المحلية. إذا فشل تقديم الطلب أو خصم الدفع أو احتياطي المخزون في منتصف الطريق، فإن استدعاء ROLLBACK يعيد كل تعديل لقاعدة البيانات على الفور.
ومع ذلك، عند الانتقال إلى بنية الخدمات الصغيرة الحديثة، تتغير إدارة البيانات بشكل أساسي. لضمان استقلالية المجال وقابلية التوسع المستقلة، تمتلك كل خدمة صغيرة قاعدة بياناتها الخاصة. تمتد الآن عملية تجارية واحدة - مثل معالجة عملية دفع التجارة الإلكترونية - عبر حدود الخدمة ومحركات قواعد البيانات المتعددة (على سبيل المثال، PostgreSQL for Orders، وDynamoDB for Payments، وRedis for Inventory).
نظرًا لأن الخدمات الصغيرة الموزعة لا يمكنها الاعتماد على معاملة قاعدة بيانات واحدة، فإن الحفاظ على اتساق البيانات عبر حدود الشبكة يصبح أحد أكثر المشكلات صعوبة في هندسة الأنظمة الموزعة.
لحل هذه المشكلة دون التضحية بتوفر النظام أو أدائه، يعتمد مهندسو البرمجيات على Saga Pattern.
في هذا الغوص العميق، سوف نستكشف سبب فشل المعاملات الموزعة التقليدية، وتفكيك الآليات الأساسية لنمط Saga، ومقارنة تصميم الرقصات مقابل التنسيق، وتحليل إجراءات العزل المضادة، وفحص تطبيقات كود الإنتاج في Go وJava، ومعرفة كيفية التعامل مع عمليات التراجع عن الفشل في العالم الحقيقي بأمان.
المشكلة الجذرية: لماذا يفشل الالتزام على مرحلتين (2PC) في الخدمات الصغيرة
قبل اعتماد نموذج Saga، غالبًا ما يتساءل المهندسون: لماذا لا يمكننا استخدام الالتزام التقليدي ثنائي المرحلتين (2PC / XA) عبر خدماتنا الصغيرة؟
ميكانيكا 2PC
يقوم الالتزام على مرحلتين بتنسيق معاملة موزعة عبر عقد قاعدة بيانات متعددة باستخدام مدير المعاملات المركزي على مرحلتين:
- مرحلة الإعداد: يطلب مدير المعاملات من جميع عقد قاعدة البيانات المشاركة إعداد الصفوف المطلوبة وقفلها. يصوت المشاركون على
YESأوNO. - مرحلة الالتزام: إذا صوت جميع المشاركين على
YES، يصدر المدير أمرCOMMITلجميع العقد. إذا صوتت أي عقدة علىNOأو انتهت المهلة، فإنها تصدرROLLBACK.
Client ----> Transaction Manager
|
+-------------+-------------+
| (Prepare) | (Prepare) | (Prepare)
v v v
Order DB Payment DB Inventory DB
لماذا يعد جهاز 2PC نموذجًا مضادًا للخدمات الصغيرة
على الرغم من أن 2PC تضمن اتساقًا قويًا، إلا أنها تتعطل في بيئات الخدمات الصغيرة السحابية الأصلية بسبب العديد من العيوب الهيكلية:
- الحظر والتنافس على الموارد: تظل صفوف قاعدة البيانات مقفلة طوال عملية تأكيد اتصال الشبكة متعددة المراحل. إذا زاد زمن استجابة الشبكة أو تباطأت الخدمة، فسيتم إبقاء الأقفال مفتوحة، مما يستهلك تجمعات الاتصال بسرعة ويتسبب في فشل النظام المتتالي.
- عنق الزجاجة في التوفر: في جهازي كمبيوتر، يكون توفر النظام مقيدًا بـ المنتج الخاص بجميع توفر المشاركين ($A_{total} = A_1 \times A_2 \times \dots \times A_n$). إذا انقطع اتصال أي خدمة أو عقدة قاعدة بيانات واحدة أثناء مرحلة الإعداد، فسيتم حظر المعاملة العالمية بأكملها إلى أجل غير مسمى.
- فقدان استقلالية الخدمة: تجبر 2PC الخدمات على الكشف عن بروتوكولات XA على مستوى قاعدة البيانات عبر واجهات برمجة تطبيقات الشبكة، مما يؤدي إلى اقتران محركات قاعدة البيانات الخاصة بها مباشرةً.
- قيود الوسيط: لا يشارك وسطاء الرسائل عالية الإنتاجية مثل Apache Kafka أو RabbitMQ بشكل أصلي في معاملات XA 2PC التقليدية عبر قواعد البيانات العلائقية.
وفقًا نظرية CAP، يجب أن تختار الأنظمة الموزعة بين الاتساق القوي (C) والتوافر العالي (A) ضمن أقسام الشبكة (P). تعطي أنظمة الخدمات الصغيرة الحديثة الأولوية للتوفر والتسامح مع الأقسام، وتداول الاتساق الفوري من أجل الاتساق النهائي (القاعدة: متوفرة بشكل أساسي، الحالة الناعمة، الاتساق النهائي).
ما هو نمط الملحمة؟
تم اقتراح نموذج الملحمة في الأصل من قبل هيكتور جارسيا مولينا وكينيث سالم في عام 1987 كآلية للتعامل مع المعاملات طويلة الأمد في أنظمة إدارة قواعد البيانات. في الخدمات الصغيرة الحديثة، تمثل Saga سلسلة من المعاملات المحلية المنفصلة.
بدلاً من تغليف تدفق الخدمات المتعددة بالكامل داخل قفل عالمي واحد، تقوم Saga بتنفيذ عملية تجارية كسلسلة من معاملات قاعدة البيانات المحلية المستقلة ($T_1, T_2, \dots, T_n$).
تقوم كل معاملة محلية بتحديث البيانات في قاعدة بيانات خدمة صغيرة واحدة وتنشر حدث أو رسالة مجال. يؤدي هذا الحدث إلى تشغيل المعاملة المحلية التالية ($T_{i+1}$) في الخدمة النهائية.
[Order Service] [Payment Service] [Inventory Service]
Local Tx T1 -------------> Local Tx T2 -------------> Local Tx T3
(Create Order) (Process Payment) (Reserve Stock)
التنفيذ الآجل مقابل التعويض الخلفي
إذا نجحت جميع المعاملات المحلية، فستكتمل الملحمة بنجاح (التنفيذ الآجل).
ومع ذلك، إذا فشلت معاملة محلية في منتصف الطريق (على سبيل المثال، فشل $T_3$ لأن أحد العناصر غير متوفر)، فلا يمكن لـ Saga ببساطة استدعاء قاعدة بيانات ROLLBACK للخطوات السابقة ($T_1، T_2$) لأن تلك المعاملات المحلية قد التزمت بالفعل بقواعد البيانات الخاصة بها.
للتراجع عن المعاملات المحلية التي تم الالتزام بها بالفعل، يجب على Saga تنفيذ المعاملات التعويضية ($C_{n-1}, \dots, C_1$) بترتيب عكسي.
Forward Flow: T1 (Create Order) ---> T2 (Charge Card) ---> T3 (Reserve Inventory - FAILS)
|
Backward Rollback: C1 (Cancel Order) <--- C2 (Refund Card) <----------+
تصنيف معاملات الملحمة
لتصميم سير عمل Saga قوي، يجب تصنيف كل خطوة في تسلسل المعاملات إلى واحد من ثلاثة أنواع هيكلية:
| نوع الصفقة | الوصف | متطلبات العجز والتراجع |
|---|---|---|
| ** المعاملات القابلة للتعويض ** | الخطوات التي تم تنفيذها قبل نقطة اللاعودة. يمكن التراجع عنها أو عكسها في حالة فشل الخطوة النهائية. | يجب أن يكون لديك معاملة تعويضية مقابلة ($C_i$). |
| المعاملة المحورية | الخطوة النهائية في الملحمة. إذا نجح المحور، فسيتم ضمان انتهاء الملحمة. إذا فشلت، تتراجع الملحمة. | غير قابلة للتعويض ولا لإعادة المحاكمة؛ فهو يمثل الحد الفاصل بين التراجع والإكمال الأمامي. |
| ** المعاملات القابلة للإعادة ** | الخطوات التي يتم تنفيذها بعد المعاملة المحورية. إنهم مضمونون بالنجاح في النهاية ولا يحتاجون إلى تعويض. | يجب أن يكون عاجزًا تمامًا، حيث ستتم إعادة المحاولة تلقائيًا حتى تنجح. |
مثال على الخروج من التجارة الإلكترونية
فكر في عملية دفع طلب التجارة الإلكترونية والتي تتكون من أربع خطوات:
- $T_1$: إنشاء أمر معلق (قابل للتعويض) $\to$ تراجع عبر $C_1$: إلغاء الطلب.
- $T_2$: تفويض الدفع (قابل للتعويض) $\to$ تراجع عبر $C_2$: دفعة استرداد الأموال.
- $T_3$: المخزون الاحتياطي (معاملة محورية) $\to$ إذا نجح تخصيص المخزون، فسيتم الانتهاء من الطلب. إذا فشلت، قم بتشغيل $C_2$ و$C_1$.
- $T_4$: إرسال طلب الشحن (قابل لإعادة التسجيل) $\to$ يتم تنفيذه بعد المحور؛ إعادة المحاولة حتى تسليمها.
أنماط الملحمة المعمارية: تصميم الرقصات مقابل التنسيق
هناك نوعان من الأساليب المعمارية الأساسية لتنفيذ نمط Saga في الأنظمة الموزعة: تصميم الرقصات (لامركزي) و التنسيق (مركزي).
النمط 1: تصميم الرقصات (اللامركزية القائمة على الأحداث)
في الملحمة المبنية على تصميم الرقصات، لا يوجد وحدة تحكم أو منسق مركزي. وبدلاً من ذلك، تتواصل الخدمات الصغيرة بشكل غير متزامن من خلال الاستماع إلى أحداث المجال المنشورة على ناقل الأحداث المركزي (مثل Apache Kafka أو NATS أو RabbitMQ).
ميكانيكا سير العمل
- خدمة الطلب تنفذ $T_1$ (تنشئ طلبًا معلقًا) وترسل حدث
OrderCreatedإلى كافكا. - خدمة الدفع تستمع إلى
OrderCreated، وتنفذ $T_2$ (تشحن بطاقة الائتمان)، وترسل حدثPaymentCompleted(أوPaymentFailed). - خدمة المخزون تستمع إلى
PaymentCompletedوتنفذ $T_3$ (العناصر الاحتياطية). إذا كانت العناصر غير متوفرة في المخزون، فسيتم إصدارInventoryReservationFailed. - خدمة الدفع تستمع إلى
InventoryReservationFailedوتنفذ $C_2$ (تصدر رد الأموال). - خدمة الطلب تستمع إلى
PaymentRefundedوتنفذ $C_1$ (تضع علامة على الطلب كملغى).
مزايا الكوريغرافيا
- الاقتران غير المحكم: تشترك الخدمات في موضوعات الأحداث فقط؛ لا يعرفون عن تطبيقات الخدمة الأخرى.
- الإنتاجية العالية واللامركزية: تعمل حانة/فرعية الحدث المباشر على التخلص من اختناقات المنسق المركزي.
- البساطة لسير العمل القصير: من السهل الإعداد لسير العمل البسيط المكون من 2-3 خطوات.
عيوب الكوريغرافيا
- مخاطر التبعية الدورية: قد ينتهي الأمر بالخدمات إلى الاستماع إلى أحداث بعضها البعض، مما يؤدي إلى إنشاء تبعيات دورية معقدة.
- ** صعوبة تتبع التعليمات البرمجية **: يتطلب فهم عملية الأعمال الشاملة منطق التتبع عبر مستودعات قاعدة التعليمات البرمجية المتعددة.
- تعقيد الأحداث وتعقيدها: مع زيادة عدد الخطوات (على سبيل المثال، أكثر من 10 خدمات)، تؤدي إدارة حالات حافة الخطأ إلى انفجار حالة الحدث.
النمط 2: التنسيق (إدارة سير العمل المركزية)
في Saga المستندة إلى Orchestration، تتحكم خدمة صغيرة مخصصة تُعرف باسم Saga Orchestrator في دورة الحياة الكاملة للمعاملة الموزعة. يعمل المنسق كمنسق مركزي، ويصدر أوامر واضحة للخدمات الصغيرة المشاركة ويستمع إلى أحداث الاستجابة الخاصة بهم.
ميكانيكا سير العمل
- يرسل العميل طلب طلب إلى Saga Orchestrator.
- يرسل المنسق أمر
CreateOrderإلى طلب الخدمة. ترجع خدمة الطلبOrderCreated. - يقوم المُنسق بتحديث جهاز الحالة الخاص به ويرسل أمر
ProcessPaymentإلى خدمة الدفع. تقوم خدمة الدفع بإرجاعPaymentSuccessful. - يرسل المنسق أمر
ReserveInventoryإلى خدمة المخزون. تقوم خدمة المخزون بإرجاعInventoryFailed (Out of Stock). - يكتشف المنسق الفشل، ويبدأ تدفق التعويض:
- إرسال الأمر
RefundPaymentإلى خدمة الدفع. - يرسل الأمر
CancelOrderإلى طلب الخدمة.
- إرسال الأمر
- يقوم المنسق بوضع علامة على تنفيذ Saga على أنه
FAILED.
مزايا التنسيق
- منطق الأعمال المركزي: تتم ترجمة حالة سير العمل ومنطق الأعمال في خدمة منسقة واحدة أو جهاز حالة.
- لا توجد تبعيات دورية: تستجيب الخدمات الصغيرة لأوامر المنسق؛ ولا يعتمدون على خدمات المصب الأخرى أو يعرفون عنها.
- مراقبة واضحة وتصحيح الأخطاء: يتم تخزين حالة المعاملة من طرف إلى طرف بشكل صريح في مخزن حالة المنسق (على سبيل المثال، PostgreSQL أو محركات سير العمل مثل Temporal / Camunda).
- معالجة أسهل للأخطاء: تتم إدارة إضافة خطوات جديدة أو تغيير قواعد التراجع بالكامل داخل المنسق.
مساوئ التنسيق
- تعقيد المنسق: خطر وضع قدر كبير جدًا من منطق النطاق في المنسق، وتحويله إلى نمط مضاد “منسق ذكي، خدمة غبية”.
- نقطة الفشل الفردية المحتملة: يجب أن يكون المنسق متاحًا بدرجة عالية وذو حالة.
المصفوفة المقارنة: تصميم الرقصات مقابل التوزيع الموسيقي
| ميزة | الكوريغرافيا | تنسيق |
|---|---|---|
| هيكل التحكم | لامركزية (حدث Pub/Sub) | مركزية (منسق الملحمة / آلة الدولة) |
| ** اقتران ** | منخفض للغاية (الخدمات تستهلك الأحداث) | متوسط (الخدمات تقبل الأوامر من المنسق) |
| رؤية العملية | منخفض (موزع عبر ملفات السجل) | عالي (مخزن الحالة الواحدة يتصور سير العمل) |
| ** الأنسب لـ ** | سير عمل بسيط (من 2 إلى 4 خطوات خدمة) | سير عمل المؤسسة المعقدة (أكثر من 5 خطوات، منطق التفرع) |
| ** الأدوات / الأطر ** | كافكا، رابيتMQ، NATS، AWS EventBridge | Temporal.io، AWS Step Functions، Camunda، Axon |
تحديات العزل والتدابير المضادة (التعامل مع “ACID ناقص I”)
نظرًا لأن المعاملات المحلية في Saga تلتزم فورًا بقواعد البيانات المحلية الخاصة بها، فإن نمط Saga يفتقر إلى العزل (I) من ضمانات ACID التقليدية.
إذا قرأ العميل صف قاعدة بيانات تم تعديله بواسطة $T_1$ بينما لا تزال Saga قيد التشغيل، فإنه يقرأ حالة متوسطة غير ملتزم بها. إذا فشلت خطوة المصب وتسببت في التعويض ($C_1$)، يكون العميل قد أجرى قراءة سيئة.
الحالات الشاذة الشائعة الناجمة عن عدم العزلة
- ** التحديثات المفقودة **: تقوم Saga A بتحديث السجل. قبل اكتمال Saga A، تقوم Saga B بالكتابة فوق نفس السجل. إذا فشل Saga A ونفذ التعويض، فإنه يحل محل تحديث Saga B.
- القراءات المتسخة: يقرأ العميل المخزون المتاح المحدث بواسطة Saga A ($T_1$). تفشل Saga A في اتجاه مجرى النهر ($T_3$) وتستعيد المخزون ($C_1$)، ولكن العميل قد قدم طلبًا بالفعل بناءً على بيانات قديمة.
- القراءات غير القابلة للتكرار: تقرأ الخدمة البيانات في الخطوة $T_1$ وتقرأها مرة أخرى في الخطوة $T_3$، ولكن قامت Saga متزامنة أخرى بتعديل البيانات بينهما.
التدابير المضادة واستراتيجيات التخفيف
للحفاظ على سلامة البيانات على الرغم من عدم وجود العزلة، يقوم مهندسو البرمجيات بتنفيذ أنماط تصميم عزل محددة:
1. القفل الدلالي (الحالة المعلقة / ذات العلامة)
عندما تقوم المعاملة المحلية $T_1$ بتحديث سجل قاعدة البيانات، فإنها تقوم بتعيين حقل الحالة إلى PENDING أو APPROVAL_REQUIRED (على سبيل المثال، ORDER_PENDING_PAYMENT).
يجب على الملحمات المتزامنة الأخرى التي تقرأ هذا السجل التحقق من علامة القفل الدلالي وحظر سلوكها أو تغييره حتى تتغير الحالة إلى COMMITTED أو CANCELLED.
2. تنفيذ الأمر
صمم تسلسل المعاملات المحلية بحيث تحدث العمليات عالية المخاطر أو التي لا رجعة فيها في وقت متأخر من تنفيذ Saga، مما يقلل من نافذة الضعف.
3. إعادة التحقق من صحة القراءة (التحكم المتفائل في التزامن)
قبل تنفيذ خطوة مهمة أو تعويض، أعد قراءة سجل قاعدة البيانات الهدف وتحقق من الطوابع الزمنية للإصدار (version_id) لضمان عدم حدوث أي تعديل متزامن.
4. النظرة المتشائمة
أعد ترتيب خطوات الملحمة لتقليل التعرض الاقتصادي (على سبيل المثال، وضع تفويض الدفع في أقرب مكان ممكن من المعاملة المحورية).
تدفق تسلسل الإنتاج: المعاملات التعويضية
يوجد أدناه مخطط تدفق التسلسل الكامل الذي يوضح فشل التنفيذ الأمامي وتنفيذ التعويض الخلفي الناتج:
عمليات تنفيذ التعليمات البرمجية
دعنا نستكشف أمثلة التنفيذ الجاهزة للإنتاج لكل من تصميم الرقصات (في Go) والتنسيق (في Java Spring Boot).
التنفيذ 1: الملحمة القائمة على تصميم الرقصات في Go
في مثال Go هذا، نعرض خدمة الطلب التعامل مع إنشاء الطلب والاستماع إلى أحداث فشل الدفع عبر وسيط الأحداث لتنفيذ منطق التراجع التعويضي.
package saga
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
)
// Event definitions
type OrderCreatedEvent struct {
OrderID string `json:"order_id"`
CustomerID string `json:"customer_id"`
Amount float64 `json:"amount"`
}
type PaymentFailedEvent struct {
OrderID string `json:"order_id"`
Reason string `json:"reason"`
}
// OrderRepository handles local DB operations
type OrderRepository interface {
CreateOrder(ctx context.Context, orderID string, amount float64) error
UpdateOrderStatus(ctx context.Context, orderID string, status string) error
}
// EventBus abstraction for message broker (e.g., Kafka / NATS)
type EventBus interface {
Publish(topic string, payload []byte) error
Subscribe(topic string, handler func(payload []byte)) error
}
type OrderSagaChoreographer struct {
repo OrderRepository
eventBus EventBus
}
func NewOrderSagaChoreographer(repo OrderRepository, bus EventBus) *OrderSagaChoreographer {
c := &OrderSagaChoreographer{repo: repo, eventBus: bus}
c.registerSubscriptions()
return c
}
// Step 1: Forward Transaction (T1)
func (s *OrderSagaChoreographer) StartOrderSaga(ctx context.Context, orderID, customerID string, amount float64) error {
// Execute local database transaction
err := s.repo.CreateOrder(ctx, orderID, amount)
if err != nil {
return fmt.Errorf("failed local DB transaction T1: %w", err)
}
// Emit domain event for downstream Payment Service
event := OrderCreatedEvent{OrderID: orderID, CustomerID: customerID, Amount: amount}
bytes, _ := json.Marshal(event)
log.Printf("[SAGA][T1] Order %s created. Publishing OrderCreatedEvent...", orderID)
return s.eventBus.Publish("orders.created", bytes)
}
// Register subscription for compensating events
func (s *OrderSagaChoreographer) registerSubscriptions() {
_ = s.eventBus.Subscribe("payments.failed", func(payload []byte) {
var event PaymentFailedEvent
if err := json.Unmarshal(payload, &event); err != nil {
log.Printf("[ERROR] Corrupt payment event: %v", err)
return
}
// Execute Compensating Transaction (C1)
s.handlePaymentFailed(context.Background(), event)
})
}
// Step C1: Compensating Transaction
func (s *OrderSagaChoreographer) handlePaymentFailed(ctx context.Context, event PaymentFailedEvent) {
log.Printf("[SAGA][C1] Payment failed for Order %s (Reason: %s). Rolling back local order...", event.OrderID, event.Reason)
// Revert order status to CANCELLED in local DB
err := s.repo.UpdateOrderStatus(ctx, event.OrderID, "CANCELLED_PAYMENT_FAILED")
if err != nil {
log.Printf("[CRITICAL] Failed to execute compensating transaction C1 for Order %s: %v", event.OrderID, err)
// Trigger alert or write to Dead Letter Queue (DLQ)
return
}
log.Printf("[SAGA][SUCCESS] Order %s successfully compensated and cancelled.", event.OrderID)
}
التنفيذ 2: الملحمة القائمة على التنسيق في Java (التمهيد الربيعي)
في مثال Java هذا، قمنا ببناء Saga Orchestrator باستخدام نمط آلة الحالة لتنسيق الأوامر الأمامية وتنفيذ عمليات التراجع التعويضية عند فشل خطوة المصب.
package com.ghaznix.saga.orchestrator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.UUID;
public enum SagaState {
STARTED,
ORDER_CREATED,
PAYMENT_PROCESSED,
INVENTORY_RESERVED,
COMPLETED,
COMPENSATING_PAYMENT,
COMPENSATING_ORDER,
FAILED
}
@Service
public class OrderSagaOrchestrator {
private static final Logger log = LoggerFactory.getLogger(OrderSagaOrchestrator.class);
private final OrderServiceClient orderClient;
private final PaymentServiceClient paymentClient;
private final InventoryServiceClient inventoryClient;
public OrderSagaOrchestrator(OrderServiceClient orderClient,
PaymentServiceClient paymentClient,
InventoryServiceClient inventoryClient) {
this.orderClient = orderClient;
this.paymentClient = paymentClient;
this.inventoryClient = inventoryClient;
}
public boolean executeOrderSaga(String customerId, String productId, double amount, int quantity) {
String sagaId = UUID.randomUUID().toString();
log.info("[SAGA {}] Starting Order Saga Workflow...", sagaId);
SagaState currentState = SagaState.STARTED;
String orderId = null;
String paymentId = null;
try {
// Step 1: Forward Local Tx T1 - Create Order
log.info("[SAGA {}][T1] Sending CreateOrder command...", sagaId);
orderId = orderClient.createOrder(customerId, productId, amount);
currentState = SagaState.ORDER_CREATED;
// Step 2: Forward Local Tx T2 - Process Payment
log.info("[SAGA {}][T2] Sending ProcessPayment command for Order {}...", sagaId, orderId);
paymentId = paymentClient.chargePayment(orderId, customerId, amount);
currentState = SagaState.PAYMENT_PROCESSED;
// Step 3: Forward Local Tx T3 (Pivot Step) - Reserve Inventory
log.info("[SAGA {}][T3] Sending ReserveInventory command for Product {}...", sagaId, productId);
boolean stockReserved = inventoryClient.reserveStock(productId, quantity);
if (!stockReserved) {
throw new InventoryAllocationException("Stock allocation failed: Item out of stock.");
}
currentState = SagaState.INVENTORY_RESERVED;
log.info("[SAGA {}][SUCCESS] Saga completed successfully!", sagaId);
return true;
} catch (Exception ex) {
log.error("[SAGA {}][FAILURE] Step failed during state {}. Triggering Compensation...", sagaId, currentState, ex);
rollbackSaga(sagaId, currentState, orderId, paymentId);
return false;
}
}
private void rollbackSaga(String sagaId, SagaState failedState, String orderId, String paymentId) {
log.info("[SAGA {}] Initiating backward compensating transactions from state: {}", sagaId, failedState);
// Compensate Step 2 if payment was processed
if (failedState == SagaState.PAYMENT_PROCESSED || failedState == SagaState.INVENTORY_RESERVED) {
try {
log.info("[SAGA {}][C2] Executing Payment Refund compensation for Payment {}...", sagaId, paymentId);
paymentClient.refundPayment(paymentId);
} catch (Exception e) {
log.error("[CRITICAL][SAGA {}] Payment refund C2 failed! Manual intervention or DLQ required.", sagaId, e);
}
}
// Compensate Step 1 if order was created
if (failedState != SagaState.STARTED && orderId != null) {
try {
log.info("[SAGA {}][C1] Executing Order Cancel compensation for Order {}...", sagaId, orderId);
orderClient.cancelOrder(orderId);
} catch (Exception e) {
log.error("[CRITICAL][SAGA {}] Order cancellation C1 failed!", sagaId, e);
}
}
log.info("[SAGA {}] Saga compensation workflow finished. Final State: FAILED.", sagaId);
}
}
أساسيات الإنتاج: إقران الملحمة بنمط صندوق الصادر للمعاملات
في كل من تصميم الرقصات والتنسيق، يتطلب تنفيذ معاملة محلية ($T_i$) نشر حدث مجال أو أمر عبر الشبكة.
إذا قامت خدمتك بتحديث قاعدة بيانات SQL الخاصة بها ثم نشرت رسالة إلى Kafka، فسيتسبب خلل في الشبكة بعد التزام قاعدة البيانات في فقدان الحدث الصامت. وعلى العكس من ذلك، يؤدي نشر الرسالة قبل الالتزام بقاعدة البيانات إلى معالجة الأحداث الوهمية.
لحل هذه المشكلة، يجب إقران Sagas بـ نمط صندوق الصادر للمعاملات:
[Service Database Transaction Boundary]
+-----------------------------------------------------+
| 1. INSERT INTO business_table (orders/payments) |
| 2. INSERT INTO outbox_table (event_payload) |
+-----------------------------------------------------+
|
(CDC / Polling Message Relay)
|
v
[Message Broker / Kafka]
من خلال استمرار حمولة الحدث في جدول outbox داخل نفس معاملة قاعدة البيانات المحلية، يتم ضمان الذرية. تقرأ عملية الخلفية غير المتزامنة (مثل Debezium أو تتابع الاستقصاء) من جدول صندوق الصادر وتنشر الأحداث إلى Kafka بشكل موثوق.
علاوة على ذلك، يجب على كل مستهلك للخدمة النهائية تنفيذ Idempotency (باستخدام idempotency_key الفريد أو رؤوس إلغاء البيانات المكررة للرسائل) بحيث لا يؤدي تسليم الرسائل المكررة أثناء إعادة المحاولة إلى فرض رسوم مكررة أو تخصيص المخزون.
قائمة مراجعة القرار المعماري
استخدم مصفوفة القرار العملية هذه عند تصميم المعاملات الموزعة لتطبيقات الخدمات الصغيرة:
Do you need cross-service data consistency?
|
+----------------+----------------+
| No | Yes
v v
Standard Single Service Can you accept Eventual
Local Database Consistency (BASE)?
|
+----------------+----------------+
| No | Yes
v v
Use Monolithic Core Adopt Saga Pattern
with Single ACID DB |
|
How complex is the workflow?
|
+-----------------+-----------------+
| Simple (2-3 steps) | Complex (4+ steps/branches)
v v
Choreography Saga Orchestration Saga
(Event-Driven Bus) (Temporal/Custom State Machine)
الاستنتاج والوجبات السريعة الرئيسية
يعد Saga Pattern نمطًا معماريًا أساسيًا لإدارة المعاملات الموزعة عبر حدود الخدمات الصغيرة دون قفل الموارد أو التضحية بتوفر النظام.
قائمة المراجعة الموجزة:
- التخلي عن 2PC/XA في الخدمات الصغيرة السحابية الأصلية: يؤدي الالتزام على مرحلتين إلى إقفال محكم وزمن استجابة مرتفع واختناقات شديدة في التوفر.
- تقسيم المعاملات إلى خطوات محلية: قسّم العمليات العالمية إلى معاملات محلية ($T_1 \dots T_n$) مقترنة بمعاملات تعويضية عكسية ($C_1 \dots C_{n-1}$).
- اختيار النمط المعماري المناسب:
- استخدم تصميم الرقصات لتدفقات بسيطة تعتمد على الأحداث مكونة من 2-3 خطوات مع اقتران فضفاض.
- استخدم التنسيق لعمليات سير عمل الأعمال المعقدة التي تتطلب رؤية مركزية وتفرعًا وتتبعًا لحالة الآلة.
- تنفيذ إجراءات العزل المضادة: الحماية من القراءات المتسخة والتحديثات المفقودة باستخدام الأقفال الدلالية (علامات
PENDING) وقفل إعادة القراءة المتفائل. - ضمان المراسلة الموثوقة: قم دائمًا بإقران تطبيقات Saga مع نمط صندوق الصادر للمعاملات وفرض المستهلكين غير القادرين للتعامل مع عمليات إعادة المحاولة بأمان.
من خلال تنفيذ نمط Saga بشكل مدروس، يمكنك إنشاء خدمات صغيرة متاحة للغاية وقابلة للتطوير وتظل مرنة ومتسقة حتى عند فشل الشبكات.