نمط Saga في المعاملات الموزعة - مع أمثلة بلغة Go
المعاملات في الميكروسيرفيسات باستخدام نمط ساجا
أنماط Saga تقدم حلًا أنيقًا من خلال تقسيم المعاملات الموزعة إلى سلسلة من المعاملات المحلية مع إجراءات تعويضية.
بدلاً من الاعتماد على قفل موزع يمكن أن يمنع العمليات عبر الخدمات، تتيح Saga الاتساق النهائي من خلال سلسلة من الخطوات القابلة للعكس، مما يجعلها مناسبة للعمليات التجارية طويلة المدى.
في بنى الميكروخدمات، الحفاظ على اتساق البيانات عبر الخدمات هو أحد أكثر المشكلات تحديًا. لا تعمل المعاملات التقليدية ACID عندما تشمل العمليات خدمات متعددة مع قواعد بيانات مستقلة، مما يترك المطورين يبحثون عن مناهج بديلة لضمان سلامة البيانات.
توضح هذه الدليل تنفيذ نمط Saga في Go مع أمثلة عملية تغطي كل من منهجيات الترتيب والرقصة. إذا كنت بحاجة إلى مرجع سريع لأساسيات Go، فإن ورقة Go توفر ملخصًا مفيدًا.
هذا الصورة الجميلة تولدت من قبل نموذج AI Flux 1 dev.
فهم نمط Saga
تم وصف نمط Saga في الأصل من قبل Hector Garcia-Molina و Kenneth Salem في عام 1987. في سياق الميكروخدمات، هو سلسلة من المعاملات المحلية حيث تقوم كل معاملة بتحديث البيانات داخل خدمة واحدة. إذا فشل أي خطوة، يتم تنفيذ إجراءات تعويضية لعكس تأثير الخطوات السابقة.
على عكس المعاملات الموزعة التقليدية التي تستخدم التزامًا ثنائي المرحلة (2PC)، لا تمسك Saga القفل عبر الخدمات، مما يجعلها مناسبة للعمليات التجارية طويلة المدى. التبادل هو الاتساق النهائي بدلًا من الاتساق القوي.
الخصائص الرئيسية
- لا توجد قفل موزع: كل خدمة تدير معاملتها المحلية الخاصة
- إجراءات تعويضية: لكل عملية آلية عكسية مقابلة
- الاتساق النهائي: النظام يصل في النهاية إلى حالة اتساق
- طويل المدى: مناسب للعمليات التي تستغرق ثوانٍ، دقائق، أو حتى ساعات
مناهج تنفيذ Saga
هناك طريقتان رئيسيتان لتنفيذ نمط Saga: الترتيب والرقصة.
نمط الترتيب
في الترتيب، يدير مُنسق مركزي (منسق) تدفق المعاملة بالكامل. يتحمل المنسق المسؤولية عن:
- دعوة الخدمات في الترتيب الصحيح
- التعامل مع الفشل وتفعيل التعويضات
- الحفاظ على حالة Saga
- تنسيق إعادة المحاولة والوقت المحدد
المزايا:
- التحكم المركزي والرؤية
- أسهل فهمًا وتصحيحًا
- أفضل معالجة للأخطاء والتعافي
- اختبار أبسط لتدفق الكلي
العيوب:
- نقطة فشل واحدة (على الرغم من أن هذا يمكن تخفيفه)
- خدمة إضافية يجب الحفاظ عليها
- يمكن أن يصبح عنق زجاجة لتدفق معقد
مثال في Go:
type OrderSagaOrchestrator struct {
orderService OrderService
paymentService PaymentService
inventoryService InventoryService
shippingService ShippingService
}
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
sagaID := generateSagaID()
// الخطوة 1: إنشاء طلب
orderID, err := o.orderService.Create(order)
if err != nil {
return err
}
// الخطوة 2: احتفظ بالمخزون
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // تعويض
return err
}
// الخطوة 3: معالجة الدفع
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // تعويض
o.orderService.Cancel(orderID) // تعويض
return err
}
// الخطوة 4: إنشاء شحنة
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // تعويض
o.inventoryService.Release(order.Items) // تعويض
o.orderService.Cancel(orderID) // تعويض
return err
}
return nil
}
نمط الرقصة
في الرقصة، لا يوجد مُنسق مركزي. تعرف كل خدمة ما يجب فعله وتتواصل عبر الأحداث. تسمع الخدمات الأحداث وتتفاعل وفقًا لذلك. هذا النهج القائم على الأحداث يكون خاصًا قوة عندما يُدمج مع منصات تدفق الرسائل مثل AWS Kinesis، التي توفر بنية تحتية قابلة للتطوير لنقل الأحداث عبر الميكروخدمات. لمعرفة دليل شامل حول تنفيذ الميكروخدمات القائمة على الأحداث مع Kinesis، راجع بناء ميكروخدمات قائمة على الأحداث مع AWS Kinesis.
المزايا:
- موزعة وقابلة للتطوير
- لا توجد نقطة فشل واحدة
- تبقى الخدمات مفصولة
- مناسبة طبيعية للبنية القائمة على الأحداث
العيوب:
- أصعب فهمًا لتدفق الكلي
- صعب التصحيح والتعقب
- معالجة الأخطاء معقدة
- خطر وجود اعتمادات دائرية
مثال مع البنية القائمة على الأحداث:
// خدمة الطلب
type OrderService struct {
eventBus EventBus
repo OrderRepository
}
func (s *OrderService) CreateOrder(order Order) (string, error) {
orderID, err := s.repo.Save(order)
if err != nil {
return "", err
}
s.eventBus.Publish("OrderCreated", OrderCreatedEvent{
OrderID: orderID,
CustomerID: order.CustomerID,
Items: order.Items,
Total: order.Total,
})
return orderID, nil
}
func (s *OrderService) HandlePaymentFailed(event PaymentFailedEvent) error {
return s.repo.Cancel(event.OrderID) // تعويض
}
// خدمة الدفع
type PaymentService struct {
eventBus EventBus
client PaymentClient
}
func (s *PaymentService) HandleOrderCreated(event OrderCreatedEvent) {
paymentID, err := s.client.Charge(event.CustomerID, event.Total)
if err != nil {
s.eventBus.Publish("PaymentFailed", PaymentFailedEvent{
OrderID: event.OrderID,
})
return
}
s.eventBus.Publish("PaymentSucceeded", PaymentSucceededEvent{
OrderID: event.OrderID,
PaymentID: paymentID,
})
}
func (s *PaymentService) HandleInventoryReservationFailed(event InventoryReservationFailedEvent) error {
// تعويض: استرداد الدفع
return s.client.Refund(event.PaymentID)
}
استراتيجيات التعويض
التعويض هو قلب نمط Saga. يجب أن يكون لكل عملية تعويض مقابل يمكنه عكس تأثيرها.
أنواع التعويض
-
العمليات القابلة للعكس: العمليات التي يمكن إلغاؤها مباشرة
- مثال: إلغاء المخزون المحجوز، استرداد الدفعات
-
إجراءات التعويض: عمليات مختلفة تحقق التأثير المعاكس
- مثال: إلغاء الطلب بدلًا من حذفه
-
التعويض المتشدد: تخصيص الموارد التي يمكن إلغاؤها
- مثال: احتفاظ بالمخزون قبل تحميل الدفع
-
التعويض المتفائل: تنفيذ العمليات وتعويضها إذا لزم الأمر
- مثال: تحميل الدفع أولاً، واسترداده إذا لم يكن المخزون متاحًا
متطلبات التكرار
يجب أن تكون جميع العمليات والتعويضات مكررة. هذا يضمن أن إعادة المحاولة لفشل العملية لا تسبب تأثيرات مكررة.
func (s *PaymentService) Refund(paymentID string) error {
// التحقق من الاسترداد المسبق
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // تم الاسترداد بالفعل، مكرر
}
// معالجة الاسترداد
return s.processRefund(paymentID)
}
أفضل الممارسات
1. إدارة حالة Saga
احتفظ بحالة كل حالة Saga لتتبع التقدم وتمكين التعافي. عند تخزين حالة Saga في قاعدة بيانات، اختيار ORM المناسب أمر حيوي للأداء والصيانة. لمقارنة ORMs في Go لـ PostgreSQL، راجع مقارنة ORMs في Go لـ PostgreSQL: GORM مقابل Ent مقابل Bun مقابل sqlc لاختيار الأنسب لاحتياجات تخزين حالة Saga الخاصة بك:
type SagaState struct {
ID string
Status SagaStatus
Steps []SagaStep
CurrentStep int
CreatedAt time.Time
UpdatedAt time.Time
}
type SagaStep struct {
Service string
Operation string
Status StepStatus
Compensated bool
Data map[string]interface{}
}
2. إدارة الوقت المحدد
قم بتنفيذ وقت محدد لكل خطوة لمنع Saga من التوقف إلى الأبد:
type SagaOrchestrator struct {
timeout time.Duration
}
func (o *SagaOrchestrator) ExecuteWithTimeout(step SagaStep) error {
ctx, cancel := context.WithTimeout(context.Background(), o.timeout)
defer cancel()
done := make(chan error, 1)
go func() {
done <- step.Execute()
}()
select {
case err := <-done:
return err
case <-ctx.Done():
// حدث وقت محدد، تعويض
if err := step.Compensate(); err != nil {
return fmt.Errorf("فشل التعويض: %w", err)
}
return fmt.Errorf("الخطوة %s تجاوزت الوقت المحدد بعد %v", step.Name(), o.timeout)
}
}
3. منطق إعادة المحاولة
قم بتنفيذ تراجع أسي في حالات الفشل المؤقت:
func retryWithBackoff(operation func() error, maxRetries int) error {
backoff := time.Second
for i := 0; i < maxRetries; i++ {
err := operation()
if err == nil {
return nil
}
if !isTransientError(err) {
return err
}
time.Sleep(backoff)
backoff *= 2
}
return fmt.Errorf("فشل العملية بعد %d محاولة", maxRetries)
}
4. استخدام سجل الأحداث لحالة Saga
استخدم سجل الأحداث للحفاظ على سجل كامل. عند تنفيذ مخازن الأحداث وآليات إعادة التشغيل، يمكن لـ Go Generics مساعدة إنشاء كود معالجة الأحداث قابل لإعادة الاستخدام وآمن من الناحية النوعية. لمعرفة الأنماط المتقدمة باستخدام Generics في Go، راجع Generics في Go: استخدامات وأنماط.
type SagaEvent struct {
SagaID string
EventType string
Payload []byte
Timestamp time.Time
Version int64
}
type SagaEventStore struct {
store EventRepository
}
func (s *SagaEventStore) AppendEvent(sagaID string, eventType string, payload interface{}) error {
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("فشل تحويل الحمولة: %w", err)
}
version, err := s.store.GetNextVersion(sagaID)
if err != nil {
return fmt.Errorf("فشل الحصول على الإصدار: %w", err)
}
event := SagaEvent{
SagaID: sagaID,
EventType: eventType,
Payload: data,
Timestamp: time.Now(),
Version: version,
}
return s.store.Save(event)
}
func (s *SagaEventStore) ReplaySaga(sagaID string) (*Saga, error) {
events, err := s.store.GetEvents(sagaID)
if err != nil {
return nil, fmt.Errorf("فشل الحصول على الأحداث: %w", err)
}
saga := NewSaga()
for _, event := range events {
if err := saga.Apply(event); err != nil {
return nil, fmt.Errorf("فشل تطبيق الحدث: %w", err)
}
}
return saga, nil
}
5. المراقبة والمراقبة
قم بتنفيذ سجل شامل ومتابعة:
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
span := tracer.StartSpan("saga.create_order")
defer span.Finish()
span.SetTag("saga.id", sagaID)
span.SetTag("order.id", order.ID)
logger.WithFields(log.Fields{
"saga_id": sagaID,
"order_id": order.ID,
"step": "create_order",
}).Info("بدأ Saga")
// ... تنفيذ Saga
return nil
}
الأنماط الشائعة والأنماط المضادة
الأنماط التي يجب اتباعها
- نمط منسق Saga: استخدام خدمة مخصصة للترتيب
- نمط الخروج: ضمان نشر الأحداث بشكل موثوق
- مفاتيح التكرار: استخدام مفاتيح فريدة لكل عملية
- آلة حالة Saga: نمذجة Saga كآلة حالة
الأنماط المضادة التي يجب تجنبها
- التعويض المتزامن: لا تنتظر اكتمال التعويض
- Sagas المتشابكة: تجنب Sagas التي تدعو Sagas أخرى (استخدم Sagas فرعية بدلًا من ذلك)
- الحالة المشتركة: لا تشارك الحالة بين خطوات Saga
- الخطوات طويلة المدى: قسم الخطوات التي تستغرق وقتًا طويلاً
الأدوات والمنصات
يمكن لعدة منصات مساعدة في تنفيذ نمط Saga:
- Temporal: منصة تنسيق العمليات مع دعم مدمج لـ Saga
- Zeebe: محرك عمليات لتنسيق الميكروخدمات
- Eventuate Tram: منصة Saga لـ Spring Boot
- AWS Step Functions: تنسيق العمليات بدون خادم
- Apache Camel: منصة تكامل مع دعم لـ Saga
لخدمات المنسق التي تحتاج واجهات سطر الأوامر لإدارة ومراقبة، بناء تطبيقات سطر الأوامر في Go مع Cobra & Viper توفر أنماط ممتازة لبناء أدوات سطر الأوامر لتفاعل مع منسق Saga.
عند نشر ميكروخدمات تعتمد على Saga في Kubernetes، يمكن أن يؤدي تنفيذ شبكة خدمة إلى تحسين كبير في المراقبة والأمان وإدارة المرور. تنفيذ شبكة خدمة مع Istio وLinkerd تغطي كيفية مساعدة شبكات الخدمة في أنماط المعاملات الموزعة من خلال تقديم ميزات مثل تتبع التوزيع وانقطاع الدائرة.
متى يجب استخدام نمط Saga
استخدم نمط Saga عندما:
- ✅ العمليات تشمل عدة ميكروخدمات
- ✅ العمليات التجارية طويلة المدى
- ✅ الاتساق النهائي مقبول
- ✅ تحتاج إلى تجنب قفل موزع
- ✅ الخدمات لها قواعد بيانات مستقلة
تجنب عندما:
- ❌ يتطلب الاتساق القوي
- ❌ العمليات بسيطة وسريعة
- ❌ جميع الخدمات تشارك نفس قاعدة البيانات
- ❌ منطق التعويض معقد
الخاتمة
نمط Saga ضروري لإدارة المعاملات الموزعة في بنى الميكروخدمات. على الرغم من أنه يضيف تعقيدًا، فإنه يوفر حلًا عمليًا للحفاظ على اتساق البيانات عبر الحدود الخدمية. اختر الترتيب للحصول على تحكم أفضل ورؤية، أو الرقصة للحصول على قابلية للتطوير وانفصال خفيف. تأكد دائمًا من أن العمليات مكررة، وقم بتنفيذ منطق التعويض المناسب، وحافظ على مراقبة شاملة.
مفتاح نجاح تنفيذ Saga هو فهم متطلبات الاتساق، وتصميم منطق التعويض بعناية، واختيار المنهج المناسب لحالة الاستخدام الخاصة بك. مع التنفيذ المناسب، يتيح لك Saga بناء ميكروخدمات قوية وقابلة للتطوير تحافظ على سلامة البيانات عبر الأنظمة الموزعة.
روابط مفيدة
- أنماط الميكروخدمات بواسطة Chris Richardson
- نمط Saga - Martin Fowler
- منصة Saga لـ Eventuate Tram
- محرك عمليات Temporal
- مستندات AWS Step Functions
- ورقة Go
- Generics في Go: استخدامات وأنماط
- مقارنة ORMs في Go لـ PostgreSQL: GORM مقابل Ent مقابل Bun مقابل sqlc
- بناء تطبيقات سطر الأوامر في Go مع Cobra & Viper
- تنفيذ شبكة خدمة مع Istio وLinkerd
- بناء ميكروخدمات قائمة على الأحداث مع AWS Kinesis