Software Engineering

هندسة النظم للتوسع: تطبيق Event Sourcing و CQRS في الأنظمة الموزعة

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

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

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

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

لماذا تختار هذه المعمارية للإنتاجية العالية؟

بالنسبة للأنظمة الموزعة عالية الإنتاجية، تكمن الفوائد الرئيسية في الأداء والمرونة. من خلال فصل مسارات القراءة والكتابة، تمنع أعباء العمل الثقيلة في القراءة من حجب عمليات الكتابة. علاوة على ذلك، لأن الأحداث قابلة للإضافة فقط وثابتة، يمكن كتابتها في أنظمة سجلات عالية الأداء (مثل Apache Kafka أو AWS Kinesis) بدلاً من قواعد البيانات العلائقية التقليدية، مما يزيد بشكل كبير من إنتاجية الكتابة.

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

مثال على التنفيذ: خدمة طلبات بسيطة

لننظر في تنفيذ مفاهيمي في مجال مبسط. سنحدد حدثاً وأمرًا لتوضيح التدفق.

// Define the immutable event
class OrderCreatedEvent {
  constructor(orderId, customerId, items) {
    this.orderId = orderId;
    this.customerId = customerId;
    this.items = items;
    this.timestamp = new Date();
  }
}

// Define the command
class PlaceOrderCommand {
  constructor(userId, orderDetails) {
    this.userId = userId;
    this.orderDetails = orderDetails;
  }
}

// The aggregate root handles the command and produces events
class OrderAggregate {
  constructor() {
    this.events = [];
  }

  // Apply the command
  async placeOrder(command) {
    // 1. Validate business rules
    if (command.orderDetails.items.length === 0) {
      throw new Error("Order cannot be empty");
    }

    // 2. Create the event
    const event = new OrderCreatedEvent(
      generateId(),
      command.userId,
      command.orderDetails.items
    );

    // 3. Apply the event to the current state
    this.applyEvent(event);

    // 4. Save the event to the event store (Write Side)
    await eventStore.save([event]);
  }

  // Apply event to internal state
  applyEvent(event) {
    if (event instanceof OrderCreatedEvent) {
      this.id = event.orderId;
      this.customerId = event.customerId;
      this.items = event.items;
    }
  }
}

إعادة بناء الحالة للقراءة

في جانب القراءة، يستمع محرك إسقاط منفصل (projection engine) إلى تدفق الأحداث. يستهلك هذه الأحداث ويحدث نماذج القراءة غير الطبيعية (مثل Elasticsearch أو العرض المادي في SQL) المحسنة للاستعلام السريع. يضمن ذلك أن تكون استجابات واجهة برمجة التطبيقات (API) فورية، حتى مع زيادة عبء الكتابة.

// Projection Handler (Read Side)
class OrderProjection {
  constructor(readDb) {
    this.readDb = readDb;
  }

  handleEvent(event) {
    if (event.type === 'ORDER_CREATED') {
      // Insert into a denormalized table for fast reading
      this.readDb.insert({
        orderId: event.payload.orderId,
        customerId: event.payload.customerId,
        itemsCount: event.payload.items.length,
        createdAt: event.payload.timestamp
      });
    }
  }
}

الخاتمة

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

Share: