الگوی حماسه: تراکنش های توزیع شده در معماری میکروسرویس ها

الگوی حماسه: تراکنش های توزیع شده در معماری میکروسرویس ها

در برنامه های کاربردی یکپارچه سنتی، حفظ ثبات داده ها در چندین موجودیت ساده است. موتورهای پایگاه داده رابطه ای تضمین های ACID (اتمی، سازگاری، جداسازی، دوام) را در داخل تراکنش های SQL محلی ارائه می کنند. اگر قرار دادن سفارش، کسر پرداخت، یا ذخیره موجودی در نیمه راه با شکست مواجه شود، با فراخوانی ROLLBACK هر تغییر پایگاه داده فوراً برمی گردد.

با این حال، هنگام مهاجرت به معماری مدرن Microservices، مدیریت داده اساساً تغییر می کند. برای اطمینان از استقلال دامنه و مقیاس پذیری مستقل، هر میکروسرویس دارای پایگاه داده خصوصی خود است. یک عملیات تجاری واحد - مانند پردازش پرداخت تجارت الکترونیک - اکنون مرزهای سرویس و موتورهای پایگاه داده متعددی را در بر می گیرد (مانند PostgreSQL برای سفارشات، DynamoDB برای پرداخت، Redis برای موجودی).

از آنجایی که میکروسرویس های توزیع شده نمی توانند به یک تراکنش پایگاه داده تکیه کنند، حفظ ثبات داده ها در سراسر مرزهای شبکه به یکی از چالش برانگیزترین مشکلات در مهندسی سیستم های توزیع شده تبدیل می شود.

برای حل این مشکل بدون به خطر انداختن در دسترس بودن یا عملکرد سیستم، معماران نرم افزار به Saga Pattern تکیه می کنند.

در این بررسی عمیق، بررسی خواهیم کرد که چرا تراکنش های توزیع شده سنتی شکست می خورند، مکانیک های اصلی الگوی حماسه را تجزیه می کنیم، رقص رقص در مقابل ارکستراسیون را مقایسه می کنیم، اقدامات متقابل جداسازی را تجزیه و تحلیل می کنیم، پیاده سازی کد تولید را در Go و Java بررسی می کنیم، و یاد می گیریم که چگونه با شکست در دنیای واقعی به طور ایمن برخورد کنیم.


مشکل ریشه: چرا دو فاز commit (2PC) در میکروسرویس ها ناموفق است

قبل از اتخاذ الگوی Saga، مهندسان اغلب می‌پرسند: چرا نمی‌توانیم از Commit سنتی دو فازی (2PC / XA) ​​در میکروسرویس‌های خود استفاده کنیم؟

مکانیک 2PC

Commit دو مرحله ای یک تراکنش توزیع شده را در چندین گره پایگاه داده با استفاده از یک Transaction Manager مرکزی در دو مرحله هماهنگ می کند:

  1. ** مرحله آماده سازی **: مدیر تراکنش از تمام گره های پایگاه داده شرکت کننده می خواهد که ردیف های مورد نیاز را آماده و قفل کنند. شرکت‌کنندگان به YES یا NO رأی می‌دهند.
  2. ** فاز تعهد **: اگر همه شرکت کنندگان به YES رای دادند، مدیر یک فرمان COMMIT را برای همه گره ها صادر می کند. اگر هر گره ای به NO رای داد یا زمان آن تمام شد، یک ROLLBACK صادر می کند.
Client ----> Transaction Manager
                   |
     +-------------+-------------+
     | (Prepare)   | (Prepare)   | (Prepare)
     v             v             v
 Order DB     Payment DB    Inventory DB

چرا 2PC یک ضدالگو برای میکروسرویس ها است

