نمط Outbox
Outbox هو نمط معماري تكتب فيه خدمة المجال تغييرًا تجاريًا والحدث المقابل في معاملة محلية واحدة لمستودعها. يتم نشر الحدث إلى الحافلة/قائمة الانتظار الخارجية بشكل غير متزامن من خلال عملية آمنة منفصلة (ناشر) تقرأ جدول «outbox» وتنقل السجلات. يقضي هذا النهج على السباق «أولاً إلى قاعدة البيانات، ثم إلى الحافلة» ويوفر تسليمًا موثوقًا به حتى في حالة الفشل.
1) متى يتم التقديم
مناسبة:- Microservices and modular monoliths with events between contexts.
- مطلوب لضمان «إصلاح الدولة ↔ لا يمكن فقدان الحدث».
- نحن بحاجة إلى الغباء وإعادة التسليم الخاضعة للرقابة.
- تعد المعاملات العالمية الصعبة على العديد من الموارد أمرًا بالغ الأهمية (أفضل من TCC/sagas بعقود صريحة).
- لا يوجد مصدر مخصص للحقيقة (لا يتم تخزين الدولة حيث يتم إنشاء الحدث).
2) الأهداف والممتلكات
الكتابة الذرية: سجل النطاق + صندوق الخروج - في معاملة واحدة.
النشر مرة واحدة على الأقل: نسمح بالتكرار، باستثناء الخسارة.
خمول المستهلك: الحماية ضد المشتركين.
فعال مرة واحدة بالضبط: يتحقق من خلال الجمع بين outbox + المستهلك الخفي + dedup.
القياس عن بعد - ربط المعاملات والأحداث التجارية.
3) مخطط البيانات (مثال)
sql
-- Domain table (example: orders)
CREATE TABLE orders (
id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
status TEXT NOT NULL,
total_amount NUMERIC(12,2) NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT now()
);
-- Outbox
CREATE TABLE outbox (
id UUID PRIMARY KEY, -- event_id aggregate_type TEXT NOT NULL, -- 'order'
aggregate_id UUID NOT NULL, -- order_id tenant_id TEXT NOT NULL,
type TEXT NOT NULL, -- 'OrderCreated'
payload JSONB NOT NULL, -- serialized headers event JSONB NOT NULL DEFAULT '{}':: jsonb,
occurred_at TIMESTAMP NOT NULL, -- time in domain transaction available_at TIMESTAMP NOT NULL, -- earliest publish time (backoff)
published_at TIMESTAMP, - is filled by the attempts INT NOT NULL DEFAULT 0,
error TEXT
);
CREATE INDEX ON outbox (available_at) WHERE published_at IS NULL;
CREATE INDEX ON outbox (tenant_id, available_at) WHERE published_at IS NULL;
4) طبقة التطبيق
pseudo begin tx domainChange () # INSERT/UPDATE in domain table insert into outbox (event) # event with aggregate/tenant commit tx keys
إذا كان الالتزام ناجحًا، فمن المؤكد أن الحدث في صندوق الخروج موجود. إذا تم إسقاط الطلب بعد الالتزام، فسوف يلحق الناشر بالركب.
5) ناشر (قارئ → ناشر)
المهام:- اقرأ بشكل دوري الأحداث غير المنشورة ("published _ at IS NULl' و" available _ at <= now () ")، دفعات.
- حاول النشر إلى الحافلة/قائمة الانتظار ؛ إذا نجحت، ضع علامة «منشورة _ على».
- في حالة الخطأ - زيادة 'المحاولات'، وضع 'متاح في' للمستقبل (التراجع الأسي)، اكتب 'خطأ'.
- احترم الحدود المفروضة على المستأجرين/المفاتيح (الإنصاف)، ولا تحجب المنتج.
pseudo loop:
events = select from outbox where published_at is null and available_at <= now()
order by occurred_at limit BATCH_SIZE for update skip locked
for e in events:
try:
broker. publish(topicFor(e), serialize(e. payload), headers(e))
markPublished(e. id, now())
except Retryable:
backoff = computeBackoff(e. attempts)
reschedule(e. id, now()+backoff, attempts+1, last_error)
except NonRetryable:
moveToDLQ (e) or markError (e) # by sleep (POLL_INTERVAL) policy
6) الفراغ والتفريط
على جانب المستهلك (متجر Inbox/Idempotency):sql
CREATE TABLE inbox (
consumer_name TEXT,
event_id UUID,
processed_at TIMESTAMP NOT NULL,
PRIMARY KEY (consumer_name, event_id)
);
الخوارزمية: عند تلقي حدث ما، حاول أولاً «إدراج» في «صندوق الوارد» ؛ إذا كان هناك نزاع رئيسي، فقد تم التعامل مع الحدث بالفعل → عدم وجود عملية. التالي هو منطق العمل.
على جانب الناشر: «Idempotency-Key» في الرؤوس (على سبيل المثال، «event _ id») بحيث يمكن للناقل/السمسار/الوكيل تصفية النسخ المكررة.
7) الترتيب والسببية
يتم توفير النظام المحلي بواسطة "agregate _ id" عن طريق فرز "المعالجة _ at' والنشر" حسب المفتاح ".
بالنسبة لحافلات السجل مع التقسيم - التقسيم مع مفتاح «agregate _ id »/« المستأجر _ id» بحيث تكون أحداث مجمع واحد في نفس التقسيم.
إذا كان النظام أمرًا بالغ الأهمية، فتجنب سباقات الناشر ذات المفتاح الواحد.
8) مركز السيطرة على الأمراض (التقاط بيانات التغيير)
بدلاً من الناشر النشط، يمكنك استخدام CDC: يقرأ المحرك سجل معاملات قاعدة البيانات ويترجم خطوط «outbox» إلى الحافلة. الإيجابيات - الحد الأدنى من الحمل على قاعدة البيانات، التسلسل الدقيق، عدم الاقتراع. المساوئ - تعقيد العملية وربط تفاصيل DBMS. كلا النهجين صحيحان ؛ الاختيار حسب الكفاءات و SLO.
9) الأخطاء و DLQ و Redrive
Retryable (network، limits) - زيادة "attuges"، وتأجيل "available _ at' (التراجع الأسي + jitter).
غير قابل للإعادة (مخطط/عقد غير صالح) - تم نقله إلى DLQ/Dead-Letter Topic مع بيانات وصفية غنية.
Safe Redrive: دفعات، حد السعر، التحقق من صحة المخطط، الأولوية أقل من حركة الإنتاج.
10) تعدد الإيجارات والحدود
العلامات المطلوبة: "مستأجر _ معرف"، "خطة"، "منطقة" - في "outbox. الرؤوس ".
الإنصاف لكل مستأجر: يوزع الناشر «نوافذ» المنشورات وحدود محاولات المستأجرين.
الإقامة: صندوق التخزين الخارجي في نفس المنطقة مثل بيانات النطاق ؛ المنشورات الأقاليمية - المجاميع/الموجزات فقط.
11) السلامة والامتثال
إصدار PII في الحمولة/الرؤوس في سياسة المستأجر/المنطقة.
توقيع/تشفير الحمولة إذا كانت الحافلة أجنبية.
تدقيق جميع التحولات الحكومية: إنشاء، نشر، خطأ، إعادة صياغة.
12) إمكانية الملاحظة
المقاييس:- تأخر النشر ('الآن - occurred_at' p50/p95/p99).
- معدل النجاح، معدل الخطأ، سبب التوزيع.
- حجم Outbox (عدد غير منشور)، إعادة/ثانية
- الرسوم البيانية لكل مستأجر الإنتاجية والتأخر.
- الارتباط 'حدث _ معرف '/' مجمع _ معرف '/' ملحمة _ معرف' ؛ يمتد «db-tx»، «publish»، «retry».
- التعليقات التوضيحية: "محاولة"، "تراجع _ ms'،" dlq = صحيح ".
- سجلات قصيرة للنجاح ؛ التفاصيل الكاملة لكل خطأ/إعادة صياغة.
13) الاختبار والفوضى
اختبار الذرة: «يسقط» بشكل مصطنع بعد ارتكاب معاملة مجال قبل النشر - يجب إصدار الحدث لاحقًا.
اختبار مكرر: ننشر نفس الحدث عدة مرات - يؤدي المستهلك تأثيرًا واحدًا بالضبط (صندوق الوارد).
اختبار الطلب: مجموعة من الأحداث بمجموع واحد - فحص التسلسل/الخصوصية.
الفوضى: فشل الوسيط، زيادة زمن انتقال قاعدة البيانات، ناشري تقسيم الدماغ، انحراف الساعة.
14) نماذج التكوين (مثال)
yaml outbox:
poll_interval_ms: 200 batch_size: 200 order_by: occurred_at backoff:
strategy: exponential_full_jitter initial_ms: 250 max_ms: 10_000 max_attempts: 20 fairness:
per_tenant_parallelism: 4 per_key_serial: true
publisher:
rate_limit_per_sec: 500 headers:
idempotency_key: event_id schema_version: v3 dlq:
enabled: true topic: myapp. events. dlq include_metadata:
- error
- attempts
- source_table
- tenant_id
- aggregate_id
15) التكامل مع الملحمات والخلوات
Outbox - «النقل الأمني» لخطوات الملحمة: تكتب المعاملات المحلية الأثر والأمر/الحدث ؛ النشر - موثوق به وجرعة.
يجب أن تكون سياسات التكرار والتراجع متسقة مع «Retry-After» و Circuit Breaker ؛ تجنب «عاصفة العودة».
16) أخطاء نموذجية
يكتبون حدثًا بعد التزام حالة المجال - الخسارة أثناء السقوط ممكنة.
لا توجد فهارس/أرشيف في «خارج الصندوق» → زيادة زمن النشر.
الناشر بدون «SKIP LOCKED» أو بدون شحن - المنافسة والحجب.
نقص الخصوصية بين المستهلكين - الازدواجية والآثار الجانبية.
خلط PII دون إخفاء في DLQ/logs.
طابور نشر عالمي واحد بدون عدالة - مستأجر «صاخب» يبطئ الجميع.
عدم رصد التأخر → التدهور الكامن.
17) اختيار الإستراتيجية السريع
مستوى البداية: الاقتراع من قاعدة البيانات، 100-500 دفعة، عودة كاملة، صندوق الوارد للمستهلكين.
الحمل المرتفع: CDC من سجل المعاملات، الشحن بواسطة «المستأجر _ المعرف/المجمع _ المعرف»، WFQ حسب المستأجر.
الترتيب الصارم حسب المجموع: النشر المتسلسل لكل مفتاح (mutex)، تقسيم الموضوع بمفتاح.
الامتثال/PII: تشفير الحمولة، إصدار DLQ، صندوق الخروج الإقليمي.
18) قائمة مرجعية قبل البيع
- يتغير المجال ويكتب إلى «outbox» يحدث في نفس المعاملة.
- يتعامل الناشر مع الدفعات، ويستخدم «SKIP LOCKED»، ويتراجع مع النبض والحدود.
- المستهلكون غير فاعلين (الجدول «inbox »/deadup log).
- تم تكوين DLQ والإصدار الآمن.
- مقاييس التأخر/الخطأ والتنبيه على العتبات p95/p99.
- الترتيب الرئيسي مضمون (دفعات/مسلسلات).
- الأرشيف/الاحتفاظ بـ «صندوق خارجي» ومسح السجلات المنشورة.
- سياسات معهد بنغلاديش الدولي ومراجعة الحسابات الانتقالية للدولة.
- إسقاط الاختبارات بين الالتزام والنشر، والاستنساخ والطلب.
- وثائق عقود الأحداث (المخططات/النسخ/التوافق).
استنتاج
يحول نمط outbox الحزمة «الهشة» من «حافلة ↔ DB» إلى خط أنابيب موثوق به: تثبيت الحالة الذرية، والنشر المضمون (وإن كان «مرة واحدة على الأقل»)، والمشتركين الأغبياء وإعادة السحب الخاضعة للرقابة. مع القياس عن بعد المناسب، والقيود، وانضباط المخطط، فإنه يعطي سلوكًا عمليًا مرة واحدة بالضبط، مما يقلل من تعقيد المعاملات الموزعة ويزيد من مرونة النظام في مواجهة الأعطال وذروة الأحمال.