الرسائل غير المتزامنة
الرسائل غير المتزامنة
كل نظام موزَّع يصطدم في نهاية المطاف بالحاجز ذاته: إذا استدعت الخدمة A الخدمةَ B مباشرةً عبر HTTP وكانت B بطيئة أو متوقفة، فإن A ستصبح بطيئة أو متوقفة أيضًا. التبعية هنا تامة. الرسائل غير المتزامنة تكسر هذا الترابط بوضع وسيط رسائل (message broker) بين الخدمتين. تنشر A رسالة وتواصل عملها فورًا، وتستهلك B الرسالة متى كانت مستعدة. لا تعرف إحداهما عنوان الأخرى ولا حالة تشغيلها ولا حِملها الحالي.
يغطي هذا الدرس المفاهيم والتوصيل البرمجي مع Spring Boot والمفاضلات التشغيلية التي تحتاجها لاتخاذ قرارات تصميمية واعية — لا مجرد نسخ كود يعمل.
ما الذي يقدمه وسيط الرسائل
الوسيط هو خادم (RabbitMQ أو Apache Kafka أو Amazon SQS وغيرها) يقبل الرسائل من المنتجين (producers) ويحتفظ بها حتى يسحبها المستهلكون (consumers) أو يستقبلوها. يوفر الوسيط:
- الاستدامة — تبقى الرسائل بعد إعادة تشغيل المستهلك (مع تفعيل المثابرة).
- الضغط العكسي — إذا كان المستهلك بطيئًا تتراكم الرسائل في الطابور بدلًا من إفشال المنتج.
- البث للمتعددين — يمكن تسليم رسالة واحدة منشورة إلى مستهلكين مستقلين متعددين.
- الفصل الزمني — لا يحتاج المنتج والمستهلك إلى التشغيل في الوقت ذاته.
المفاهيم الأساسية: التبادلات والطوابير والمواضيع
يستخدم RabbitMQ نموذج exchange → binding → queue. ينشر المنتج إلى exchange وليس إلى طابور مباشرةً. يوجّه الـ exchange الرسالة إلى طابور واحد أو أكثر استنادًا إلى مفتاح التوجيه وقواعد الربط. تشترك تطبيقات المستهلك في الطوابير. أنواع الـ exchange الشائعة:
- Direct — يوجّه إلى الطوابير التي يطابق مفتاح ربطها مفتاح التوجيه تمامًا.
- Topic — مفاتيح التوجيه أنماط مفصولة بنقاط؛
*تطابق كلمة واحدة،#تطابق صفرًا أو أكثر. - Fanout — يتجاهل مفاتيح التوجيه ويبث إلى كل الطوابير المرتبطة.
يستخدم Kafka مفردات مختلفة — topics وpartitions — لكن مبدأ الفصل متطابق. نركز هنا على RabbitMQ عبر Spring AMQP؛ ويخصّص Kafka درسًا مستقلًا.
إضافة Spring AMQP إلى مشروع Spring Boot 3
أضف المبدئ (starter) إلى pom.xml:
اضبط اتصال الوسيط في application.yml:
acknowledge-mode: manual في الإنتاج. مع التأكيد التلقائي يحذف الوسيط الرسالة فور تسليمها حتى لو رمى الكود استثناءً. التأكيد اليدوي يتيح لك التأكيد بعد المعالجة الناجحة، فتُعاد الرسائل غير المعالجة إلى الطابور تلقائيًا.
تعريف البنية التحتية كـ Beans
يمكن لـ Spring AMQP تعريف الـ exchange والطابور والـ binding تلقائيًا عند بدء التشغيل. عرّفها كـ beans في فئة إعداد:
إنتاج رسالة
RabbitTemplate هو المكوّن المركزي الآمن للخيوط (thread-safe) للإرسال. أحقنه وانشر:
بشكل افتراضي تستخدم convertAndSend تسلسل Java. تجاوز ذلك بتعريف Bean من نوع MessageConverter:
استهلاك رسالة
زيّن دالة بـ @RabbitListener ويُنشئ Spring حاوية مستمعة تستطلع الطابور في تجمّع خيوط خلفية:
الثباتية (Idempotency): أهم عقد المستهلك
لأن الرسائل يمكن تسليمها أكثر من مرة (عطل شبكي، تعطل المستهلك قبل التأكيد)، يجب أن يكون المستهلك ثابتًا (idempotent): معالجة الرسالة ذاتها مرتين يجب أن تُعطي نتيجةً مطابقة لمعالجتها مرة واحدة. استراتيجيات شائعة:
- احتفظ بجدول معرّفات الرسائل المعالجة؛ تجاهل ما سبق رؤيته.
- استخدم قيودًا فريدة في قاعدة البيانات لتفشل الإدخالات المكررة بصمت.
- صمّم العمليات لتكون ثابتةً بطبيعتها (تعيين قيمة لـ X آمن للتكرار؛ زيادة عداد ليست كذلك).
اعتبارات الأمان
وسيط الرسائل هو حد ثقة. احمه:
- TLS أثناء النقل — استخدم
spring.rabbitmq.ssl.enabled=trueمع TLS المتبادل بين الخدمات والوسيط. - بيانات اعتماد لكل خدمة — تحصل كل خدمة مصغّرة على مستخدم RabbitMQ خاص بها بأقل صلاحيات ضرورية (قراءة من طابورها فقط، كتابة إلى تبادلها فقط).
- سلامة الرسالة — إذا عبرت الرسائل حدود الثقة، وقّع الحمولة (مثلًا HMAC-SHA256) وتحقق منها قبل المعالجة. وإلا قد يُدخل منتج مارق رسائل احتيالية.
- لا بيانات شخصية في مفاتيح التوجيه — يمكن أن تظهر مفاتيح التوجيه في سجلات الوسيط؛ اجعلها هيكلية لا حاملة للبيانات.
الاختيار بين HTTP المتزامن والرسائل غير المتزامنة
لا يسود أيٌّ من النمطين عالميًا. استخدم هذا الحكم التجريبي:
- استخدم HTTP حين يحتاج المُستدعي إجابةً فورية (مثلًا استعلام عن سعر منتج قبل عرضه للمستخدم).
- استخدم الرسائل حين يحتاج المُستدعي فقط أن يعرف أن العمل قُبِل لا أنه اكتمل (مثلًا تقديم طلب أو إرسال إشعار أو تشغيل تقرير خلفي).
تزيد الرسائل غير المتزامنة المرونةَ والإنتاجيةَ لكنها تضيف تعقيدًا تشغيليًا: تحتاج وسيطًا يعمل ومعالجةً للرسائل الميتة ومراقبةً للمستهلكين ومنطق ثباتية. هذا الثمن يستحق دفعه حين يحتاج نظامك إليه فعلًا.
الخلاصة
تفصل الرسائل غير المتزامنة الخدماتِ في الزمان والمكان: يستدعي المنتج convertAndSend ويواصل عمله؛ يعالج المستهلك بإيقاعه الخاص. تُغلّف Spring AMQP الـ RabbitMQ بـ RabbitTemplate للإرسال و@RabbitListener للاستقبال. استخدم Jackson2JsonMessageConverter للتشغيل البيني، والتأكيدات اليدوية للأمان، وتبادل الرسائل الميتة لرؤية الفشل، وصمّم كل مستهلك ليكون ثابتًا. في الدرس التالي ستتعلم كيف تتحد هذه الأنماط لبناء خدمات مصغّرة مدفوعة بالأحداث بالكامل.