در حالی که 2PC ثبات قوی را تضمین می کند، در محیط های میکروسرویس بومی ابری به دلیل چندین نقص معماری خراب می شود:

  1. مسدود کردن و مناقشه منابع: ردیف های پایگاه داده در طول دست دادن شبکه چند مرحله ای قفل می مانند. اگر تأخیر شبکه افزایش یابد یا یک سرویس کند شود، قفل ها باز نگه داشته می شوند و به سرعت استخرهای اتصال را مصرف می کنند و باعث خرابی سیستم آبشاری می شوند.
  2. ** گلوگاه در دسترس بودن**: در 2PC، در دسترس بودن سیستم با محصول همه موجودی های شرکت کننده محدود می شود ($A_{total} = A_1 \times A_2 \times \dots \times A_n$). اگر هر یک از خدمات یا گره های پایگاه داده در مرحله آماده سازی آفلاین شود، کل تراکنش جهانی به طور نامحدود مسدود می شود.
  3. از دست دادن استقلال سرویس: 2PC سرویس ها را مجبور می کند تا پروتکل های XA در سطح پایگاه داده را در سراسر API های شبکه افشا کنند و موتورهای پایگاه داده خود را مستقیماً جفت کنند.
  4. محدودیت های بروکر: کارگزاران پیام با توان عملیاتی بالا مانند Apache Kafka یا RabbitMQ بطور بومی در تراکنش های سنتی XA 2PC در پایگاه های داده رابطه ای شرکت نمی کنند.

طبق نظریه CAP، سیستم های توزیع شده باید بین سازگاری قوی (C) و دسترسی بالا (A) تحت پارتیشن های شبکه (P) انتخاب کنند. سیستم‌های میکروسرویس مدرن در دسترس بودن و تحمل پارتیشن را اولویت‌بندی می‌کنند و ثبات فوری را برای ثبات نهایی معامله می‌کنند (پایه: اساساً موجود، حالت نرم، سازگاری نهایی).


الگوی حماسه چیست؟

الگوی Saga در ابتدا توسط هکتور گارسیا-مولینا و کنت سالم در سال 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)

اجرای رو به جلو در مقابل جبران عقب ماندگی

اگر همه تراکنش‌های محلی موفق شوند، Saga با موفقیت کامل می‌شود (اجرای پیشرو).

با این حال، اگر یک تراکنش محلی در نیمه راه با شکست مواجه شود (به عنوان مثال، $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$) داشته باشد.
تراکنش محوری گام قطعی در حماسه. اگر Pivot موفق شود، Saga تضمین شده است که به پایان برسد. اگر شکست بخورد، Saga به عقب برمی گردد. نه قابل جبران و نه قابل استرداد. مرز بین بازگشت به عقب و تکمیل رو به جلو را مشخص می کند.
**معاملات قابل استرداد ** مراحل پس از تراکنش Pivot اجرا شد. آنها تضمین می شوند که در نهایت موفق شوند و نیازی به غرامت ندارند. باید کاملاً ناتوان باشد، زیرا تا زمانی که موفقیت آمیز باشد، به طور خودکار دوباره امتحان خواهند شد.

تفکیک نمونه پرداخت تجارت الکترونیک

تسویه حساب سفارش تجارت الکترونیکی شامل چهار مرحله را در نظر بگیرید:

  1. $T_1$: ایجاد سفارش معلق (قابل جبران) $\to$ واگرد از طریق $C_1$: لغو سفارش.
  2. $T_2$: مجوز پرداخت (قابل جبران) $\to$ واگرد از طریق $C_2$: پرداخت بازپرداخت.
  3. $T_3$: موجودی ذخیره (معامله محوری) $\to$ اگر تخصیص سهام موفقیت آمیز باشد، سفارش نهایی می شود. اگر ناموفق بود، $C_2$ و $C_1$ را فعال کنید.
  4. $T_4$: درخواست ارسال ارسال (قابل تکرار) $\to$ بعد از پیوت اجرا شد. تا زمان تحویل مجدد تلاش کرد.

سبک های معماری حماسه: رقص در مقابل ارکستراسیون

دو سبک معماری اولیه برای اجرای الگوی ساگا در سیستم های توزیع شده وجود دارد: کوئوگرافی (غیرمتمرکز) و ارکستراسیون (متمرکز).

سبک‌های پیاده‌سازی الگوی حماسه: نمودار معماری رقص در مقابل ارکستراسیون

سبک 1: رقص (غیرمتمرکز رویداد محور)

در یک حماسه مبتنی بر رقص، هیچ کنترل کننده یا هماهنگ کننده مرکزی وجود ندارد. در عوض، میکروسرویس ها با گوش دادن به رویدادهای دامنه منتشر شده در یک گذرگاه رویداد مرکزی (مانند آپاچی کافکا، NATS یا RabbitMQ) به صورت ناهمزمان ارتباط برقرار می کنند.

مکانیک گردش کار

  1. Order Service $T_1$ را اجرا می کند (سفارش معلق ایجاد می کند) و یک رویداد OrderCreated را برای کافکا منتشر می کند.
  2. خدمات پرداخت به OrderCreated گوش می دهد، T_2$ را اجرا می کند (کارت اعتباری را شارژ می کند)، و یک رویداد PaymentCompleted (یا PaymentFailed) منتشر می کند.
  3. ** سرویس موجودی ** به PaymentCompleted گوش می دهد، T_3$ را اجرا می کند (اقلام را رزرو می کند). اگر اقلام موجود نباشد، InventoryReservationFailed را منتشر می کند.
  4. خدمات پرداخت به InventoryReservationFailed گوش می دهد و $C_2$ را اجرا می کند (بازپرداخت را صادر می کند).
  5. سرویس سفارش به PaymentRefunded گوش می دهد و $C_1$ را اجرا می کند (سفارش را به عنوان لغو علامت گذاری می کند).

مزایای رقص

  • ** اتصال شل **: خدمات فقط در موضوعات رویداد مشترک می شوند. آنها در مورد اجرای خدمات دیگر نمی دانند.
  • ** توان عملیاتی بالا و تمرکززدایی **: میخانه/زیر رویداد مستقیم، گلوگاه های ارکستر مرکزی را از بین می برد.
  • سادگی برای گردش های کاری کوتاه: تنظیم آسان برای گردش های کاری ساده 2 تا 3 مرحله ای.

معایب رقص

  • ریسک وابستگی چرخه ای: سرویس ها می توانند در نهایت به رویدادهای یکدیگر گوش دهند و وابستگی های چرخه ای پیچیده ای ایجاد کنند.
  • ردیابی کد دشوار: درک فرآیند کسب و کار انتها به انتها نیاز به منطق ردیابی در چندین مخزن کد پایه دارد.
  • توفان و پیچیدگی رویداد: با افزایش تعداد مراحل (به عنوان مثال، بیش از 10 سرویس)، مدیریت موارد لبه خطا منجر به انفجار حالت رویداد می شود.

سبک 2: ارکستراسیون (مدیریت متمرکز جریان کار)

در یک Saga مبتنی بر ارکستراسیون، یک میکروسرویس اختصاصی به نام Saga Orchestrator کل چرخه حیات تراکنش توزیع شده را کنترل می کند. ارکستراتور به عنوان هماهنگ کننده مرکزی عمل می کند و دستورات صریح را برای میکروسرویس های شرکت کننده صادر می کند و به رویدادهای پاسخ آنها گوش می دهد.

مکانیک گردش کار

  1. مشتری درخواست سفارش را به Saga Orchestrator ارسال می کند.
  2. ارکستراتور یک فرمان CreateOrder را به Order Service ارسال می کند. سفارش سرویس OrderCreated را برمی گرداند.
  3. ارکستراتور ماشین حالت خود را به روز می کند و یک فرمان ProcessPayment را به خدمات پرداخت ارسال می کند. سرویس پرداخت PaymentSuccessful را برمی گرداند.
  4. ارکستراتور یک فرمان ReserveInventory را به Inventory Service ارسال می کند. سرویس موجودی InventoryFailed (Out of Stock) را برمی گرداند.
  5. ارکستراتور شکست را تشخیص می دهد، جریان جبران را آغاز می کند:
    • دستور RefundPayment را به خدمات پرداخت ارسال می کند.
    • دستور CancelOrder را به Order Service ارسال می کند.
  6. ارکستراتور اجرای Saga را به عنوان FAILED علامت گذاری می کند.

مزایای ارکستراسیون

  • منطق کسب و کار متمرکز: حالت گردش کار و منطق تجاری در یک سرویس ارکستراتور یا دستگاه حالت بومی سازی می شوند.
  • بدون وابستگی چرخه ای: میکروسرویس ها به دستورات ارکستر پاسخ می دهند. آنها به سایر خدمات پایین دستی وابسته نیستند یا از آنها اطلاعی ندارند.
  • پاک کردن نظارت و اشکال زدایی: وضعیت تراکنش سرتاسر به صراحت در فروشگاه حالت ارکستراتور ذخیره می شود (به عنوان مثال، PostgreSQL یا موتورهای گردش کار مانند Temporal / Camunda).
  • ** مدیریت خطا آسانتر **: افزودن مراحل جدید یا تغییر قوانین بازگشت به طور کامل در داخل ارکستراتور مدیریت می شود.

معایب ارکستراسیون

  • پیچیدگی ارکستراتور: خطر قرار دادن منطق دامنه بیش از حد در ارکستراتور، تبدیل آن به یک ضد الگوی “ارکستراتور هوشمند، سرویس گنگ”.
  • نقطه بالقوه شکست: ارکستراتور باید بسیار در دسترس و حالت دار باشد.

ماتریس مقایسه ای: رقص در مقابل ارکستراسیون

ویژگی رقص ارکستراسیون
ساختار کنترل غیرمتمرکز (Event Pub/Sub) متمرکز (هماهنگ کننده حماسه / ماشین دولتی)
کوپلینگ بسیار کم (خدمات مصرف رویدادها) متوسط ​​(سرویس ها دستورات Coordinator را می پذیرند)
قابلیت مشاهده فرآیند کم (توزیع شده در بین فایل های گزارش) بالا (Single State Store گردش کار را تجسم می کند)
بهترین مناسب گردش کار ساده (2 تا 4 مرحله خدمات) گردش کار سازمانی پیچیده (5+ مرحله، منطق انشعاب)
ابزار / چارچوب Kafka، RabbitMQ، NATS، AWS EventBridge Temporal.io، AWS Step Functions، Camunda، Axon

چالش ها و اقدامات متقابل جداسازی (کنترل “ACID منهای I”)

از آنجا که تراکنش‌های محلی در یک Saga بلافاصله به پایگاه‌های اطلاعاتی محلی خود متعهد می‌شوند، الگوی Saga فاقد Isolation (I) از تضمین‌های سنتی ACID است.

اگر مشتری یک ردیف پایگاه داده اصلاح شده توسط $T_1$ را در حالی که Saga هنوز در حال اجرا است بخواند، در حال خواندن حالت متوسط ​​و غیرمتعهد است. اگر یک مرحله پایین‌دستی با شکست مواجه شود و باعث جبران خسارت شود ($C_1$)، مشتری یک خواندن کثیف را انجام داده است.

ناهنجاری های رایج ناشی از عدم انزوا

  1. **به روز رسانی گم شده **: حماسه A یک رکورد را به روز می کند. قبل از اینکه Saga A کامل شود، Saga B همان رکورد را بازنویسی می کند. اگر Saga A شکست بخورد و جبران را اجرا کند، به‌روزرسانی Saga B را بازنویسی می‌کند.
  2. خواندن کثیف: یک مشتری سهام موجود را که توسط Saga A به روز شده است ($T_1$) می خواند. Saga A در پایین‌دست ($T_3$) شکست می‌خورد و سهام ($C_1$) را بازیابی می‌کند، اما مشتری قبلاً بر اساس داده‌های قدیمی سفارش داده است.
  3. ** خوانده‌های غیرقابل تکرار **: یک سرویس داده‌ها را در مرحله $T_1$ می‌خواند و در مرحله $T_3$ دوباره می‌خواند، اما Saga همزمان دیگری داده‌ها را در این بین تغییر داد.

اقدامات متقابل و راهبردهای کاهش

برای حفظ یکپارچگی داده ها علیرغم فقدان جداسازی، معماران نرم افزار الگوهای طراحی جداسازی خاصی را پیاده سازی می کنند:

1. قفل معنایی (در انتظار / وضعیت پرچمدار)

هنگامی که تراکنش محلی $T_1$ یک رکورد پایگاه داده را به روز می کند، یک فیلد وضعیت را روی PENDING یا APPROVAL_REQUIRED تنظیم می کند (به عنوان مثال، ORDER_PENDING_PAYMENT).

سایر حماسه‌های همزمان که این رکورد را می‌خوانند باید پرچم قفل معنایی را بررسی کنند و رفتار خود را مسدود یا تغییر دهند تا زمانی که حالت به COMMITTED یا CANCELLED تغییر کند.

2. انجام دستور

دنباله تراکنش های محلی را طوری طراحی کنید که عملیات پرخطر یا غیرقابل برگشت در اواخر اجرای Saga رخ دهد و پنجره آسیب پذیری را به حداقل برساند.

3. بازخوانی اعتبارسنجی (کنترل همزمانی خوش بینانه)

قبل از اجرای یک مرحله حیاتی یا جبران، رکورد پایگاه داده هدف را دوباره بخوانید و مهرهای زمانی نسخه (version_id) را تأیید کنید تا مطمئن شوید که هیچ تغییری همزمان رخ نداده است.

4. دیدگاه بدبینانه

برای به حداقل رساندن مواجهه اقتصادی، مراحل یک Saga را دوباره ترتیب دهید (به عنوان مثال، مجوز پرداخت را تا حد امکان نزدیک به تراکنش محوری قرار دهید).


جریان توالی تولید: معاملات جبرانی

در زیر نمودار جریان توالی کاملی وجود دارد که یک شکست اجرای رو به جلو و اجرای جبران عقب‌نشینی حاصل را نشان می‌دهد:

نمودار جریان دنباله الگوی Saga که شکست اجرای رو به جلو و جبران عقبگردها را نشان می دهد

پیاده سازی کد عملی

بیایید نمونه‌های پیاده‌سازی آماده تولید را برای هر دو رقص (در ** Go**) و ارکستراسیون (در Java Spring Boot) بررسی کنیم.

پیاده سازی 1: حماسه مبتنی بر رقص در Go

در این مثال Go، یک Order Service را نشان می‌دهیم که ایجاد سفارش و گوش دادن به رویدادهای شکست پرداخت را از طریق یک کارگزار رویداد برای اجرای منطق بازگشت جبران‌کننده انجام می‌دهد.

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: حماسه مبتنی بر ارکستراسیون در جاوا (Spring Boot)

در این مثال جاوا، ما یک 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 خود را به روز کند و سپس پیامی را برای کافکا منتشر کند، یک مشکل شبکه پس از ارتکاب پایگاه داده باعث از دست دادن رویداد خاموش می شود. برعکس، انتشار پیام قبل از ارتکاب پایگاه داده منجر به پردازش رویداد فانتوم می شود.

برای حل این مشکل، Sagas باید با Transactional Outbox Pattern جفت شود:

[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 یا یک رله رای‌گیری) از جدول صندوق خروجی خوانده می‌شود و رویدادها را به‌طور قابل اعتمادی برای کافکا منتشر می‌کند.

علاوه بر این، هر مصرف‌کننده سرویس پایین‌دستی باید 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 یک الگوی معماری ضروری برای مدیریت تراکنش های توزیع شده در سراسر مرزهای میکروسرویس بدون قفل کردن منابع یا به خطر انداختن در دسترس بودن سیستم است.

چک لیست خلاصه:

  1. رها کردن 2PC/XA در میکروسرویس های Cloud-Native: commit دو فاز باعث قفل شدن محکم، تأخیر زیاد و تنگناهای دسترسی شدید می شود.
  2. تراکنش ها را به مراحل محلی تقسیم کنید: عملیات جهانی را به تراکنش های محلی ($T_1 \dots T_n$) که با تراکنش های جبران کننده معکوس جفت شده اند ($C_1 \dots C_{n-1}$) تقسیم کنید.
  3. سبک معماری مناسب را انتخاب کنید:
    • از رقص برای جریانهای ساده و 2-3 مرحله ای مبتنی بر رویداد با جفت شل استفاده کنید.
    • از Orchestration برای گردش های کاری پیچیده تجاری که نیاز به دید متمرکز، انشعاب و ردیابی ماشین حالت دارند، استفاده کنید.
  4. اقدامات متقابل ایزوله را اجرا کنید: با استفاده از قفل های معنایی (پرچم های PENDING) و بازخوانی قفل خوش بینانه از خواندن های کثیف و به روز رسانی های از دست رفته محافظت کنید.
  5. پیام‌رسانی قابل اعتماد را تضمین کنید: همیشه پیاده‌سازی‌های Saga را با الگوی صندوق خروجی معاملاتی جفت کنید و مصرف‌کنندگان بی‌عیب را مجبور کنید تا با تلاش‌های مجدد با خیال راحت رسیدگی کنند.

با اجرای دقیق الگوی Saga، می‌توانید ریزسرویس‌های بسیار در دسترس و مقیاس‌پذیر بسازید که حتی زمانی که شبکه‌ها از کار می‌افتند، انعطاف‌پذیر و پایدار می‌مانند.