نمط CQRS

Azure Storage

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

السياق والمشكلة

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

غالباً ما تكون أحمال العمل الخاصة بالقراءة والكتابة غير متكافئة، مع اختلاف كبير في متطلبات الأداء والحجم.

بنية CRUD تقليدية

  • غالباً ما يكون هناك عدم تطابق بين تمثيلات القراءة والكتابة للبيانات، مثل الأعمدة أو الخصائص الإضافية التي يجب تحديثها بشكل صحيح رغم أنها غير مطلوبة كجزء من العملية.

  • يمكن أن تحدث المنافسة على الاتصال على البيانات عندما يتم إجراء العمليات بالتوازي على نفس مجموعة البيانات.

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

  • يمكن أن تصبح إدارة الأمان والأذونات معقدة، لأن كل كيان يخضع لعمليات القراءة والكتابة، ما قد يعرض البيانات في سياق خاطئ.

حل

يفصل CQRS عمليات القراءة والكتابة في نماذج مختلفة، باستخدام الأوامر لتحديث البيانات، والاستعلامات لقراءة البيانات.

  • يجب أن تكون الأوامر مستندة إلى المهام وليس على البيانات. ("Book hotel room" وليس "set ReservationStatus to Reserved"). قد يتطلب هذا بعض التغييرات المقابلة لنمط تفاعل المستخدم. الجزء الآخر من ذلك هو النظر في تعديل منطق العمل لمعالجة هذه الأوامر لتكون ناجحة بشكل أكثر تكرارا. إحدى التقنيات التي تدعم ذلك هي تشغيل بعض قواعد التحقق من الصحة على العميل حتى قبل إرسال الأمر، وربما تعطيل الأزرار، ما يفسر سبب وجودها على واجهة المستخدم ("لا توجد غرف متبقية"). وبهذه الطريقة، يمكن تضييق سبب فشل الأوامر من جانب الخادم إلى ظروف السباق (مستخدمان يحاولان حجز الغرفة الأخيرة)، وحتى تلك التي يمكن معالجتها أحيانا ببعض البيانات والمنطق (وضع ضيف على قائمة انتظار).
  • قد يتم وضع الأوامر في قائمة انتظار للمعالجة غير المتزامنة، بدلا من معالجتها بشكل متزامن.
  • الاستعلامات لا تقوم بتعديل قاعدة البيانات. يقوم الاستعلام بإرجاع DTO الذي لا يغلف أي معرفة بالمجال.

يمكن بعد ذلك عزل النماذج، كما هو موضح في الرسم البياني التالي، رغم أن هذا ليس مطلباً مطلقاً.

بنية CQRS أساسية

يؤدي وجود نماذج استعلام وتحديث منفصلة إلى تبسيط التصميم والتنفيذ. ومع ذلك، فإن أحد العيوب هو أنه لا يمكن إنشاء التعليمات البرمجية CQRS تلقائيا من مخطط قاعدة بيانات باستخدام آليات التدعيم مثل أدوات O/RM (ومع ذلك، ستتمكن من إنشاء التخصيص الخاص بك أعلى التعليمات البرمجية التي تم إنشاؤها).

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

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

بنية CQRS مع مخازن منفصلة للقراءة والكتابة

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

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

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

تشمل فوائد CQRS ما يلي:

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

مشكلات واعتبارات التنفيذ

تتضمن بعض تحديات تنفيذ هذا النمط ما يلي:

  • التعقيد. الفكرة الأساسية لـ CQRS بسيطة. ولكن يمكن أن يؤدي إلى تصميم تطبيق أكثر تعقيداً، خاصةً إذا كان يشتمل على نمط تحديد مصادر الأحداث.

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

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

متى تستخدم نمط CQRS

ضع في اعتبارك CQRS للسيناريوهات التالية:

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

  • واجهات المستخدم المستندة إلى المهام حيث يتم توجيه المستخدمين من خلال عملية معقدة كسلسلة من الخطوات أو باستخدام نماذج المجال المعقدة. يحتوي نموذج الكتابة على مكدس معالجة أوامر كامل مع منطق الأعمال والتحقق من صحة الإدخال والتحقق من صحة الأعمال. قد يتعامل نموذج الكتابة مع مجموعة من العناصر المرتبطة كوحدة واحدة لتغييرات البيانات (تجميع، في مصطلحات DDD) والتأكد من أن هذه العناصر دائماً في حال متسقة. لا يحتوي نموذج القراءة على منطق عمل أو مكدس تحقق من الصحة، ويقوم فقط بإرجاع DTO لاستخدامه في نموذج العرض. يتوافق نموذج القراءة في النهاية مع نموذج الكتابة.

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

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

  • السيناريوهات التي يُتوقع أن يتطور فيها النظام بمرور الوقت وقد تحتوي على إصدارات متعددة من النموذج، أو حيث تتغير قواعد العمل بانتظام.

  • التكامل مع الأنظمة الأخرى، خاصةً بالاشتراك مع تحديد مصادر الأحداث، حيث لا ينبغي أن يؤثر الفشل الزمني لنظام فرعي على توفر الأنظمة الأخرى.

