Software Engineering

معماری برای مقیاس‌پذیری: پیاده‌سازی Event Sourcing و CQRS در سیستم‌های توزیع‌شده

در منظر مدرن مهندسی نرم‌افزار، ساخت سیستم‌هایی که بتوانند میلیون‌ها کاربر همزمان را با حفظ یکپارچگی داده مدیریت کنند دیگر یک لوکس نیست، بلکه ضرورتی است. معماری‌های سنتی CRUD (ایجاد، خواندن، به‌روزرسانی، حذف) اغلب در برابر تراکنش‌های بالا دچار مشکل می‌شوند که منجر به رقابت در پایگاه داده، مدیریت پیچیده تراکنش‌ها و مشکلات وضعیت دشوار برای اشکال‌زدایی می‌گردد. برای غلبه بر این گلوگاه‌ها، تیم‌های پیشرفته‌تر به‌طور فزاینده‌ای به ترکیبی از تفکیک مسئولیت‌های دستوری و پرس‌وجو (CQRS) و منبع‌سازی رویداد (Event Sourcing) روی می‌آورند.

درک مفاهیم پایه

CQRS و Event Sourcing اغلب با هم ذکر می‌شوند، اما نگرش‌های متفاوتی را هدف قرار می‌دهند. 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
      });
    }
  }
}

نتیجه‌گیری

پیاده‌سازی Event Sourcing و CQRS یک راه‌حل جادویی نیست؛ این کار پیچیدگی‌هایی را در زمینه نسخه‌بندی رویدادها، اتفاقات یکپارچگی و هزینه‌های عملیاتی ایجاد می‌کند. با این حال، برای سیستم‌های توزیع‌شده با تراکنش بالا که در آن‌ها مقیاس‌پذیری، قابلیت حسابرسی و عملکرد در اولویت هستند، مزایای آن بسیار فراتر از هزینه‌هاست. با تسلط بر این الگوها، می‌توانید سیستم‌هایی بسازید که نه‌تنها تاب‌آور باشند، بلکه قادر به تکامل با تقاضای فزاینده کاربران شما نیز باشند.

Share: