بناء الخدمات المصغّرة بـ Spring Boot

إدارة البيانات لكل خدمة

18 دقيقة الدرس 6 من 12

إدارة البيانات لكل خدمة

من أكثر القرارات أثرًا في بنية الخدمات المصغّرة، وفي الوقت نفسه من أكثرها سوء فهم: يجب أن تمتلك كل خدمة بياناتها حصريًا. يعني هذا المبدأ — الذي يُعرف أحيانًا بـقاعدة بيانات لكل خدمة — أن جداول الخدمة وهيكل بياناتها ومخزنها تبقى خاصة بها تمامًا. لا تقرأ خدمة أخرى تلك البيانات مباشرةً؛ بل يمر كل وصول إليها عبر واجهة برمجية (API) خاصة بالخدمة المالكة. يشرح هذا الدرس سبب وجود هذه القاعدة، وكيفية تطبيقها مع Spring Boot 3، والمقايضات التي تقبلها حين تلتزم بها.

لماذا تُعدّ قاعدة البيانات المشتركة نمطًا مضادًا؟

قد تبدو مشاركة قاعدة بيانات واحدة عبر خدمات متعددة مغرية: تتجنب التكرار، تكون عمليات الربط (JOIN) بسيطة، وأنت تعرف الهيكل أصلًا. لكنها في الواقع تُنشئ اقترانًا محكمًا عند أسوأ الطبقات الممكنة:

  • اقتران الهيكل: إعادة تسمية عمود في جدول orders تُكسر كل خدمة تستعلم منه، حتى تلك التي تديرها فرق مختلفة.
  • اقتران النشر: لا يمكنك إطلاق نسخة جديدة من خدمة inventory إذا كانت خدمة order لا تزال تتوقع الهيكل القديم.
  • اقتران التوسع: لا يمكنك تشغيل خدمة catalog الكثيفة القراءة على نسخة قراءة منفصلة بينما تستخدم خدمة checkout الكثيفة الكتابة الخادم الأساسي؛ إذ يتشاركان نفس تجمّع الاتصالات.
  • اقتران التقنية: فريق يريد PostgreSQL مع JSONB، وفريق آخر يريد مخزن وثائق. قاعدة البيانات المشتركة تُجبر الجميع على محرك واحد.
القاعدة: إذا كانت خدمتان تشتركان في قاعدة بيانات ولا يمكنك نشر إحداهما دون التحقق من الأخرى، فأنت لم تبنِ خدمات مصغّرة حقيقية — بل بنيت مونوليث موزعًا يحمل كل تعقيدات التوزيع دون أي استقلالية.

تعدد تقنيات التخزين (Polyglot Persistence)

يُتيح مبدأ قاعدة بيانات لكل خدمة لكل فريق اختيار تقنية التخزين الأنسب لمشكلته:

  • خدمة الطلبات — علائقية (PostgreSQL) لضمانات ACID للمعاملات.
  • خدمة الكتالوج — مخزن وثائق (MongoDB) لخصائص منتج مرنة.
  • خدمة السلة — مخزن مفتاح-قيمة (Redis) لبيانات جلسة مؤقتة وذات زمن استجابة منخفض.
  • خدمة البحث — محرك بحث (Elasticsearch) للاستعلامات النصية الكاملة.

يدعم الضبط التلقائي لـ Spring Boot جميع هذه التقنيات. أعلن عن التبعية وأعدّ رابط الاتصال؛ يتولى Spring تهيئة الباقي.

إعداد مخازن بيانات معزولة في Spring Boot 3

تمتلك كل خدمة ملف application.yml خاصًا بها مع مصدر بيانات خاص. قد تبدو خدمة order-service هكذا:

# order-service/src/main/resources/application.yml spring: datasource: url: jdbc:postgresql://orders-db:5432/orders username: ${DB_USER} password: ${DB_PASS} jpa: hibernate: ddl-auto: validate # لا إنشاء تلقائي في الإنتاج أبدًا open-in-view: false # تجنب مشاكل التحميل الكسول flyway: locations: classpath:db/migration enabled: true

بينما تستخدم خدمة catalog-service نسخة MongoDB منفصلة تمامًا:

# catalog-service/src/main/resources/application.yml spring: data: mongodb: uri: mongodb://catalog-db:27017/catalog auto-index-creation: false
لا تستخدم أبدًا ddl-auto: create أو create-drop في الإنتاج. استخدم دائمًا أداة ترحيل — Flyway أو Liquibase — حتى تكون تغييرات الهيكل ذات إصدارات وقابلة للمراجعة والتراجع. Flyway هو الخيار المعياري المُهيَّأ تلقائيًا في Spring Boot.

ترحيل الهيكل مع Flyway

تحمل كل خدمة سكريبتات الترحيل الخاصة بها في src/main/resources/db/migration. اصطلاح التسمية هو V{version}__{description}.sql:

-- order-service/src/main/resources/db/migration/V1__create_orders.sql CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'PENDING', total_cents INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_orders_customer ON orders (customer_id);

يعمل Flyway تلقائيًا عند بدء تشغيل التطبيق (قبل فتح منفذ HTTP)، ويُطبق أي عمليات ترحيل معلقة بترتيب الإصدار، ويسجل كل سكريبت مُطبَّق في جدول flyway_schema_history. إذا فشل ترحيل ما، رفض التطبيق الانطلاق — وهذا بالضبط ما تريده: هيكل نصف مُرحَّل أسوأ من لا هيكل.

التكلفة: لا ربط (JOIN) بين الخدمات