لا يوصى بهذا النمط عندما:

  • المجال أو قواعد العمل بسيطة.

  • تكفي واجهة مستخدم بسيطة على غرار CRUD وعمليات الوصول إلى البيانات.

ضع في اعتبارك تطبيق CQRS على أقسام محدودة من نظامك حيث ستكون أكثر قيمة.

تصميم حمل العمل

يجب على المهندس المعماري تقييم كيفية استخدام نمط CQRS في تصميم حمل العمل الخاص بهم لمعالجة الأهداف والمبادئ التي تغطيها ركائز Azure Well-Architected Framework. على سبيل المثال:

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

- PE:05 التحجيم والتقسيم
- أداء بيانات PE:08

كما هو الحال مع أي قرار تصميم، ضع في اعتبارك أي مفاضلات ضد أهداف الركائز الأخرى التي يمكن إدخالها مع هذا النمط.

مصادر الأحداث ونمط CQRS

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

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

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

عند استخدام CQRS جنباً إلى جنب مع نمط مصادر الأحداث، ضع في اعتبارك ما يلي:

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

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

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

مثال على نمط CQRS

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

توضح التعليمة البرمجية التالية تعريف نموذج القراءة.

// Query interface
namespace ReadModel
{
  public interface ProductsDao
  {
    ProductDisplay FindById(int productId);
    ICollection<ProductDisplay> FindByName(string name);
    ICollection<ProductInventory> FindOutOfStockProducts();
    ICollection<ProductDisplay> FindRelatedProducts(int productId);
  }

  public class ProductDisplay
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal UnitPrice { get; set; }
    public bool IsOutOfStock { get; set; }
    public double UserRating { get; set; }
  }

  public class ProductInventory
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public int CurrentStock { get; set; }
  }
}

يسمح النظام للمستخدمين بتقييم المنتجات. تقوم التعليمة البرمجية للتطبيق بهذا باستخدام الأمر RateProduct الموضح في التعليمة البرمجية التالية.

public interface ICommand
{
  Guid Id { get; }
}

public class RateProduct : ICommand
{
  public RateProduct()
  {
    this.Id = Guid.NewGuid();
  }
  public Guid Id { get; set; }
  public int ProductId { get; set; }
  public int Rating { get; set; }
  public int UserId {get; set; }
}

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

public class ProductsCommandHandler :
    ICommandHandler<AddNewProduct>,
    ICommandHandler<RateProduct>,
    ICommandHandler<AddToInventory>,
    ICommandHandler<ConfirmItemShipped>,
    ICommandHandler<UpdateStockFromInventoryRecount>
{
  private readonly IRepository<Product> repository;

  public ProductsCommandHandler (IRepository<Product> repository)
  {
    this.repository = repository;
  }

  void Handle (AddNewProduct command)
  {
    ...
  }

  void Handle (RateProduct command)
  {
    var product = repository.Find(command.ProductId);
    if (product != null)
    {
      product.RateProduct(command.UserId, command.Rating);
      repository.Save(product);
    }
  }

  void Handle (AddToInventory command)
  {
    ...
  }

  void Handle (ConfirmItemsShipped command)
  {
    ...
  }

  void Handle (UpdateStockFromInventoryRecount command)
  {
    ...
  }
}

الخطوات التالية

الأنماط والإرشادات التالية مفيدة عند تنفيذ هذا النمط:

منشورات مدونة مارتن فاولر:

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

  • نمط العرض الفعلي. يمكن أن يحتوي النموذج المقروء لتطبيق CQRS على آراء مجسدة لبيانات نموذج الكتابة، أو يمكن استخدام نموذج القراءة لتوليد وجهات نظر محسوسة.

  • عرض تقديمي حول CQRS أفضل من خلال أنماط تفاعل المستخدم غير المتزامنة