ملخص المقال: دليل عملي يفرّق بين الاستدعاء والحدث، ويشرح متى تنقل الرسالة بدل أن تطلب البيانات، وكيف تبني مواضيع تصير مصدرًا للحقيقة، وكيف تتعامل مع التكرار وإعادة المحاولة، وتضمن الترتيب بالأرقام لا بالطوابع، وتقود أحداث المجال.
طريقة القراءة: ابدأ بفهم المشكلة، ثم راجع القرار أو الخطوات العملية في الأقسام التالية، واستفد من المخطط لتصور الأجزاء المرتبطة ببعضها.
ماذا ستخرج به من هذا المقال؟
كل تكامل بين نظامين يبدأ بسؤال واحد: من يتحكم في التوقيت؟ في النمط القائم على الاستدعاء (Request/Response) تتصل بالطرف الآخر وتنتظر رده، فتحمل أنت مسؤولية توقفه وبطء شبكته وإجباره على أن يكون متاحًا الآن. أما في النمط القائم على الأحداث (Event-Driven) فأنت تُعلن عن واقعة انتهت فعلًا، ثم تترك الطرف الآخر يكتشفها متى شاء. يشرح هذا المقال متى ينفعك الثاني ومتى يضرّك، وكيف تبني مواضيع الرسائل (Topics) بحيث تصير مصدرًا للحقيقة لا قناة إشعارات، وماذا تفعل مع التكرار وإعادة المحاولة، ولماذا الطابع الزمني ليس ضمانة للترتيب، وكيف تصمّم أسماء أحداث المجال قبل أن تلتزم بعقد يصعب تغييره. والمخرج هنا عملي: نقاط قرار عن حدود المعاملة، وطريقة للتعامل مع الرسالة الفاشلة دون فقدان أثرها، ومنهج في كتابة حدث يبقى صالحًا للتفسير بعد سنوات.
الاستدعاء والحدث: الفرق في العقد لا في قناة النقل
الخلط الشائع يبدأ من التسمية: كلاهما «رسالة» تتبادلها شريكتان. لكن الفرق في مَن يملك السياق ومَن يتحمّل المسؤولية عند الفشل. في الاستدعاء تكون أنت الطرف المُنادى، وقد سمّيت الطرف الآخر مسبقًا وطلبت نتيجة محددة، وحق لك أن تنتظر إجابة تحمل قيمة أو خطأ. أما الحدث فيُخبر ولا يطلب: يقول «يحدث هذا» دون أن يحدّد من يفعل ماذا بعده، والناشر لا يعرف عدد المستهلكين ولا هوياتهم.
- الاقتران الزمني (Temporal Coupling): في الاستدعاء لا إجابة قبل أن يستجيب الطرف الآخر، وفي الحدث لا يوجد طرف ينتظر، فتكمل معاملتك محليًا ثم تنشر لاحقًا.
- مصدر الحقيقة: الاستدعاء يجلب قيمة مرجعية في لحظة معيّنة وهذا مريح لسؤال مثل «كم رصيد العميل الآن؟»، بينما يبني الحدث الحالة محليًا لدى كل مستهلك.
- مسار الفشل: فشل الاستدعاء ملفوف وواضح: مهلة أو رمز خطأ أو خدمة غير متاحة. فشل الحدث غير مرئي: قد لا يقرأه أحد، أو يُقرأ متأخرًا، أو يُرفض بعد أن نجح جزء منه.
- حجم التغيّر: إضافة مستهلك جديد لا تلمس الناشر إطلاقًا، بينما إضافة حقل في استجابة الاستدعاء قد تكسر مستهلكًا يقرأ الرد بحزم.
- زمن الاستجابة: الاستدعاء يناسب سؤالًا يحتاج إجابة قبل أن يكمل المستخدم فعله، والحدث يناسب أثرًا يقع بعد ثوانٍ أو ساعات دون انتظار.
مثال: بوابة دفع تنتظر موافقة البنك
عندما يدفع العميل لا يمكن تأجيل النتيجة: إما تُقبل العملية أو تُرفض. هذا استدعاء متزامن بامتياز، والتأخير فيه مكلف على المستخدم ومرتبط مباشرة بزمن إتاحة الطرف الآخر. لكن بعد قبول الدفع يُنشر حدث «تم الدفع» دون انتظار: إرسال الإيصال، وتحديث نقاط الولاء، وإشعار المخزون قد تحدث بعد دقائق وبغير ترتيب، ولم يُطلب منك رد على أي منها.
متى يبقى طلب البيانات هو الخيار الصحيح؟
التحوّل الشامل إلى الأحداث خطأ شائع أكبر من البقاء على الاستدعاء. بعض الأسئلة لا تحتمل التأخير ولا التجزئة، وبعض الإجراءات تحتاج إجابة قبل أن تُنفّذ أصلًا. والقاعدة العملية واحدة: إن كان المستخدم ينتظر نتيجة على الشاشة الآن، فلن ينفعه أن تبني الشاشة من جدول يُحدَّث لاحقًا دون أن تشرح له ذلك صراحةً.
| الحالة | الاختيار الأنسب | السبب |
|---|---|---|
| مستخدم ينتظر نتيجة فورية مثل البحث أو التحقق من صلاحية | استدعاء أو قراءة مباشرة | الحدث لا يجيب عن سؤال، ولا يُعرف وقت وصوله |
| التحقق قبل تنفيذ أمر، مثل تجاوز سقف الاعتماد | استدعاء | تحتاج نتيجة الطلب لتقرّر القبول أو الرفض |
| الحاجة إلى رؤية متسقة عبر عدة جداول | استدعاء أو معاملة واحدة | الحدث يبني الحالة من عدة مصادر متأخرة |
| إشعار أو اقتراح مستقل عن مسار الطلب | حدث | لا أحد ينتظره، وفشله لا يجب أن يُسقط العملية |
| تغذية عدة أنظمة بالوقائع نفسها | حدث | إعادة النشر أرخص من إعادة الاستدعاء من كل جهة |
لا تحوّل كل قراءة إلى حدث
إن نقلت كل قراءة إلى نموذج أحداث، تحمل على نفسك تكلفة نظام موزّع كامل: توقّع الطلبات، وإعادة المحاولة، والتعارضات، وتأخر البيانات، مقابل فائدة لا توفّرها في حالة السؤال المباشر. ابدأ من عملية تنتهي بتغيير حالة، واترك القراءة المباشرة كما هي.
مواضيع الرسائل كحقيقة موزّعة
حين يتحوّل الموضوع (Topic) من قناة إشعار إلى سجل دائم (Durable Log)، تتغيّر طريقة التفكير فيه. لم يعد «رسالة تُرسل وتُنسى»، بل جزء من بياناتك التي تُعاد قراءتها أثناء الاستعادة، أو في تصحيح خطأ، أو لبناء مستهلك جديد لم يكن موجودًا وقت النشر. من هنا قاعدة ذهبية: صمّم الموضوع كسجل بيانات لا كقناة إشعار.
- مدة الاحتفاظ (Retention): نافذتك للاستعادة. مدة قصيرة تعني أنك لن تستطيع إعادة بناء حالة بعد انقطاع في مستهلك، مهما كانت نسخك الاحتياطية سليمة.
- التقسيم (Partitioning): يحدّد التوازي بحد أقصى، وهو أيضًا ما يحدّد حدود الترتيب. مفتاح التقسيم قرار معماري لا تفصيل تشغيلي.
- مجموعات المستهلكين (Consumer Groups): كل مجموعة تقرأ السجل كاملًا بمفردها، فإضافة مجموعة جديدة تعني بناء نسخة ثانية من الحالة. خطّط لما ستبنيه منها قبل إنشائها.
- الإزاحة (Offsets): هي وعد التقدم. خيار «التسليم قبل حفظ الإزاحة» يفتح باب التكرار، وخيار «الحفظ قبل التسليم» يفتح باب الفقدان.
- الحجم والتكلفة: كل بايت تحتفظ به تكلفة تخزين دائمة. اضبط الاحتفاظ على الحد الأدنى الذي يخدم الاستعادة والتدقيق.
التسليم مرة واحدة على الأقل: التكرار قبل الإهمال
معظم الوسيطات تسلّم مرة واحدة على الأقل (At-Least-Once): قد تتكرر الرسالة إذا انقطع الاتصال بعد تنفيذ المعالجة وقبل حفظ الإزاحة. لذلك يُبنى تصميم المستهلك حول سؤال واحد: ماذا يحدث إن وصلت الرسالة نفسها مرتين؟ الجواب ليس الاعتماد على عدم التكرار، بل آثار جانبية آمنة عند التكرار. وعمليًا: مفتاح ثبات لكل رسالة يُحفظ في جدول محلي ضمن معاملة المعالجة نفسها، أو مفتاح طبيعي مستخرَج من محتوى الحدث مثل «الطلب 42 انتقل إلى حالة تأكيد».
interface EventEnvelope<T> {
id: string; // معرّف فريد لهذا الحدث تحديدًا
type: string; // payment.captured
aggregateId: string; // معرّف الكيان، وهو مفتاح التقسيم
aggregateVersion: number; // تسلسل الأحداث على الكيان نفسه
occurredAt: string; // وقت وقوع الواقعة في المصدر
correlationId: string; // يربط حوادث المعاملة الواحدة
payload: T;
}
async function consume(e: EventEnvelope<PaymentCaptured>) {
const key = `${e.aggregateId}:${e.aggregateVersion}:${e.type}`;
if (await processedEvents.exists(key)) return; // تكرار: تجاهل آمن
await withRetry(() => applyToLedger(e), {
attempts: 5,
backoff: (n) => 200 * 2 ** n + random(50) // تصاعدي مع عشوائية
});
await processedEvents.insert(key); // في نفس معاملة الأثر
}لاحظ ثلاث نقاط: مفتاح الثبات مشتق من هوية المعنى لا من معرّف عشوائي، وتسجيل المعالجة يحدث في المعاملة نفسها التي تُحدّث الأثر، والتأخير تصاعدي مع عشوائية حتى لا تتزاحم عدة نسخ من المستهلك على فشل متزامن. أما الرسائل التي استنفدت المحاولات فلا تُهمل بصمت: تُنقل إلى موضوع مخصص للأعطال (Dead Letter Topic) مع رقم المحاولة وآخر خطأ، ويصل إشعار إلى فريق التشغيل.
الترتيب والطوابع: ضمنية دقيقة مقابل ترتيب شامل
الترتيب من أكثر الادعاءات التي تُقال دون سند دقيق. ما يضمنه الوسيط فعليًا هو أن الرسائل التي تحمل نفس مفتاح التقسيم داخل موضوع واحد تُسلَّم بالترتيب إلى مجموعة مستهلكين واحدة. وما لا يضمنه هو الترتيب بين موضوعات مختلفة، أو بين أرقام نسخ متفاوتة، أو بين نسخ منتجة متعددة لا يربطها بينها تسلسل واحد.
الطابع الزمني ليس بديلًا عن الترتيب. ساعات الأجهزة تنحرف حتى مع ضبط بروتوكول الوقت، وتصبح إعادة ترتيب «أحدث عشر رسائل» متفاوتة بين مستهلكين إذا اعتمدوا على جدول زمني خارجي. الحل المعتمد هو رقم نسخة على مستوى الكيان (Aggregate Version): كل حدث يحمل رقمًا يزيد واحدًا لكل كيان، والمستهلك يقارن بما لديه. فإن كان الرقم أقل مما لديه فقد سبق تصله، وإن كان أعلى فقد فاتته أحداث، وأي فجوة تُعالَج بإعادة القراءة من مخزن المصدر لا بترجمتها إلى تخمين.
تحذير: ابنِ على الضمانات الضمنية
لا تفترض الترتيب بين موضوعين ولا بين عمليتي نشر. فالمستهلك الذي يبني رصيدًا ويصله حدث «تم الدفع» قبل حدث «فشل التفويض» بسبب مفتاح تقسيم خاطئ ينتج رقمًا نهائيًا خاطئًا يصعب اكتشافه لاحقًا. إن كان الكيان يحتاج ترتيبًا صارمًا فاجعل مفتاح التقسيم هو معرّف الكيان ولا شيء غيره، واختبر الفجوات صراحةً.
نمذجة أحداث المجال: الاسم والحقل الذي يبقى
الحدث الجيد اسم ماضٍ، وكيان محدد، ووقعة لا أمر. «أرسل تذكيرًا» أمر، أما «تم إرسال تذكير» فحدث. الفرق ليس شكليًا: الأمر يحمل التزامًا عليك بتنفيذه الآن، والحدث يصف واقعًا يستنتجه منه أكثر من مستهلك استنتاجات مختلفة دون أن يفسّره هو.
- صيغة الاسم:
entity.action.v1بصيغة ماضٍ واسم الكيان أولًا، مثلpayment.captured.v1أوaccount.closed.v1، وهذا يجعل القائمة قابلة للتجميع والبحث. - حقول ثابتة في كل حدث: المعرّف، والنوع، ومعرّف الكيان، ورقم النسخة، ووقت الوقوع، ومعرّف الارتباط (correlation)، وسبب التغيّر (causation)، والحمولة. هذه الهوية هي ما يجعل الحدث قابلًا للتتبّع عبر عدة أنظمة.
- حمولة صغيرة ذات معنى: لا تنسخ جدولًا كاملًا داخل الحدث؛ أرسل المعرّفات والقيم في لحظة الوقوع، ودع المستهلك يسأل عن التفاصيل الناقصة فقط.
- معالجة فجوات الماضي: من ينشئ حدثًا برقم نسخة أعلى عليه أن يعالج الأرقام الناقصة، أو ينشر حدث «اكتملت المزامنة» ليعرف المستهلك أن السلسلة وصلت إلى نهاية متسقة.
- سجل الأحداث (Event Catalog): قائمة مكتوبة بكل نوع حدث: معناه، ومن ينتجه، ومن يستهلكه، ومدة الاحتفاظ المطلوبة. في الأنظمة البنكية والمالية هذه القائمة هي الحدّ الأوّلي لأي تدقيق أو إصلاح.
قرار تصميمي: من يملك شكل الرسالة
الوسيط لا يفرض شكل الرسالة في أغلب المنصات، فاجعل مالك الشكل هو المنتج الذي يولّد المعنى لا فريق البنية التحتية. وكل حقل جديد يُكتب له معنى صريح في سجل الأحداث قبل النشر، لأن الحقول التي يقرأها أحدهم بصمت تتحول إلى عقد ضمني بعد ستة أشهر دون أن تعرف.
أخطاء شائعة
أخطاء تكلّف في الإنتاج
- نشر الحدث داخل المعاملة دون Outbox: إن فشل النشر بعد إتمام المعاملة تكون الحالة حُفظت والحدث ضاع. ضع سجل الحدث في نفس معاملة الكتابة، ثم انشره من هناك.
- الافتراض بأن الترتيب مضمون: الأحداث القادمة من مصادر أو موضوعات مختلفة تصل بترتيب غير متوقع، والأرقام والتواريخ لا تنقذك.
- الاعتماد على الوسيط لتسجيل التسليم: النجاح في الاستقبال ليس نجاحًا في المعالجة. إن لم تكتب الأثر في نفس معاملتك فستضاعفه، وإن لم تسجّله بعدها فستخسره.
- موضوع واحد كبير لكل شيء: خلط المبيعات بالمخزون والفوترة يعني معالجة كل شيء في كل مستهلك، وترتيبًا لا معنى له، ومقاييس لا تفسَّر.
- إهمال زمن الحداثة: مستهلك يتأخر ساعة عن المنتج قد يكتب فوق بيانات صحيحة بقرار مبني على حالة قديمة، خصوصًا إن لم يقارن بأرقام النسخ.
قائمة تحقق
- حدّد لكل تكامل: هل ينتظر المستخدم إجابة الآن فاستخدم استدعاء، أم لا ينتظر شيئًا فاستخدم حدثًا؟
- اكتب حدث المجال بصيغة ماضٍ باسم الكيان، وأضف معرّف الكيان ورقم النسخة في كل رسالة.
- اضبط مفتاح التقسيم على معرّف الكيان إذا كان الترتيب مهمًا، ورتّب المستهلك على رقم النسخة لا على الطابع الزمني.
- اجعل المستهلك متسامحًا مع التكرار: مفتاح ثبات لكل حدث يُحفظ في نفس المعاملة التي ينفّذ فيها الأثر.
- حدّد سياسة إعادة المحاولة: عدد المحاولات، وتأخير تصاعدي مع عشوائية، ووجهة الأعطال، وإشعار عند تجاوزها.
- اكتب في سجل الأحداث معنى كل نوع ومن ينتجه ومن يستهلكه، وراقب تأخر إدراج الأحداث وحجم موضوع الأعطال وعدد الفجوات المرصودة.
الأسئلة الشائعة
هل يمكن الاعتماد على ترتيب الرسائل من الوسيط؟
داخل موضوع واحد وبمفتاح تقسيم واحد، نعم: أحداث الكيان نفسه تُقرأ من قسم واحد بالترتيب. أما عبر الموضوعات، أو عند إعادة التقسيم (Repartitioning)، أو عند تشغيل عدة نسخ منتجة، فلا ضمان. لذلك اجعل المستهلك آمنًا إذا وصلته نسخة قديمة أو مكررة: قارن أرقام النسخ، وتجاوز ما سبق معالجته، ولا تبنِ نتيجة على ترتيب لم تضمنه.
ماذا أفعل إذا تغيّر شكل الرسالة بعد إطلاقها؟
لا تعدّل حقولًا قائمة في النوع نفسه. أضف نوعًا جديدًا بنسخة أعلى مثل payment.captured.v2 ينتجه المنتج إلى جانب القديم، ثم انتقل المستهلكون إليه بالتدريج. ولا تحذف حقلًا قبل أن تنتهي مدة الاحتفاظ بكل الرسائل التي تحمله؛ فإن كان الحذف لا مفر منه، فاتركه فارغًا ووجّه القراءات إلى القيم الفارغة بدل الإيقاف المفاجئ.
ماذا لو احتجت مستهلكًا جديدًا لبيانات لم تُنشر عنها أحداث من قبل؟
ابنِ الحالة الأولى بقراءة مباشرة (Backfill) من مخزن المصدر، ثم ابدأ تشغيل المستهلك من لحظة بعينها في السجل لا من أوله، وسجّل رقم النسخة الأخير الذي بنيت عليه، ثم اقبل الأحداث الأكبر منه فقط. هذا هو الجسر المعتاد بين «بيانات لم تكن أحداثًا بعد» و«نظام قائم على الأحداث».
الخلاصة
ما يجب أن يبقى معك
- الحدث مُعلم لا طلب: يُخبر ولا يسأل، ولذلك لا ينبغي أن يحمل وعدًا بنتيجة أو بردّ متوقع.
- إن كان المستخدم ينتظر إجابة على الشاشة فالتطلب المباشر ما زال الأداة الصحيحة، والأحداث للفعل اللاحق.
- التسليم مرة واحدة على الأقل واقع تصميمي لا عطل: اجعل الأثر قابلًا للتكرار بمفتاح ثبات في نفس المعاملة.
- الترتيب مضمون فقط بين أحداث الكيان نفسه على نفس التقسيم، ورقم النسخة هو أداتك لا الطابع الزمني.
- سمّ الحدث بالماضي، وضع معرّف الكيان ورقم النسخة في كل رسالة، واضبط مدة الاحتفاظ على أسوأ حالة استعادة تحتاجها.
الخطوة التالية
بعد أن يستقر قرارك بين الاستدعاء والحدث، تظهر طبقة جديدة من المسؤولية عند الحدّ الخارجي للنظام: اللحظة التي يصبح فيها مستهلكك هو الطرف الآخر. يتناول المقال التالي في هذا المسار تصميم Webhooks: توقيع الحمولة، وسياسة إعادة المحاولة التي تحترم موارد العميل قبل مواردك، ومصير الرسائل التي فشلت خمس مرات، وواجهة يستعلم بها العميل عن حالة التسليم. وهو الجسر من «نظام ينتج أحداثًا» إلى «شريك خارجي يستهلكها».
كيف تستخدم هذا المقال في عمل حقيقي؟
طبّق ما سبق على تكامل واحد محدّد بدل المنظومة كلها: اختر العملية التي تنتهي بتغيير حالة ولها أكثر من مستهلك، وارسم فيها حدّ المعاملة والموضع الدقيق لسطر كتابة الحدث، ثم اختبر الفشل عمدًا. واكتب في صفحة واحدة القيم التي اخترتها: مدة الاحتفاظ، وعدد المحاولات، ونافذة الترتيب التي تعتمد عليها. وإن كان نظامك يملك قناة أعطال قائمة اليوم، فالمهمة الأقرب إليها ربطها بلوحة مراقبة وإشعار بدل أن تبقى صندوقًا باردًا.
نقطة تحقق
مخطط يربط عناصر الموضوع
flowchart LR
A["طلب العميل"]
B["مصادقة وتوثيق"]
C["طبقة التكامل"]
D["الخدمة الخارجية"]
E["التحقق والتدقيق"]
A --> B
B --> C
C --> D
D --> E
التسلسل: طلب العميل ← مصادقة وتوثيق ← طبقة التكامل ← الخدمة الخارجية ← التحقق والتدقيق.
سيناريو تطبيقي ومراجعة
افترض أنك مسؤول عن تطبيق ما ورد في هذا المقال داخل فريق عمل. ابدأ بتحديد الهدف الذي يقيس النجاح في موضوع التكامل بالأحداث: متى ننقل الرسائل بدل طلب البيانات، ثم اكتب المدخلات المطلوبة والقيود والصلاحيات، وجرب حالة طبيعية وأخرى استثنائية قبل الاعتماد.
- حدد الافتراضات التي تحتاج إلى تأكيد في بيئتك.
- حوّل القرارات الأساسية الواردة أعلاه إلى خطوات أو اختبارات قابلة للتنفيذ.
- وثّق طريقة التراجع أو التصحيح إذا فشل أحد الاختبارات.
أسئلة للمراجعة
كيف أبدأ بتطبيق هذا الموضوع؟
اختر حالة استخدام واحدة، وطبّق أصغر جزء يحقق قيمة قابلة للقياس؛ ثم قارن النتيجة بالمتطلبات.
كيف أتحقق من جودة الحل؟
اختبر المسار المعتاد ومسار الخطأ، وراجع الأمان وسلامة البيانات والأداء وفق طبيعة الموضوع، ثم أعد الاختبار بعد أي تعديل.
الرد على التعليق