We are still cooking the magic in the way!
الخدمات المصغّرة المدفوعة بالأحداث
الخدمات المصغّرة المدفوعة بالأحداث
في معمارية الطلب/الاستجابة التقليدية تستدعي إحدى الخدمات خدمةً أخرى وتنتظر الرد. هذا الاقتران مقبول في عمليات CRUD البسيطة، لكنه يصبح هشًّا على نطاق واسع: فالمستدعي غير متاح كلما كان المستدعَى غير متاح، وتتراكم زوايا الاستجابة عبر سلاسل الاستدعاء، وتتسرّب الأعطال من الخدمة المتأخرة إلى المستدعي. المعمارية المدفوعة بالأحداث تكسر هذا الاقتران. بدلًا من استدعاء خدمة مباشرةً تُنشر حدثًا يصف ما جرى، وتتفاعل معه أي عدد من الخدمات المهتمة باستقلالية تامة. المنتِج لا يعرف — ولا يهتم — من يستمع.
الأحداث مقابل الأوامر مقابل الاستعلامات
قبل كتابة الكود يستحقّ الأمر التمييز بين ثلاثة أشكال للرسائل:
- الأمر (Command) — توجيه موجَّه لخدمة بعينها: "عالِج الدفع للطلب 42." المُرسِل يعرف المستقبِل ويتوقع منه أن يتصرف.
- الاستعلام (Query) — طلب للبيانات: "ما رصيد الحساب 7؟" متزامن ويحمل استجابة.
- الحدث (Event) — إشعار بأن شيئًا ما قد وقع: "تم تقديم الطلب 42." لا يُملي الناشر ما يحدث بعد ذلك. الأحداث وقائع ثابتة بصيغة الماضي.
الأحداث هي لبنة البناء في الخدمات المصغّرة المدفوعة بالأحداث. صمّم أسماء أحداثك بصيغة الماضي (OrderPlaced، PaymentSucceeded، InventoryReserved) ودَع المستهلِكين يقرّرون باستقلالية كيفية التفاعل.
حافلة الأحداث في Spring (داخل العملية الواحدة)
يأتي Spring مزوّدًا بآلية أحداث متزامنة داخل العملية الواحدة مضمّنة في ApplicationContext. هي ليست وسيط رسائل — فالأحداث لا تعبر حدود العمليات — لكنها طريقة نظيفة لفصل المكوّنات داخل خدمة واحدة وتعلّم النمط قبل إضافة البنية التحتية.
ضَع تعليق @EventListener على طريقة المستمع وسيحقن Spring الحدث بالنوع. أضف @Async معه لتنفيذ المستمع في مجموعة مؤشرات ترابط منفصلة — لكن لاحظ أنه مع الأحداث غير المتزامنة داخل العملية قد يُفقد الحدث غير المنشور عند تعطّل الناشر.
عبور حدود العمليات بوسيط الرسائل
للخدمات المصغّرة الحقيقية تحتاج الأحداث إلى التنقّل بين عمليات JVM مختلفة. يعمل وسيط الرسائل (RabbitMQ أو Apache Kafka أو AWS SNS/SQS) بوصفه وسيطًا متينًا. يوفّر Spring Cloud Stream نموذجًا برمجيًا موحّدًا يعمل مع أي رابط مدعوم.
أضف رابط RabbitMQ إلى pom.xml:
مع Spring Cloud Stream تُنمذَج الرسائل كدوال Java عادية. المورّد (Supplier) ينتج الأحداث؛ والمستهلِك (Consumer) يتفاعل معها؛ والدالة (Function) تحوّلها. يربط Spring Cloud Stream هذه الدوال بقنوات الوسيط عبر الإعداد.
تُعلن الخدمة المستهلِكة عن bean بسيطة من نوع Consumer<T>:
group لروابط المستهلِكين. بدون مجموعة تستقبل كل نسخة من الخدمة كل رسالة (دلالات البثّ). مع مجموعة، يوزّع الوسيط الحِمل بين النسخ — وهذا تمامًا ما تريده لطوابير العمل.
نمط صندوق الصادر — ضمان التسليم مرة واحدة على الأقل
النمط المضاد الأشدّ خطورة في الأنظمة المدفوعة بالأحداث هو نشر حدث بعد إيداع معاملة قاعدة البيانات في خطوتين منفصلتين. إذا تعطّلت الخدمة بين الإيداع ومكالمة الوسيط فُقد الحدث للأبد — طلب وهمي بلا إشعار.
يحلّ نمط صندوق الصادر المعاملاتي (Transactional Outbox) هذه المشكلة: اكتب الحدث في جدول outbox ضمن نفس معاملة قاعدة البيانات التي تحتوي البيانات التجارية. يقرأ برنامج استطلاع منفصل (أو Debezium CDC) الصفوف غير المعالَجة وينشرها في الوسيط ثم يُعلّمها كمُرسَلة.
إصدارات الأحداث وتطوّر المخطّط
الأحداث واجهة برمجية عامة. قد يعتمد مستهلِكون لا تتحكّم فيهم على شكل OrderPlacedEvent. تطوير هذا الشكل دون كسر المستهلِكين يتطلّب انضباطًا:
- التغييرات الإضافية آمنة — أضف حقولًا اختيارية؛ يتجاهل المستهلِكون الحقول غير المعروفة (مع إعداد Jackson الافتراضي
FAIL_ON_UNKNOWN_PROPERTIES = falseفي Spring Boot). - حذف الحقول أو إعادة تسميتها يكسر التوافق — افصلها أولًا وانتظر حتى تُهاجر جميع المستهلِكين، ثم احذفها.
- إصدار أنواع أحداثك — استخدم حقل
versionأو وجّه التغييرات الكاسِرة إلى موضوع جديد (orders.placed.v2).
اعتبارات الأمان
للأنظمة المدفوعة بالأحداث سطح هجوم أوسع من استدعاءات REST المباشرة:
- اعتمد هوية المنتِجين — هيّئ SASL/TLS على الوسيط. أي شخص يستطيع الكتابة في
orders.placedيستطيع حقن أحداث احتيالية. - تحقّق من كل حدث — عامِل الرسائل الواردة بنفس الحذر الذي تعامل به طلبات HTTP. حمولة مشوّهة قد تُعطّل المستهلِك؛ وحمولة محقونة قد تُفسد البيانات.
- لا تضمّن البيانات الحساسة — تجنّب وضع معلومات تعريف شخصية أو بيانات بطاقات الدفع في الأحداث. انشر مرجعًا (معرّف الطلب) ودَع المستهلِك يجلب التفاصيل الحساسة من الخدمة المالكة عبر استدعاء API مؤمَّن.
- انقل سياق التتبع — ضمّن ترويسة W3C
traceparentفي بيانات وصف الرسالة حتى يستطيع التتبع الموزّع (المُغطَّى في الدرس 8) ربط التدفق الكامل عبر الخدمات.
الخلاصة
تستبدل الخدمات المصغّرة المدفوعة بالأحداث الاستدعاءات المتزامنة الهشّة بأحداث متينة يتوسّط فيها الوسيط. داخل الخدمة استخدم ApplicationEventPublisher و@EventListener من Spring لفصل المكوّنات. عبر الخدمات يوفّر Spring Cloud Stream مع رابط RabbitMQ أو Kafka نموذجًا قائمًا على الدوال حيث تكتب beans Java عادية من نوع Supplier وFunction وConsumer. استخدم نمط صندوق الصادر المعاملاتي لضمان التسليم مرة واحدة على الأقل دون إخفاق الكتابة المزدوجة. صمّم المستهلِكين ليكونوا مضمونَي الوحدانية، وطوّر مخطّطات أحداثك بصورة إضافية، وأمّن وصول الوسيط دائمًا. في الدرس 7 ستتعمّق في دلالات البثّ اللوغاريتمي في Kafka.