اتصالات الخدمات المصغرة: برنامج تعليمي للتكامل الربيعي مع Redis
نشرت: 2022-03-11تعد بنية الخدمات المصغرة أسلوبًا شائعًا جدًا في تصميم وتنفيذ تطبيقات الويب القابلة للتطوير بدرجة كبيرة. عادة ما يعتمد الاتصال داخل تطبيق مترابط بين المكونات على استدعاءات الطريقة أو الوظائف في نفس العملية. من ناحية أخرى ، فإن التطبيق القائم على الخدمات المصغرة هو نظام موزع يعمل على أجهزة متعددة.
يعد الاتصال بين هذه الخدمات المصغرة أمرًا مهمًا من أجل الحصول على نظام مستقر وقابل للتطوير. هناك طرق متعددة للقيام بذلك. يعد الاتصال المستند إلى الرسائل إحدى الطرق للقيام بذلك بشكل موثوق.
عند استخدام المراسلة ، تتفاعل المكونات مع بعضها البعض من خلال تبادل الرسائل بشكل غير متزامن. يتم تبادل الرسائل عبر القنوات.
عندما تريد الخدمة "أ" الاتصال بالخدمة "ب" ، بدلاً من إرسالها مباشرةً ، يرسلها "أ" إلى قناة معينة. عندما تريد الخدمة "ب" قراءة الرسالة ، فإنها تلتقط الرسالة من قناة رسالة معينة.
في هذا البرنامج التعليمي Spring Integration ، ستتعلم كيفية تنفيذ المراسلة في تطبيق Spring باستخدام Redis. سيتم توجيهك عبر تطبيق مثال حيث تقوم إحدى الخدمات بدفع الأحداث في قائمة الانتظار وتقوم خدمة أخرى بمعالجة هذه الأحداث واحدة تلو الأخرى.
تكامل الربيع
يعمل مشروع Spring Integration على توسيع إطار عمل Spring لتقديم الدعم للرسائل بين التطبيقات المستندة إلى Spring أو داخلها. يتم توصيل المكونات معًا عبر نموذج المراسلة. قد لا تكون المكونات الفردية على دراية بالمكونات الأخرى في التطبيق.
يوفر Spring Integration مجموعة واسعة من الآليات للتواصل مع الأنظمة الخارجية. محولات القناة هي إحدى هذه الآليات المستخدمة للتكامل أحادي الاتجاه (إرسال أو استقبال). وتستخدم البوابات لسيناريوهات الطلب / الرد (واردة أو صادرة).
Apache Camel هو بديل يستخدم على نطاق واسع. يُفضل عادةً تكامل الربيع في الخدمات القائمة على الربيع لأنه جزء من نظام الربيع البيئي.
ريديس
Redis هو مخزن بيانات سريع للغاية في الذاكرة. يمكن أن تستمر اختياريًا على القرص أيضًا. وهو يدعم هياكل بيانات مختلفة مثل أزواج بسيطة ذات قيمة رئيسية ومجموعات وقوائم الانتظار وما إلى ذلك.
إن استخدام Redis كقائمة انتظار يجعل مشاركة البيانات بين المكونات والقياس الأفقي أسهل بكثير. يمكن لمنتج أو العديد من المنتجين دفع البيانات إلى قائمة الانتظار ، ويمكن للمستهلك أو العديد من المستهلكين سحب البيانات ومعالجة الحدث.
لا يمكن للعديد من المستهلكين استهلاك نفس الحدث - وهذا يضمن معالجة حدث واحد مرة واحدة.
فوائد استخدام Redis كقائمة انتظار للرسائل:
- التنفيذ المتوازي للمهام المنفصلة بطريقة غير معطلة
- أداء رائع
- استقرار
- سهولة المراقبة والتصحيح
- سهولة التنفيذ والاستخدام
قواعد:
- يجب أن تكون إضافة مهمة إلى قائمة الانتظار أسرع من معالجة المهمة نفسها.
- يجب أن تكون المهام المستهلكة أسرع من إنتاجها (وإذا لم يكن الأمر كذلك ، فأضف المزيد من المستهلكين).
تكامل الربيع مع Redis
فيما يلي خطوات عملية إنشاء نموذج تطبيق لشرح كيفية استخدام Spring Integration مع Redis.
لنفترض أن لديك تطبيقًا يسمح للمستخدمين بنشر المنشورات. وتريد بناء ميزة متابعة. مطلب آخر هو أنه في كل مرة ينشر فيها شخص ما منشورًا ، يجب إخطار جميع المتابعين عبر بعض قنوات الاتصال (على سبيل المثال ، البريد الإلكتروني أو إشعار الدفع).
تتمثل إحدى طرق تنفيذ ذلك في إرسال بريد إلكتروني إلى كل متابع بمجرد أن ينشر المستخدم شيئًا ما. ولكن ماذا يحدث عندما يكون لدى المستخدم 1000 متابع؟ وعندما ينشر 1000 مستخدم شيئًا ما في 10 ثوانٍ ، يكون لكل منهم 1000 متابع؟ أيضًا ، هل سينتظر منشور الناشر حتى يتم إرسال جميع رسائل البريد الإلكتروني؟
الأنظمة الموزعة تحل هذه المشكلة.
يمكن حل هذه المشكلة المحددة باستخدام قائمة انتظار. الخدمة أ (المنتج) ، المسؤولة عن نشر المنشورات ، ستفعل ذلك. ستقوم بنشر منشور ودفع حدث مع قائمة المستخدمين الذين يحتاجون إلى تلقي بريد إلكتروني والمنشور نفسه. يمكن جلب قائمة المستخدمين في الخدمة B ، ولكن لتبسيط هذا المثال ، سنرسلها من الخدمة A.
هذه عملية غير متزامنة. هذا يعني أن الخدمة التي تقوم بالنشر لن تضطر إلى الانتظار لإرسال رسائل البريد الإلكتروني.
ستقوم الخدمة B (المستهلك) بسحب الحدث من قائمة الانتظار ومعالجته. بهذه الطريقة ، يمكننا بسهولة توسيع نطاق خدماتنا ، ويمكن أن يكون لدينا n من المستهلكين يرسلون رسائل بريد إلكتروني (معالجة الأحداث).
لذلك لنبدأ بتنفيذ في خدمة المنتج. التبعيات الضرورية هي:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-redis</artifactId> </dependency>تبعيات Maven الثلاثة هذه ضرورية:
- Jedis هو عميل Redis.
- تسهل تبعية Spring Data Redis استخدام Redis في Java. يوفر مفاهيم Spring المألوفة مثل فئة القالب لاستخدام واجهة برمجة التطبيقات الأساسية والوصول الخفيف إلى البيانات على غرار المستودعات.
- Spring Integration يوفر Redis امتدادًا لنموذج البرمجة Spring لدعم أنماط تكامل المؤسسة المعروفة.
بعد ذلك ، نحتاج إلى ضبط عميل Jedis:
@Configuration public class RedisConfig { @Value("${redis.host}") private String redisHost; @Value("${redis.port:6379}") private int redisPort; @Bean public JedisPoolConfig poolConfig() { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(128); return poolConfig; } @Bean public RedisConnectionFactory redisConnectionFactory(JedisPoolConfig poolConfig) { final JedisConnectionFactory connectionFactory = new JedisConnectionFactory(); connectionFactory.setHostName(redisHost); connectionFactory.setPort(redisPort); connectionFactory.setPoolConfig(poolConfig); connectionFactory.setUsePool(true); return connectionFactory; } } يعني التعليق التوضيحي @Value أن Spring سيضخ القيمة المحددة في خصائص التطبيق في الحقل. هذا يعني أنه يجب تحديد قيم redis.host و redis.port في خصائص التطبيق.
الآن ، نحتاج إلى تحديد الرسالة التي نريد إرسالها إلى قائمة الانتظار. يمكن أن تبدو رسالة المثال البسيط كما يلي:
@Getter @Setter @Builder public class PostPublishedEvent { private String postUrl; private String postTitle; private List<String> emails; } ملاحظة: يوفر Project Lombok (https://projectlombok.org/)Getter و @Getter و @Builder والعديد من التعليقات التوضيحية الأخرى لتجنب ازدحام التعليمات البرمجية مع @Setter والمحددات والأشياء التافهة الأخرى. يمكنك معرفة المزيد عنها من هذا المقال Toptal.
سيتم حفظ الرسالة نفسها بتنسيق JSON في قائمة الانتظار. في كل مرة يتم فيها نشر حدث في قائمة الانتظار ، سيتم إجراء تسلسل للرسالة إلى JSON. وعند الاستهلاك من قائمة الانتظار ، سيتم إلغاء تسلسل الرسالة.
مع تحديد الرسالة ، نحتاج إلى تحديد قائمة الانتظار نفسها. في Spring Integration ، يمكن إجراؤه بسهولة عبر تكوين .xml . يجب وضع التكوين داخل دليل resources/WEB-INF .
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:int="http://www.springframework.org/schema/integration" xmlns:int-redis="http://www.springframework.org/schema/integration/redis" xsi:schemaLocation="http://www.springframework.org/schema/integration/redis http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <int-redis:queue-outbound-channel-adapter channel="eventChannelJson" serializer="serializer" auto-startup="true" connection-factory="redisConnectionFactory" queue="my-event-queue" /> <int:gateway service-interface="org.toptal.queue.RedisChannelGateway" error-channel="errorChannel" default-request-channel="eventChannel"> <int:default-header name="topic" value="queue"/> </int:gateway> <int:channel/> <int:channel/> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> <int:object-to-json-transformer input-channel="eventChannel" output-channel="eventChannelJson"/> </beans>في التكوين ، يمكنك رؤية الجزء "int-redis: queue-outbound-channel-adaptor." خصائصه هي:
- المعرف: اسم الفول للمكون.
- القناة: قناة
MessageChannelالتي تتلقى منها نقطة النهاية هذه الرسائل. - مصنع التوصيل: إشارة إلى فول
RedisConnectionFactory. - queue: اسم قائمة Redis التي يتم تنفيذ عملية الدفع المستندة إلى قائمة الانتظار لإرسال رسائل Redis. هذه السمة حصرية بشكل متبادل مع تعبير قائمة الانتظار.
- queue-expression: تعبير SpEL لتحديد اسم قائمة
#rootباستخدام الرسالة الواردة في وقت التشغيل كمتغير الجذر #. هذه السمة حصرية مع قائمة الانتظار. - المتسلسل: مرجع فول
RedisSerializer. بشكل افتراضي ، هوJdkSerializationRedisSerializer. ومع ذلك ، بالنسبة لحمولاتString، يتم استخدامStringRedisSerializerإذا لم يتم توفير مرجع مسلسل. - extract-payload: حدد ما إذا كان يجب أن ترسل نقطة النهاية هذه الحمولة إلى قائمة انتظار Redis أو الرسالة بأكملها. قيمته الافتراضية
true. - الدفع الأيسر: حدد ما إذا كان يجب أن تستخدم نقطة النهاية هذه الدفع الأيسر (عندما تكون
true) أو الدفع الأيمن (عندfalse) لكتابة الرسائل إلى قائمة Redis. إذا كان هذا صحيحًا ، فستعمل قائمة Redis كقائمة انتظار FIFO عند استخدامها مع محول قناة واردة لقائمة انتظار Redis الافتراضية. اضبط على "false" للاستخدام مع البرامج التي تقرأ من القائمة بالظهور المنبثق الأيسر أو لتحقيق ترتيب رسائل يشبه المكدس. قيمته الافتراضيةtrue.
الخطوة التالية هي تحديد البوابة المذكورة في تكوين .xml . بالنسبة للبوابة ، نستخدم فئة RedisChannelGateway من الحزمة org.toptal.queue .

يتم استخدام StringRedisSerializer لتسلسل الرسالة قبل الحفظ في Redis. أيضًا في تكوين .xml ، قمنا بتعريف البوابة وقمنا بتعيين RedisChannelGateway كخدمة بوابة. هذا يعني أنه يمكن حقن فول RedisChannelGateway في حبوب أخرى. لقد حددنا default-request-channel للخاصية لأنه من الممكن أيضًا توفير مراجع قناة لكل طريقة باستخدام التعليق التوضيحي @Gateway . تعريف الفئة:
public interface RedisChannelGateway { void enqueue(PostPublishedEvent event); } لإدخال هذا التكوين في تطبيقنا ، يتعين علينا استيراده. يتم تنفيذ ذلك في فئة SpringIntegrationConfig .
@ImportResource("classpath:WEB-INF/event-queue-config.xml") @AutoConfigureAfter(RedisConfig.class) @Configuration public class SpringIntegrationConfig { } يتم استخدام التعليق التوضيحي @ImportResource لاستيراد ملفات تكوين Spring .xml إلى @Configuration . و @AutoConfigureAfter يتم استخدام التعليق التوضيحي للتلميح إلى وجوب تطبيق التكوين التلقائي بعد فئات التكوين التلقائي المحددة الأخرى.
سننشئ الآن خدمة وننفذ الطريقة التي enqueue الأحداث في قائمة انتظار Redis.
public interface QueueService { void enqueue(PostPublishedEvent event); } @Service public class RedisQueueService implements QueueService { private RedisChannelGateway channelGateway; @Autowired public RedisQueueService(RedisChannelGateway channelGateway) { this.channelGateway = channelGateway; } @Override public void enqueue(PostPublishedEvent event) { channelGateway.enqueue(event); } } والآن ، يمكنك بسهولة إرسال رسالة إلى قائمة الانتظار باستخدام طريقة enqueue من QueueService .
قوائم انتظار Redis هي ببساطة قوائم بها منتج ومستهلك واحد أو أكثر. لنشر رسالة إلى قائمة انتظار ، يستخدم المنتجون الأمر LPUSH . وإذا كنت تراقب Redis (تلميح: اكتب redis-cli monitor ) ، يمكنك أن ترى أنه تمت إضافة الرسالة إلى قائمة الانتظار:
"LPUSH" "my-event-queue" "{\"postUrl\":\"test\",\"postTitle\":\"test\",\"emails\":[\"test\"]}"الآن ، نحتاج إلى إنشاء تطبيق للمستهلك يسحب هذه الأحداث من قائمة الانتظار ويعالجها. تحتاج خدمة المستهلك إلى نفس التبعيات مثل خدمة المنتج.
الآن يمكننا إعادة استخدام فئة PostPublishedEvent تسلسل الرسائل.
نحتاج إلى إنشاء تهيئة قائمة الانتظار ، ومرة أخرى ، يجب وضعها داخل دليل resources/WEB-INF . محتوى تكوين قائمة الانتظار هو:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:int="http://www.springframework.org/schema/integration" xmlns:int-redis="http://www.springframework.org/schema/integration/redis" xsi:schemaLocation="http://www.springframework.org/schema/integration/redis http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <int-redis:queue-inbound-channel-adapter channel="eventChannelJson" queue="my-event-queue" serializer="serializer" auto-startup="true" connection-factory="redisConnectionFactory"/> <int:channel/> <int:channel> <int:queue/> </int:channel> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> <int:json-to-object-transformer input-channel="eventChannelJson" output-channel="eventChannel" type="com.toptal.integration.spring.model.PostPublishedEvent"/> <int:service-activator input-channel="eventChannel" ref="RedisEventProcessingService" method="process"> <int:poller fixed-delay="10" time-unit="SECONDS" max-messages-per-poll="500"/> </int:service-activator> </beans> في تكوين .xml ، يمكن أن int-redis:queue-inbound-channel-adapter على الخصائص التالية:
- المعرف: اسم الفول للمكون.
- القناة: قناة
MessageChannelالتي نرسل إليها الرسائل من نقطة النهاية هذه. - بدء التشغيل التلقائي: سمة
SmartLifecycleلتحديد ما إذا كان يجب أن تبدأ نقطة النهاية هذه تلقائيًا بعد بدء سياق التطبيق أم لا. قيمته الافتراضيةtrue. - المرحلة: سمة
SmartLifecycleلتحديد المرحلة التي ستبدأ فيها نقطة النهاية هذه. قيمته الافتراضية هي0. - مصنع التوصيل: إشارة إلى فول
RedisConnectionFactory. - queue: اسم قائمة Redis التي يتم تنفيذ عملية البوب المستندة إلى قائمة الانتظار للحصول على رسائل Redis.
- قناة الخطأ: قناة
MessageChannelالتيErrorMessagesإليها رسائل خطأ معExceptionsمن مهمة الاستماعEndpoint. بشكل افتراضي ، يستخدمMessagePublishingErrorHandlerالأساسيerrorChannelالافتراضي من سياق التطبيق. - المتسلسل: مرجع فول
RedisSerializer. يمكن أن تكون سلسلة فارغة ، مما يعني عدم وجود مسلسل. في هذه الحالة ، يتم إرسالbyte[]من رسالة Redis الواردة إلى القناة على أنها حمولةMessage. بشكل افتراضي ، هوJdkSerializationRedisSerializer. - مهلة الاستلام: المهلة بالمللي ثانية لعملية البوب لانتظار رسالة Redis من قائمة الانتظار. قيمته الافتراضية هي ثانية واحدة.
- فترة الاسترداد: الوقت بالمللي ثانية الذي يجب أن تنام فيه مهمة المستمع بعد استثناءات في عملية البوب قبل إعادة تشغيل مهمة المستمع.
- توقع الرسالة: حدد ما إذا كانت نقطة النهاية هذه تتوقع احتواء البيانات من قائمة انتظار Redis على رسائل كاملة. إذا تم تعيين هذه السمة على "
true" ، فلا يمكن أن يكون المسلسل سلسلة فارغة لأن الرسائل تتطلب شكلاً من أشكال إلغاء التسلسل (تسلسل JDK افتراضيًا). قيمته الافتراضية هيfalse. - المنفذ المهمة: إشارة إلى برنامج Spring
TaskExecutor(أو JDK 1.5+ Executor القياسي). يتم استخدامه لمهمة الاستماع الأساسية. بشكل افتراضي ، يتم استخدامSimpleAsyncTaskExecutor. - البوب الأيمن: حدد ما إذا كانت نقطة النهاية هذه يجب أن تستخدم البوب الأيمن (عندما تكون
true) أو المنبثقة اليسرى (عندما تكونfalse) لقراءة الرسائل من قائمة Redis. إذا كان هذاtrue، فستعمل قائمة Redis كقائمة انتظار FIFO عند استخدامها مع محول قناة صادرة لقائمة انتظار Redis الافتراضية. اضبط علىfalseللاستخدام مع البرامج التي تكتب إلى القائمة بالدفع الأيمن أو لتحقيق ترتيب رسائل يشبه المكدس. قيمته الافتراضيةtrue.
الجزء المهم هو "منشط الخدمة" ، الذي يحدد الخدمة والطريقة التي يجب استخدامها لمعالجة الحدث.
أيضًا ، json-to-object-transformer إلى سمة نوع لتحويل JSON إلى كائنات ، تم تعيينها أعلاه على type="com.toptal.integration.spring.model.PostPublishedEvent" .
مرة أخرى ، لتوصيل هذا التكوين ، سنحتاج إلى فئة SpringIntegrationConfig ، والتي يمكن أن تكون كما كانت من قبل. وأخيرًا ، نحتاج إلى خدمة ستعالج الحدث بالفعل.
public interface EventProcessingService { void process(PostPublishedEvent event); } @Service("RedisEventProcessingService") public class RedisEventProcessingService implements EventProcessingService { @Override public void process(PostPublishedEvent event) { // TODO: Send emails here, retry strategy, etc :) } }بمجرد تشغيل التطبيق ، يمكنك أن ترى في Redis:
"BRPOP" "my-event-queue" "1"خاتمة
مع Spring Integration و Redis ، لن يكون إنشاء تطبيق Spring للخدمات الصغيرة أمرًا شاقًا كما هو معتاد. مع القليل من التكوين وكمية صغيرة من التعليمات البرمجية المعيارية ، يمكنك بناء أسس بنية الخدمات المصغرة الخاصة بك في أي وقت من الأوقات.
حتى إذا كنت لا تخطط لخدش مشروع Spring الحالي بالكامل والتحول إلى بنية جديدة ، بمساعدة Redis ، فمن السهل جدًا الحصول على تحسينات هائلة في الأداء من خلال قوائم الانتظار.
