System Design

رمزگشایی از CQRS: مقیاس‌پذیری سیستم با تفکیک مسئولیت دستور و پرس‌وجو

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

CQRS چیست؟

CQRS یک اصل در معماری نرم‌افزار است که مدل داده را به دو بخش تقسیم می‌کند: یک مدل دستور (Command) برای مدیریت نوشتن‌ها (تغییرات) و یک مدل پرس‌وجو (Query) برای مدیریت خواندن‌ها (بازیابی). ایده اصلی ساده است: عملیات‌هایی که وضعیت را تغییر می‌دهند (دستورها) نباید داده برگردانند، و عملیات‌هایی که داده بازیابی می‌کنند (پرس‌وجوها) نباید وضعیت را تغییر دهند.

این جداسازی به بخش خواندن و نوشتن امکان می‌دهد که طرح‌های پایگاه داده، فناوری‌های ذخیره‌سازی یا حتی زیرساخت‌های میزبانی متفاوتی داشته باشند که دقیقاً برای موارد استفاده خاص آن‌ها طراحی شده‌اند.

چرا از CQRS استفاده کنیم؟

  • بهینه‌سازی عملکرد: می‌توانید مدل خواندن را برای پرس‌وجوهای سریع غیرنرمال (Denormalize) کنید بدون اینکه بر یکپارچگی مدل نوشتن تأثیر بگذارد.
  • مقیاس‌پذیری: برنامه‌های با بار خواندن بالا می‌توانند بخش خواندن را به صورت مستقل از بخش نوشتن مقیاس‌بندی کنند.
  • امنیت: عملیات‌های نوشتن می‌توانند به طور سختگیرانه کنترل و ممیزی شوند، در حالی که عملیات‌های خواندن باز و سریع باقی می‌مانند.
  • مدیریت پیچیدگی: در دامنه‌های پیچیده، جداسازی نگرانی‌ها از الگوی ضدالگوی "مدل خدا" (God Model) جلوگیری می‌کند که در آن یک موجودیت غول‌پیکر همه چیز را مدیریت می‌کند.

نمای کلی معماری

در یک پیاده‌سازی معمولی CQRS، جریان به این صورت است:

  1. یک دستور (Command) به سمت دستور ارسال می‌شود.
  2. سمت دستور منطق کسب‌وکار را اعتبارسنجی و پردازش می‌کند و رویدادها را در یک فروشگاه نوشتن (Write Store) ذخیره می‌کند.
  3. رویدادها به یک اتوبوس رویداد (Event Bus) یا صف پیام (Message Queue) منتشر می‌شوند.
  4. پردازنده‌ها در سمت پرس‌وجو این رویدادها را مصرف کرده و فروشگاه خواندن (Read Store) را به‌روزرسانی می‌کنند (که اغلب یک پایگاه داده NoSQL یا شاخص جستجو است).
  5. یک پرس‌وجو (Query) به سمت پرس‌وجو ارسال می‌شود، که داده‌های از پیش محاسبه شده را از فروشگاه خواندن بازیابی می‌کند.

نمونه کد

در زیر یک نمونه ساده‌شده C# آورده شده است که جداسازی دستورها و پرس‌وجوها را نشان می‌دهد.

// 1. تعریف یک دستور (عملیات نوشتن)
public record CreateOrderCommand : ICommand<Guid>
{
    public Guid CustomerId { get; init; }
    public decimal Amount { get; init; }
}

// 2. تعریف یک پرس‌وجو (عملیات خواندن)
public record GetOrderByIdQuery : IQuery<OrderDto>
{
    public Guid OrderId { get; init; }
}

// 3. پردازنده دستور (نوشتن به منبع حقیقت)
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid>
{
    private readonly IOrderRepository _repo;
    private readonly IEventPublisher _publisher;

    public async Task<Guid> Handle(CreateOrderCommand command, CancellationToken ct)
    {
        // منطق کسب‌وکار و اعتبارسنجی
        var order = new Order(command.CustomerId, command.Amount);
        
        // ذخیره در فروشگاه نوشتن (پایگاه داده رابطه‌ای)
        await _repo.SaveAsync(order, ct);
        
        // انتشار رویداد دامنه
        await _publisher.PublishAsync(new OrderCreatedEvent(order.Id), ct);
        
        return order.Id;
    }
}

// 4. پردازنده پرس‌وجو (خواندن از فروشگاه بهینه‌شده)
public class GetOrderByIdQueryHandler : IQueryHandler<GetOrderByIdQuery, OrderDto>
{
    private readonly IOrderReadRepository _readRepo;

    public async Task<OrderDto> Handle(GetOrderByIdQuery query, CancellationToken ct)
    {
        // خواندن از NoSQL/ES/Cache
        return await _readRepo.GetByIdAsync(query.OrderId, ct);
    }
}

ملاحظات عملی

اگرچه CQRS قدرتمند است، اما پیچیدگی را وارد می‌کند. باید سازگاری نهایی (Eventual Consistency) را مدیریت کنید، زیرا ممکن است سمت خواندن عقب‌تر از سمت نوشتن باشد. علاوه بر این، حفظ همگام‌سازی بین مدل نوشتن و مدل خواندن نیاز به پردازش رویداد و پایشی مستحکم دارد. توصیه می‌شود با رویکرد "جداسازی خواندن-نوشتن" شروع کنید که در آن از همان پایگاه داده اما با نمایه‌های متفاوت استفاده می‌کنید، قبل از حرکت به سمت فروشگاه‌های داده کاملاً جداگانه.

نتیجه‌گیری

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

Share: