יישום תבנית סאגה לעסקאות מבוזרות
תארו לעצמכם אתר מסחר אלקטרוני עם מאות אלפי הזמנות ביום. לאחר תשלום מוצלח, שירות המשלוחים נופל, והנתונים יוצאים מסנכרון: כסף נוכה אך ההזמנה לא נשלחה. ללא ניהול נכון של עסקאות מבוזרות, תרחישים כאלה מובילים להפסדים כספיים ולנטישת לקוחות. תבנית הסאגה פותרת זאת על ידי פירוק עסקה עסקית לשלבים מקומיים עם פיצויים במקרה של כישלון. אנו מיישמים סאגות במערכות מיקרוסרוויסים כבר למעלה מ-5 שנים — צרו קשר לפתרון סוהר עם אחריות לשלמות נתונים.
שני סוגי סאגה
כוריאוגרפיה — שירותים מגיבים לאירועים של זה ללא מתאם מרכזי. כל שירות מפרסם אירועים לברוקר (למשל, Kafka) ונרשם לאירועים רלוונטיים. אם התשלום נכשל, שירות המלאי מבטל את ההזמנה באמצעות אירוע הפוך.
תזמור — מתאם סאגה מרכזי (למשל, Temporal) מנהל במפורש את השלבים והפיצויים. הקוד ברור יותר, קל יותר לניפוי באגים, אך דורש שירות נפרד.
| קריטריון | תזמור | כוריאוגרפיה |
|---|---|---|
| תיאום | מתאם מרכזי | אוטובוס אירועים (Kafka, RabbitMQ) |
| מורכבות פיתוח | בינונית (דורש שירות מתאם) | נמוכה בהתחלה, גבוהה עם שירותים רבים |
| ניפוי באגים | קל (יומני מתאם) | קשה (מעקב אירועים) |
| אמינות | תלוי במתאם | מבוזר |
| ביצועים | שלב אחד בכל פעם | שלבים מקבילים (פחות שליטה) |
איך לבחור בין תזמור לכוריאוגרפיה?
אם יש לכם עד 5 שירותים ופשטות ניפוי הבאגים היא קריטית — בחרו בתזמור. עבור מערכות גדולות עם 10+ שירותים ועומס גבוה, כוריאוגרפיה דרך Kafka מספקת יכולת הרחבה טובה יותר. אנו מרבים לשלב בין השניים: תזמור לשרשראות קריטיות, כוריאוגרפיה לתהליכי רקע. המהנדסים שלנו יבחרו את הגישה הנכונה לפרויקט שלכם — צרו קשר לייעוץ.
מה זה Temporal ולמה אתם צריכים אותו?
Temporal הוא מנוע מוכן לייצור עבור זרימות עבודה ארוכות טווח. הוא מנסה אוטומטית פעולות חוזרות, מאחסן היסטוריית ביצוע, ומאפשר לבדוק סאגות דרך ממשק משתמש. הוא מבטיח ביצוע פיצויים גם אם שירות קורס. למעלה מ-95% מהסאגות עם Temporal מסתיימות ללא התערבות ידנית. לפי הנתונים שלנו, תזמור דרך Temporal אמין פי 2-3 יותר מכוריאוגרפיה ללא מתאם. דוגמת תזמור עם Temporal:
import { proxyActivities, sleep } from '@temporalio/workflow'; const { reserveStock, chargePayment, createShipment, releaseStock, refund } = proxyActivities({ startToCloseTimeout: '10 seconds' }); export async function createOrderWorkflow(input: CreateOrderInput): Promise<void> { let stockReserved = false; let paymentCharged = false; try { await reserveStock({ orderId: input.orderId, items: input.items }); stockReserved = true; await chargePayment({ orderId: input.orderId, amount: input.amount }); paymentCharged = true; await createShipment({ orderId: input.orderId, address: input.address }); } catch (error) { // Temporal гарантирует выполнение компенсаций if (paymentCharged) { await refund({ orderId: input.orderId }); } if (stockReserved) { await releaseStock({ orderId: input.orderId }); } throw error; } } סאגה מתמשכת עם מצב
סאגה חייבת לשרוד הפעלות מחדש של שירותים. המצב נשמר במסד נתונים (PostgreSQL, MySQL). אנו משתמשים בטבלה עם סטטוסים (import { proxyActivities, sleep } from '@temporalio/workflow'; const { reserveStock, chargePayment, createShipment, releaseStock, refund } = proxyActivities({ startToCloseTimeout: '10 seconds' }); export async function createOrderWorkflow(input: CreateOrderInput): Promise<void> { let stockReserved = false; let paymentCharged = false; try { await reserveStock({ orderId: input.orderId, items: input.items }); stockReserved = true; await chargePayment({ orderId: input.orderId, amount: input.amount }); paymentCharged = true; await createShipment({ orderId: input.orderId, address: input.address }); } catch (error) { // Temporal гарантирует выполнение компенсаций if (paymentCharged) { await refund({ orderId: input.orderId }); } if (stockReserved) { await releaseStock({ orderId: input.orderId }); } throw error; } } , running, completed, failed) והקשר. כאשר שירות קורס, הוא קורא מחדש סאגות לא שלמות וממשיך מהשלב האחרון.
interface SagaState { sagaId: string; sagaType: string; status: 'running' | 'completed' | 'failed' | 'compensating'; currentStep: number; context: Record<string, unknown>; completedSteps: string[]; failedStep?: string; createdAt: Date; updatedAt: Date; } class PersistentSagaOrchestrator { async startSaga(sagaType: string, context: unknown): Promise<string> { const sagaId = uuidv4(); await this.sagaRepo.save({ sagaId, sagaType, status: 'running', currentStep: 0, context, completedSteps: [] }); await this.executeSaga(sagaId); return sagaId; } } כוריאוגרפיה דרך Kafka
דוגמת טיפול באירועים בשירות המלאי:
// Order Service публикует событие await kafka.producer.send({ topic: 'order.events', messages: [{ key: orderId, value: JSON.stringify({ type: 'OrderCreated', orderId, items, customerId })}] }); // Inventory Service слушает и резервирует kafka.consumer.subscribe({ topic: 'order.events' }); kafka.consumer.run({ eachMessage: async ({ message }) => { const event = JSON.parse(message.value.toString()); if (event.type !== 'OrderCreated') return; try { await inventoryService.reserveStock(event.orderId, event.items); await kafka.producer.send({ topic: 'inventory.events', messages: [{ key: event.orderId, value: JSON.stringify({ type: 'StockReserved', orderId: event.orderId })}] }); } catch { await kafka.producer.send({ topic: 'inventory.events', messages: [{ key: event.orderId, value: JSON.stringify({ type: 'StockReservationFailed', orderId: event.orderId })}] }); } } }); בעיות נפוצות ופתרונות
| בעיה | פתרון |
|---|---|
| פעולות לא אידמפוטנטיות | בדיקת מצב קיים לפני יצירה (דוגמה למעלה) |
| אובדן מצב סאגה בקריסה | שמירת סטטוס והקשר במסד נתונים |
| ניסיונות חוזרים אינסופיים ועומס מערכת | השהיה אקספוננציאלית ומגבלת ניסיונות (בדרך כלל 3-5) |
| חוסר ניטור | כלים כמו Jaeger, Grafana להמחשת התקדמות הסאגה |
מה כלול בעבודה
תהליך היישום כולל:
- ניתוח: הגדרת עסקאות עסקיות, גבולות שירותים, נקודות כשל.
- עיצוב: בחירה בין תזמור לכוריאוגרפיה, הכנת תוכנית פיצויים.
- יישום: כתיבת קוד סאגה, אינטגרציה עם Temporal או Kafka, הבטחת אידמפוטנטיות.
- בדיקות: בדיקות יחידה, בדיקות אינטגרציה, בדיקות תרחישי כשל (הנדסת כאוס).
- פריסה: פריסה ב-Docker/Kubernetes, הגדרת ניטור (Jaeger, Grafana).
תוצרים: תיעוד סאגה, גישה למאגר, הדרכת צוות, ותמיכה של שבועיים לאחר ההשקה.
דוגמת סאגה מורכבת עם פיצויים מרובים
בפרויקטים אמיתיים, סאגה אחת יכולה לכלול עשרות שירותים. לדוגמה, הזמנה עם הזמנה מוקדמת ומשלוח: שמירת מלאי במחסן, תשלום, יצירת הזמנת ספק, הגדרת משלוח. הפיצויים מתבצעים בסדר הפוך, ו-Temporal מבטיח ביצוע גם לאחר מספר הפעלות מחדש.
לוחות זמנים ואחריות
- תזמור פשוט (2-3 שירותים, ללא Temporal) — 1 עד 2 שבועות.
- תזמור עם Temporal + ניטור — 2 עד 3 שבועות.
- כוריאוגרפיה דרך Kafka עם מטפלים אידמפוטנטיים — 2 עד 4 שבועות.
העלות נקבעת באופן אישי לאחר ניתוח. אנו מציעים אחריות לקוד למשך 6 חודשים. למעלה מ-100 פרויקטים שנמסרו במיקרוסרוויסים (5+ שנים בשוק). צרו קשר להערכת פרויקט חינמית ולפתרון אופטימלי.
קריאה נוספת: תבנית סאגה (ויקיפדיה).







