التواصل المتزامن مقابل غير المتزامن
التواصل المتزامن مقابل غير المتزامن
في معمارية الخدمات المصغّرة يُعدّ السؤال "كيف تتحدث الخدمات مع بعضها؟" من أكثر قرارات التصميم أثرًا. الخطأ فيه يحوّل النظام إلى مونوليث موزَّع — كل استدعاء يُعطّل، والأعطال تتسلسل، وتضيع مزايا المرونة التي وعدت بها الخدمات المصغّرة. يتناول هذا الدرس الأسلوبين الأساسيين — المتزامن (طلب/استجابة) وغير المتزامن (حدث/رسالة) — ويشرح مقايضاتهما، ويعرض كودًا اصطلاحيًا بـ Spring Boot 3 لكليهما.
التواصل المتزامن: REST وgRPC
في التواصل المتزامن ترسل الخدمة المُستدعِية طلبًا وتنتظر الاستجابة قبل المتابعة. أسلوب REST المستند إلى HTTP الذي تعرفه هو الشكل الأكثر شيوعًا. يُعدّ RestClient (المُقدَّم في Spring 6.1) والأقدم WebClient من WebFlux الأدواتَ المعيارية.
افترض وجود OrderService يحتاج إلى جلب تفاصيل المنتج من ProductService قبل تأكيد الطلب:
يُعرَّف إعداد عنوان URL الأساسي في application.yml حتى تستطيع البيئات المختلفة (محلي، تجريبي، إنتاج) توصيل عنوان الخدمة الحقيقي أو نموذج اختبار وهمي دون لمس الكود:
product-service:8081 مباشرةً. يحلّ Spring Cloud LoadBalancer الاسم المنطقي للخدمة إلى نسخة حية عبر Eureka أو DNS الخاص بـ Kubernetes. يبقى الكود متطابقًا — ويتغير عنوان URL الأساسي فقط إلى http://product-service وتُضاف التعليمة @LoadBalanced إلى الـ bean الخاص بـ RestClient.Builder.
متى يكون التواصل المتزامن منطقيًا
- تحتاج النتيجة فورًا — مثلًا: استجابة المُستدعي تعتمد على بيانات من خدمة أخرى.
- الاستدعاء للقراءة فحسب وسريع — جلب سعر المنتج للعرض منخفض الخطورة؛ وإذا فشل يمكن إعادة خطأ أو قيمة مؤقتة.
- يُشترط التناسق القوي — لا يمكن المتابعة دون معرفة الحالة الراهنة للمورد البعيد.
- دلالات طلب/استجابة بسيطة — بوابة API تواجه العملاء وتجمّع بيانات من خدمات داخلية تُمثّل حالة استخدام طبيعية.
التكلفة الخفية: الاقتران الزمني
تخلق الاستدعاءات المتزامنة اقترانًا زمنيًا: إذا كانت ProductService بطيئة أو غير متاحة، فإن OrderService ستكون كذلك. في سلسلة من ثلاث قفزات متزامنة بتوافرية 99.9% لكل منها تنخفض التوافرية المركّبة إلى ~99.7%. ومع عشر قفزات تهبط إلى ما دون 99%. أضف تضخيم زمن الاستجابة واستنزاف الخيوط (كل خيط منتظر يحتجز اتصالًا) وسترى لماذا سلاسل المتزامن الصرف هشّة عند الحجم الكبير.
التواصل غير المتزامن: المراسلة
في التواصل غير المتزامن ينشر المُرسِل رسالةً (أو حدثًا) في وسيط ويكمل فورًا — دون انتظار معالجة المستقبِل. يستهلك المستقبِل الرسالة بوتيرته الخاصة. هذا يفصل الخدمات زمنيًا: لا يهتم المُرسِل بما إذا كان المستقبِل يعمل أو مشغولًا أو يُعاد نشره.
يتكامل Spring Boot مع Apache Kafka عبر spring-kafka ومع RabbitMQ (AMQP) عبر spring-boot-starter-amqp. في المثال التالي ينشر OrderService حدث OrderPlaced، وتستهلكه InventoryService المستقلة لتقليل المخزون.
أضف التبعية:
عرِّف سجلّ الحدث (record في Java 16+ — ثابت حكمًا):
النشر من OrderService:
الاستهلاك في InventoryService:
يُهيَّأ وسيط Kafka واسم الموضوع واستراتيجية التسلسل في application.yml:
onOrderPlaced استثناءً، يمكن ضبط DefaultErrorHandler في Spring Kafka على إعادة المحاولة عددًا محددًا من المرات ثم إعادة توجيه الرسالة الفاشلة إلى موضوع orders.placed.DLT. بدون DLT، ستُوقف رسالة "سمّ الاستهلاك" المستهلكَ إلى الأبد.
متى يكون التواصل غير المتزامن منطقيًا
- سير عمل "أطلق وانسَ" — إرسال بريد تأكيد، تحديث فهرس البحث، إخطار التحليلات. لا يحتاج أي منها إلى تعطيل طلب المستخدم.
- التناسق النهائي مقبول — سيُحدَّث المخزون بعد وقت قصير من وضع الطلب؛ التأخر البسيط مقبول.
- الحمل العالي أو المتقطع — يمتص الوسيط الارتفاعات المفاجئة؛ يعالج المستهلكون بوتيرة ثابتة.
- العمليات طويلة الأمد — ترميز الفيديو، إنشاء التقارير، ملفات PDF للفواتير — العمل الذي يستغرق ثوانٍ أو دقائق لا ينبغي أن يحجب خيط HTTP.
- المروحة (Fan-out) — حدث
OrderPlacedواحد يُحفّز المخزون والفوترة والتنفيذ والتحليلات باستقلالية، دون أن تعرفOrderServiceعن أي منها.
الآثار الأمنية
لكلا الأسلوبين مخاوف أمنية مميزة يسهل إغفالها في سياق الخدمات المصغّرة.
المتزامن (HTTP): مرّر JWT الخاص بالمُستدعي إلى الخدمات الداخلية باستخدام ClientHttpRequestInterceptor على RestClient. لا تُعيد إصدار رمز على مستوى الخدمة يحمل صلاحيات أوسع مما يمتلكه المستخدم الأصلي — هذا ثغرة تصعيد صلاحيات.
غير المتزامن (Kafka/RabbitMQ): الوسيط مورد شبكي — تعامل معه كقاعدة بيانات. استخدم TLS للنقل وSASL/SCRAM أو mTLS للمصادقة وقوائم ACL حتى تستطيع OrderService فحسب إنتاج الرسائل في مواضيع orders.* دون استهلاكها أو إدارتها. أدرج correlationId ومُطالبة userId بسيطة في كل حمولة حدث لأغراض تسجيل التدقيق، لكن لا تضمّن رموز JWT الكاملة — فهي تنتهي صلاحيتها ولم تُصمَّم للتخزين.
الاختيار بين الأسلوبين: إطار اتخاذ القرار
- اسأل: هل يحتاج المُستدعي الإجابة للمتابعة؟ نعم ← متزامن. لا ← غير متزامن.
- اسأل: هل يمكن للمستقبِل أن يكون غير متاح مؤقتًا؟ يجب أن يكون متاحًا دائمًا ← متزامن مع قاطع دائرة. يمكنه اللحاق لاحقًا ← غير متزامن.
- اسأل: كم خدمة تهتم بهذا الحدث؟ خدمة واحدة محددة ← استدعاء متزامن. خدمات عديدة ← انشر حدثًا ودعها تشترك.
- معظم الأنظمة الحقيقية تستخدم الأسلوبين معًا: بوابة REST للتفاعل المواجه للمستخدم، مع أحداث مروحية داخلية للتأثيرات الجانبية.
الخلاصة
استدعاءات REST/gRPC المتزامنة بسيطة وتُقدّم استجابات فورية لكنها تخلق اقترانًا زمنيًا وخطر الأعطال المتسلسلة. المراسلة غير المتزامنة عبر Kafka أو RabbitMQ تفصل الخدمات زمنيًا وتمتص ارتفاعات الحمل وتُتيح أنماط المروحة، بتكلفة التناسق النهائي والبنية التحتية الإضافية. يدعم Spring Boot 3 كلا الأسلوبين بتكاملات من الدرجة الأولى: RestClient لـ HTTP المتزامن وspring-kafka / spring-amqp للمراسلة. يعتمد الخيار الصحيح على ما إذا كان المُستدعي يحتاج الإجابة فورًا، وعدد المستهلكين، وضمانات التناسق التي يتطلبها العمل.