CQRS با مخزن خواندن جدا: projectionهای مفید و تضمین‌های تازه

منتشر شده در · 5 دقیقه مطالعه

مهندسی معماری نرم‌افزار #architecture #cqrs #projections #eventual-consistency #dotnet #csharp

در نوشته‌ی قبلی، command و query را در یک برنامه و یک دیتابیس نگه داشتیم. برای بسیاری از سیستم‌ها همین کافی است.1

مخزن خواندن جدا یک گام اضافه است، نه قدم بعدیِ اجباری. وقتی یک صفحه، گزارش یا بار خواندن نیازهایی دارد که نباید وارد مدل نوشتن شوند، این جداسازی به کار می‌آید. اما یک تضمین ضمنی را از بین می‌برد: بعد از موفق شدن یک command، ممکن است query بعدی هنوز تغییر را نبیند.

مدل خواندن برای سؤال خودش ساخته می‌شود

فرض کنید سرویس سفارش، مشتری‌ها، سفارش‌ها، ردیف‌های سفارش، ارسال‌ها و پرداخت‌ها را در دیتابیس نوشتنِ نرمال‌شده نگه می‌دارد. صفحه‌ی پشتیبانی به فهرستی مثل این نیاز دارد:

public sealed record OrderTrackingRow(
    string OrderNumber,
    string CustomerName,
    string Status,
    DateTimeOffset LastUpdated,
    decimal Total);

مسیر command به داده‌ی نرمال‌شده و قواعد تراکنش نیاز دارد. صفحه‌ی رهگیری فقط یک ردیف برای هر سفارش می‌خواهد؛ ردیفی که با همان مقادیری که نشان می‌دهد مرتب و فیلتر شود. این دو مسئله یکی نیستند.

projection می‌تواند جدول یا سندی بسازد که از قبل همه‌ی فیلدهای موردنیاز صفحه را دارد:

OrderPlaced or OrderStatusChanged
        │
        ▼
Update OrderTrackingRow
        │
        ▼
Order tracking read store

این کار مدل دامنه‌ی دوم نمی‌سازد. projection فقط تصویری قابل‌جایگزینی از واقعیت‌هایی است که مسیر command پذیرفته است. مایکروسافت CQRS را جداسازی مدل‌های خواندن و نوشتن می‌داند تا هر کدام مستقل بهینه شوند.[1]

این مرز فقط برای یک مسئله‌ی واقعی ارزش دارد

مخزن خواندن جدا وقتی مفید است که به یکی از این‌ها نیاز دارید:

این طراحی خودبه‌خود معماری بهتری نیست. مارتین فاولر هشدار می‌دهد CQRS در بیشتر سیستم‌ها پیچیدگی پرخطری اضافه می‌کند.[2] اگر یک دیتابیس و projection مستقیم جواب می‌دهند، همان‌ها را نگه دارید. مخزن دوم باید یک مشکل اندازه‌گیری‌شده در query، گزارش‌گیری یا مقیاس‌پذیری را حل کند.

command موفق هم می‌تواند داده‌ی قدیمی نشان دهد

با یک دیتابیس، ShipOrder تغییر را commit می‌کند و GetOrderSummary بعدی می‌تواند در همان مرز تراکنش وضعیت تازه را بخواند.

با مخزن خواندن جدا، ترتیب این است:

  1. ShipOrder سفارش را اعتبارسنجی می‌کند و Status = Shipped را در مخزن نوشتن commit می‌کند.
  2. تغییر برای projection رهگیری منتشر می‌شود.
  3. projection مخزن خواندن را به‌روز می‌کند.
  4. GetOrderTracking مقدار Shipped را می‌بیند.

بین گام ۱ و ۳، صفحه‌ی رهگیری ممکن است هنوز Paid را نشان دهد. این سازگاری نهایی است، نه یک مسیر خطا. وقتی مخزن خواندن و نوشتن جدا هستند، باید آن‌ها را همگام نگه داشت و مخزن خواندن ممکن است از مخزن نوشتن عقب بماند.[1]

این واقعیت را پشت redirect پنهان نکنید و به زمان‌بندی امید نبندید. بعد از command موفق، رفتار برنامه باید روشن باشد:

انتخاب به کاری بستگی دارد که کاربر انجام داده است. کارگر انبار حتی اگر داشبورد هنوز به‌روز نشده باشد، باید بداند ارسال ثبت شده است. در یک فید عمومیِ فعالیت، تأخیر کوتاه معمولاً مسئله‌ای ایجاد نمی‌کند.

projection باید بعد از توقف ادامه دهد

projection هم سیستمی جداست. ممکن است متوقف شود، یک پیام را دوبار بگیرد یا بعد از به‌روزرسانی یک رکورد خطا دهد. پس باید راهی روشن برای بازیابی داشته باشد.

برای eventِ OrderStatusChanged، همراه با به‌روزرسانی projection یک موقعیت یا شناسه‌ی event ذخیره کنید. بعد از راه‌اندازی دوباره، پردازش را از آخرین موقعیت ثبت‌شده ادامه دهید. اگر همان پیام دوباره رسید، اجرای دوباره‌اش نباید شمارنده‌ای را اضافه کند یا ردیف تکراری بسازد.

جزئیات به نوع مخزن و شیوه‌ی انتقال بستگی دارد، اما اصل ثابت است: باید بتوان projection را از تاریخچه‌ی سمت نوشتن یا یک لاگ تغییر معتبر دیگر دوباره ساخت. در غیر این صورت، داده‌ای ساخته‌اید که مسیر command نه مالک آن است و نه می‌تواند آن را ترمیم کند.

هزینه‌ی عملیاتیِ مخزن دوم همین‌جاست: پایش تأخیر، رسیدگی به خطا و آزمودن بازپخش داده. برای queryای که یک index معمولی یا projection مستقیم پاسخ می‌دهد، این هزینه را نپردازید.

فقط وقتی نیاز خواندن واقعی است، جدا کنید

مدل خواندن جدا به query اجازه می‌دهد سؤال خودش را کارآمد پاسخ دهد. اما مخزن خواندن جدا فقط وقتی ارزش دارد که دیتابیس مشترک دیگر از پس آن نیاز برنیاید.

مسیر نوشتن را مرجع معتبر نگه دارید و projectionها را قابل‌جایگزینی بدانید. مشخص کنید سازگاری نهایی کجا برای کاربر دیده می‌شود، بعد کوچک‌ترین مسیر همگام‌سازی را بسازید که همان وعده را محقق می‌کند.

منابع

  1. Microsoft Learn, "CQRS pattern"
  2. Martin Fowler, "CQRS"

پانویس‌ها

  1. CQRS حداقلی: مسیرهای کد را جدا کنید، یک برنامه و یک دیتابیس نگه دارید
اشتراک‌گذاری در LinkedIn