بمجرد أن تمتلك كل خدمة بياناتها، لا يمكن أن يكون استعلام مثل "احضر الطلب مع اسم العميل والسعر الحالي للمنتج" ربط SQL واحد. لديك ثلاثة خيارات:

  1. تجميع الواجهة البرمجية (API Composition): يجلب المُستدعي (أو خدمة تجميع مخصصة) من خدمات متعددة ويجمع النتيجة في الذاكرة. بسيط لكنه يضيف زمن استجابة ويُنشئ اعتمادية المُستدعي على واجهات برمجية متعددة.
  2. إزالة التطبيع المدفوع بالأحداث: تنشر الخدمات أحداثًا عند تغيّر البيانات؛ تُحافظ المستهلكون على نسخ محلية مُحسَّنة للقراءة. اتساق مؤجل، لكن أداء القراءة ممتاز.
  3. CQRS + نماذج القراءة: خدمة قراءة مخصصة تشترك في أحداث النطاق وتُجسّد عرضًا مُدمجًا في مخزنها الخاص (مثل فهرس Elasticsearch مُزال التطبيع).

نقطة البداية الأكثر شيوعًا هي تجميع الواجهة البرمجية للاستعلامات البسيطة إضافةً إلى التحديثات المدفوعة بالأحداث للبيانات المتغيرة كثيرًا. إليك تجميع واجهة برمجية نموذجي في order-service:

@Service @RequiredArgsConstructor public class OrderDetailsService { private final OrderRepository orderRepository; private final CustomerClient customerClient; // OpenFeign / WebClient private final CatalogClient catalogClient; public OrderDetailsDto getDetails(Long orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException(orderId)); CustomerDto customer = customerClient.findById(order.getCustomerId()); List<ProductDto> products = catalogClient.findByIds(order.getProductIds()); return OrderDetailsDto.of(order, customer, products); } }
لا تحقن أبدًا فول Spring أو مستودع JPA الخاص بخدمة أخرى في خدمتك. إذا استورد OrderService الفئة CustomerRepository من وحدة customer-service، فأنت أعدت مشكلة قاعدة البيانات المشتركة في الكود — والآن يجب نشر كلتا الخدمتين معًا. يمر الوصول إلى بيانات خدمة أخرى دائمًا عبر واجهة الشبكة البرمجية.

التعامل مع الاتساق الموزع

بدون قاعدة بيانات مشتركة، لا يمكن تغليف عملية متعددة الخدمات — كـ "أنشئ طلبًا واخصم المخزون واشحن العميل" — في معاملة ACID واحدة. يجب عليك اختيار استراتيجية للاتساق:

  • نمط Saga (التنسيق الكوريوغرافي): تنشر كل خدمة حدث نطاق بعد أن تلتزم معاملتها المحلية. تتفاعل الخدمات الأدنى مع ذلك الحدث وتنشر أحداثها. تتراجع معاملات تعويضية عند الفشل.
  • نمط Saga (التنسيق التوزيعي): يُصدر منظّم saga مخصص أوامر لكل خدمة ويتتبع الحالة. أسهل في الاستدلال عليه لكنه يُضيف مكونًا منسّقًا.
  • الاتساق المؤجل + الأمان الاصطلاحي (Idempotency): اقبل أن خدمات مختلفة ستكون خارج التزامن لفترة وجيزة. صمّم كل عملية لتكون اصطلاحية (آمنة لإعادة التشغيل) حتى لا تُكرّر إعادة المحاولة بعد أعطال الشبكة التأثيرات.

الرؤية الأساسية هي أن قواعد البيانات التقليدية نفسها تتسم بالاتساق المؤجل عبر النسخ المتماثلة. الخدمات المصغّرة تجعل هذه المقايضة صريحةً فحسب.

الانعكاسات الأمنية

تُحسّن قواعد البيانات الخاصة وضعية أمان النظام ككل:

  • خدمة catalog-service المخترقة لا تستطيع قراءة جداول orders أو payments — فهي على خوادم منفصلة بأوراق اعتماد منفصلة.
  • تعمل كل خدمة بمستخدم قاعدة بيانات يمتلك فقط الأذونات التي يحتاجها (مبدأ الصلاحية الأدنى). مستخدم catalog-service لا يستطيع تنفيذ DROP TABLE في هيكل الطلبات حتى لو عرف رابط الاتصال بطريقة ما.
  • يمكن عزل البيانات الحساسة (المعلومات الشخصية، بيانات الدفع) في خدمة تقع قاعدة بياناتها في قطاع شبكة بأمان أعلى، مشفرة في حالة السكون بمفتاح خاص بها، وتُراجَع باستقلالية.
خزّن بيانات الاعتماد في مدير أسرار (HashiCorp Vault، AWS Secrets Manager، Kubernetes Secrets) وأدخلها في وقت التشغيل عبر متغيرات البيئة. لا تضمّن كلمات المرور أبدًا في application.yml ولا تُودعها في git. يدعم Spring Cloud Vault وSpring Cloud Config هذا النمط خارج الصندوق.

الخلاصة

قاعدة البيانات لكل خدمة هي ركيزة الاستقلالية الحقيقية للخدمة. تُعلن كل خدمة Spring Boot عن مصدر بياناتها في ملف application.yml الخاص بها، وتدير هيكلها بعمليات ترحيل Flyway التي تعيش جنبًا إلى جنب مع كود الخدمة، ولا تُعرض بياناتها إلا عبر واجهة REST أو رسائل. التكلفة هي أن الاستعلامات بين الخدمات تصبح تجميع واجهة برمجية أو نماذج قراءة مدفوعة بالأحداث، وأن المعاملات متعددة الخدمات تتطلب Sagas بدلًا من معاملات ACID. هذه المقايضات حقيقية، لكنها ثمن الاستقلالية في النشر، وحرية اختيار التقنية، وعزل نطاقات الفشل — الوعود الجوهرية لنمط الخدمات المصغّرة.