Solving Message Ordering from First Principles
حل مسئلهی ترتیب پیامها از اصول اولیه 🧠
بیشتر سیستمها به global message ordering نیاز ندارند. 🌍❌
آنها به چیزی سادهتر و در عین حال کاربردیتر نیاز دارند:
اینکه رویدادها برای هر aggregate به صورت ترتیبی پردازش شوند. 🔄
برای هر OrderId، برای هر InvoiceId، برای هر CustomerId،یا هر مرزی که برای aggregate خود تعریف کردهاید.
میتوانید این مرز را هرچقدر که لازم دارید گسترده یا محدود کنید. 🎯
این مسئله در ابتدا شبیه یک مشکل در حوزهی eventing به نظر میرسد، اما اگر الزامات را تا نتیجهی منطقیشان دنبال کنید، در نهایت به یک workflow میرسید.
و آن workflow یک نام دارد: Saga 🧩
Domain Events Feel Like the Clean SolutionءDomain Eventها جذاب هستند چون از اصول اولیه میآیند:
ءDomain Eventها شبیه راهحل تمیز به نظر میرسند ✨
• یک aggregate تغییر وضعیت میدهد 🔁
• ءeventهایی منتشر میکند که توضیح میدهند چه اتفاقی افتاده 📢
• ءhandlerها واکنش نشان میدهند و کار مفید انجام میدهند ⚙️
و شما یک مدل ذهنی قشنگ هم دارید:
State change → Event → Reaction 🧠➡️📨➡️⚡️
یک مثال معمول:
• OrderPlaced
• PaymentCaptured
• OrderShipped
اما یک مشکل وجود دارد… ⚠️
ءDomain Eventها وقتی میخواهید از آنها برای integration استفاده کنید، شکننده میشوند.
اگر مستقیماً از داخل transaction رویداد منتشر کنید،دارید درستی بیزینس را به یک side effect غیرقابل اعتماد گره میزنید:
🔸️ءtransaction موفق میشود ولی publish شکست میخورد ❌
🔹️ءpublish موفق میشود ولی transaction rollback میشود 🔙
🔸️مصرفکنندهها duplicate پردازش میکنند 🔁
🔹️ءretryها باعث reordering میشوند 🔀
پس ما مدل را نگه میداریم…
اما delivery را مقاوم (hardened) میکنیم. 🛡
The Outbox Makes Publishing Reliable (but not ordered)با Outbox، ما eventهای خروجی رو در همان transactionای ذخیره میکنیم که update روی aggregate انجام میشه. 🧾
ءOutbox انتشار رو قابلاعتماد میکنه (اما مرتب نه) 📦
بعد، یک background publisher میآید و Outbox رو میخونه و eventها رو به یک queue ارسال میکنه. 📤
این کار مشکل reliability رو حل میکنه:
• اگر transaction commit بشه، event ذخیره شده ✅
• اگر publisher کرش کنه، میتونه بعداً ادامه بده 🔄
میتونیم با خیال راحت retry کنیم ♻️
حالا انتشار eventها قابلاعتماد شده. 👍
اما هنوز ترتیب (ordering) در پردازش eventها تضمین نشده. ⚠️
Competing Consumers Are Great, Until Order Mattersبه محض اینکه eventها وارد queue میشن،
ءCompeting Consumerها عالیاند… تا وقتی ترتیب مهم نشه 🚦
معمولاً با سادهترین راه scale میکنیم: competing consumers.
چندین instance از یک queue مشترک مصرف میکنن تا throughput بالا بره 📈
این کار جواب میده…
تا زمانی که ترتیب اهمیت پیدا کنه ⛔️
دو event برای یک OrderId ممکنه همزمان پردازش بشن:
• ءConsumer A رویداد PaymentCaptured رو دریافت میکنه 💳
• ءConsumer B رویداد OrderPlaced رو دریافت میکنه 🛒
ءside effectها خارج از ترتیب اجرا میشن 🔀
حتی اگر eventها به ترتیب publish شده باشن،retry و redelivery میتونن ترتیب پردازش رو بههم بزنن 🔁
و حالا شما با یک باگ ظریف طرف هستید
که فقط زیر load بالا خودش رو نشون میده 🐛🔥
این همون نکتهی کلیدیه: queueها کار رو scale میکنن، نه invariantهای شما رو 🎯
چیزی که واقعاً میخوایم: ترتیب بهازای هر Aggregate 🔗
شما به یک خط مرتب برای همهچیز نیاز ندارید 🚫
شما به چند خط مرتب مستقل نیاز دارید،
یکی برای هر aggregate 🧵
این معمولاً منطقیه چون:
ءaggregateها از قبل مرزهای consistency رو مشخص میکنن 🧱
• ءeventها ذاتاً به ترتیب تولید میشن (v1، v2، v3 …) 🔢
• ترتیب «درست» همون timeline خود aggregate هست ⏱️
اگر بتونیم تضمین کنیم که
در هر لحظه فقط یک handler ،eventهای مربوط به یک aggregate خاص رو پردازش کنه، بخش بزرگی از مشکل حل میشه ✨
مستقیمترین راهحل، که در عین حال سادهترین هم هست:
👉 استفاده از یک consumer واحد برای کل stream
این کار ترتیب رو enforce میکنه،
به شرطی که eventها به ترتیب publish شده باشن ✅
اما این راهحل یک ایراد واضح داره… ⚠️