إدارة البيانات لكل خدمة
إدارة البيانات لكل خدمة
من أكثر القرارات أثرًا في بنية الخدمات المصغّرة، وفي الوقت نفسه من أكثرها سوء فهم: يجب أن تمتلك كل خدمة بياناتها حصريًا. يعني هذا المبدأ — الذي يُعرف أحيانًا بـقاعدة بيانات لكل خدمة — أن جداول الخدمة وهيكل بياناتها ومخزنها تبقى خاصة بها تمامًا. لا تقرأ خدمة أخرى تلك البيانات مباشرةً؛ بل يمر كل وصول إليها عبر واجهة برمجية (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 هكذا:
بينما تستخدم خدمة catalog-service نسخة MongoDB منفصلة تمامًا:
ddl-auto: create أو create-drop في الإنتاج. استخدم دائمًا أداة ترحيل — Flyway أو Liquibase — حتى تكون تغييرات الهيكل ذات إصدارات وقابلة للمراجعة والتراجع. Flyway هو الخيار المعياري المُهيَّأ تلقائيًا في Spring Boot.
ترحيل الهيكل مع Flyway
تحمل كل خدمة سكريبتات الترحيل الخاصة بها في src/main/resources/db/migration. اصطلاح التسمية هو V{version}__{description}.sql:
يعمل Flyway تلقائيًا عند بدء تشغيل التطبيق (قبل فتح منفذ HTTP)، ويُطبق أي عمليات ترحيل معلقة بترتيب الإصدار، ويسجل كل سكريبت مُطبَّق في جدول flyway_schema_history. إذا فشل ترحيل ما، رفض التطبيق الانطلاق — وهذا بالضبط ما تريده: هيكل نصف مُرحَّل أسوأ من لا هيكل.
التكلفة: لا ربط (JOIN) بين الخدمات
بمجرد أن تمتلك كل خدمة بياناتها، لا يمكن أن يكون استعلام مثل "احضر الطلب مع اسم العميل والسعر الحالي للمنتج" ربط SQL واحد. لديك ثلاثة خيارات:
- تجميع الواجهة البرمجية (API Composition): يجلب المُستدعي (أو خدمة تجميع مخصصة) من خدمات متعددة ويجمع النتيجة في الذاكرة. بسيط لكنه يضيف زمن استجابة ويُنشئ اعتمادية المُستدعي على واجهات برمجية متعددة.
- إزالة التطبيع المدفوع بالأحداث: تنشر الخدمات أحداثًا عند تغيّر البيانات؛ تُحافظ المستهلكون على نسخ محلية مُحسَّنة للقراءة. اتساق مؤجل، لكن أداء القراءة ممتاز.
- CQRS + نماذج القراءة: خدمة قراءة مخصصة تشترك في أحداث النطاق وتُجسّد عرضًا مُدمجًا في مخزنها الخاص (مثل فهرس Elasticsearch مُزال التطبيع).
نقطة البداية الأكثر شيوعًا هي تجميع الواجهة البرمجية للاستعلامات البسيطة إضافةً إلى التحديثات المدفوعة بالأحداث للبيانات المتغيرة كثيرًا. إليك تجميع واجهة برمجية نموذجي في order-service:
OrderService الفئة CustomerRepository من وحدة customer-service، فأنت أعدت مشكلة قاعدة البيانات المشتركة في الكود — والآن يجب نشر كلتا الخدمتين معًا. يمر الوصول إلى بيانات خدمة أخرى دائمًا عبر واجهة الشبكة البرمجية.
التعامل مع الاتساق الموزع
بدون قاعدة بيانات مشتركة، لا يمكن تغليف عملية متعددة الخدمات — كـ "أنشئ طلبًا واخصم المخزون واشحن العميل" — في معاملة ACID واحدة. يجب عليك اختيار استراتيجية للاتساق:
- نمط Saga (التنسيق الكوريوغرافي): تنشر كل خدمة حدث نطاق بعد أن تلتزم معاملتها المحلية. تتفاعل الخدمات الأدنى مع ذلك الحدث وتنشر أحداثها. تتراجع معاملات تعويضية عند الفشل.
- نمط Saga (التنسيق التوزيعي): يُصدر منظّم saga مخصص أوامر لكل خدمة ويتتبع الحالة. أسهل في الاستدلال عليه لكنه يُضيف مكونًا منسّقًا.
- الاتساق المؤجل + الأمان الاصطلاحي (Idempotency): اقبل أن خدمات مختلفة ستكون خارج التزامن لفترة وجيزة. صمّم كل عملية لتكون اصطلاحية (آمنة لإعادة التشغيل) حتى لا تُكرّر إعادة المحاولة بعد أعطال الشبكة التأثيرات.
الرؤية الأساسية هي أن قواعد البيانات التقليدية نفسها تتسم بالاتساق المؤجل عبر النسخ المتماثلة. الخدمات المصغّرة تجعل هذه المقايضة صريحةً فحسب.
الانعكاسات الأمنية
تُحسّن قواعد البيانات الخاصة وضعية أمان النظام ككل:
- خدمة
catalog-serviceالمخترقة لا تستطيع قراءة جداولordersأوpayments— فهي على خوادم منفصلة بأوراق اعتماد منفصلة. - تعمل كل خدمة بمستخدم قاعدة بيانات يمتلك فقط الأذونات التي يحتاجها (مبدأ الصلاحية الأدنى). مستخدم
catalog-serviceلا يستطيع تنفيذDROP TABLEفي هيكل الطلبات حتى لو عرف رابط الاتصال بطريقة ما. - يمكن عزل البيانات الحساسة (المعلومات الشخصية، بيانات الدفع) في خدمة تقع قاعدة بياناتها في قطاع شبكة بأمان أعلى، مشفرة في حالة السكون بمفتاح خاص بها، وتُراجَع باستقلالية.
application.yml ولا تُودعها في git. يدعم Spring Cloud Vault وSpring Cloud Config هذا النمط خارج الصندوق.
الخلاصة
قاعدة البيانات لكل خدمة هي ركيزة الاستقلالية الحقيقية للخدمة. تُعلن كل خدمة Spring Boot عن مصدر بياناتها في ملف application.yml الخاص بها، وتدير هيكلها بعمليات ترحيل Flyway التي تعيش جنبًا إلى جنب مع كود الخدمة، ولا تُعرض بياناتها إلا عبر واجهة REST أو رسائل. التكلفة هي أن الاستعلامات بين الخدمات تصبح تجميع واجهة برمجية أو نماذج قراءة مدفوعة بالأحداث، وأن المعاملات متعددة الخدمات تتطلب Sagas بدلًا من معاملات ACID. هذه المقايضات حقيقية، لكنها ثمن الاستقلالية في النشر، وحرية اختيار التقنية، وعزل نطاقات الفشل — الوعود الجوهرية لنمط الخدمات المصغّرة.