Software Engineering

إتقان Event Sourcing و CQRS لميكروسيرفيس قابلة للتوسع

في المشهد الحديث للأنظمة الموزعة، يُعد التوسع واتساق البيانات أمراً بالغ الأهمية. غالباً ما تواجه هياكل CRUD التقليدية (إنشاء، قراءة، تحديث، حذف) صعوبة في تلبية متطلبات التطبيقات عالية الإنتاجية، حيث قد تصل نسبة الأوامر إلى عمليات القراءة إلى 1000:1. هنا يأتي دور الفصل بين مسؤولية الاستعلامات والأوامر (CQRS) ومصدر الأحداث (Event Sourcing) لتقديم بديل قوي وعالي الأداء.

فهم المفاهيم الأساسية

CQRS هو نمط معماري يفصل عمليات القراءة والكتابة إلى نماذج مختلفة. بدلاً من وجود نموذج مجال واحد يتعامل مع كلاهما، لديك جانب الأوامر المسؤول عن معالجة الأوامر وكتابة البيانات، وجانب الاستعلامات المحسّن للقراءة وعرض البيانات. يسمح هذا الفصل للفرق بتوسع سعات القراءة والكتابة بشكل مستقل، مما يحسن كل جانب لاحتياجات العمل المحددة الخاصة به.

يأخذ مصدر الأحداث (Event Sourcing) هذا خطوة إلى الأمام من خلال تخزين حالة الكيان ليس كقيمها الحالية، ولكن كسلسلة من الأحداث. يتم التقاط كل تغيير في حالة التطبيق كحدث. لإعادة بناء الحالة الحالية، ما عليك سوى إعادة تشغيل هذه الأحداث. يوفر هذا النهج سجلاً تدقيقياً كاملاً ويمكّن من ميزات قوية مثل تصحيح الأخطاء عبر الزمن والاستعلامات الزمنية.

تنفيذ النمط: مثال عملي

لنلقِ نظرة على تنفيذ مبسط لخدمة الطلبات. في نظام تقليدي، قد يبدو تحديث الطلب كالتالي:

// Traditional UPDATE approach
await db.orders.update(
  { id: orderId }, 
  { $set: { status: 'SHIPPED' } }
);

في بنية مصدر الأحداث، لا نقوم بتحديث الطلب مباشرة. بدلاً من ذلك، نضيف حدثاً جديداً إلى مخزن الأحداث.

// Event Sourcing approach: Append an event
const event = {
  aggregateId: orderId,
  type: 'OrderShippedEvent',
  timestamp: new Date(),
  payload: {
    trackingNumber: 'TRK-12345',
    shippedBy: 'FedEx'
  }
};

await eventStore.append(event);

يتم اشتقاق الحالة الحالية للطلب من خلال إعادة تشغيل جميع الأحداث الخاصة بمعرف التجميع (aggregate ID) هذا. عندما يصل طلب قراءة، تقوم عملية منفصلة (غالباً ما تكون معالج أحداث) بعرض هذه البيانات في نموذج قراءة غير طبيعي (denormalized view model)، مثل مستند NoSQL أو جدول SQL متخصص، محسّن للاسترجاع السريع.

// Projecting events into a read model
class OrderReadModel {
  constructor(eventStore, repository) {
    this.eventStore = eventStore;
    this.repository = repository;
  }

  async handle(event) {
    if (event.type === 'OrderShippedEvent') {
      await this.repository.update({
        _id: event.aggregateId,
        $set: {
          status: 'SHIPPED',
          trackingNumber: event.payload.trackingNumber
        }
      });
    }
  }
}

المزايا والتحديات

يوفر تنفيذ CQRS ومصدر الأحداث مزايا كبيرة:

  • القابلية للتوسع: يمكنك توسيع نطاق نسخ القراءة الخاصة بك بشكل مستقل عن شرائح الكتابة.
  • قابلية التدقيق: يتم تسجيل كل إجراء كحدث غير قابل للتغيير، مما يسهل الامتثال وتصحيح الأخطاء.
  • المرونة: يمكنك إنشاء نماذج قراءة جديدة عند الطلب دون التأثير على جانب الكتابة.

ومع ذلك، تأتي هذه التعقيدات بتكلفة. يجب عليك التعامل مع الاتساق النهائي (eventual consistency)، وإدارة إصدارات الأحداث لتجنب التغييرات المعطلة، والتعامل مع العبء التشغيلي لصيانة مخازن الأحداث. إنه ليس حلاً سحرياً؛ استخدمه عندما تبرر عدم التماثل بين القراءة/الكتابة أو متطلبات التدقيق التعقيد المعماري.

الخاتمة

يُعد مصدر الأحداث و CQRS أدوات قوية لبناء ميكروسيرفيس مرنة وقابلة للتوسع. من خلال فصل الأوامر عن الاستعلامات والاستفادة من عدم قابلية تغيير الأحداث، يمكن للمطورين إنشاء أنظمة تعمل بأداء استثنائي تحت الأحمال الثقيلة. وعلى الرغم من أن منحنى التعلم حاد، فإن الفوائد طويلة المدى من حيث القابلية للصيانة، والقابلية للتوسع، وسلامة البيانات تجعلها استثماراً يستحق العناء للتطبيقات المؤسسية المعقدة.

Share: