Database Engineering

تسلط بر اسنپ‌شات‌ها و پروژکشن‌ها در Event Sourcing با SQL

رویداد-محوری (Event Sourcing) یک الگوی معماری قدرتمند است که وضعیت برنامه را به عنوان دنباله‌ای از رویدادها در نظر می‌گیرد. اگرچه این رویکرد ردپای تغییرات غیرقابل تغییر و قابلیت‌های بازنویسی قدرتمندی ارائه می‌دهد، اما چالش عملکردی قابل توجهی ایجاد می‌کند: بازسازی وضعیت فعلی یک موجودیت با بازنویسی تک‌تک رویدادها از نظر محاسباتی پرهزینه و از نظر ورودی/خروجی (I/O) سنگین است. برای توسعه‌دهندگان متوسط تا پیشرفته‌ای که سیستم‌هایی را روی پایگاه‌های داده رابطه‌ای می‌سازند، راه‌حل در یک استراتژی مستحکم شامل اسنپ‌شات‌ها (Snapshots) و پروژکشن‌ها (Projections) نهفته است.

چالش بازنویسی رویدادها

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

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

پیاده‌سازی اسنپ‌شات‌ها در پایگاه‌های داده رابطه‌ای

اگرچه انبارهای اسناد NoSQL اغلب به دلیل انعطاف‌پذیری آن‌ها با داده‌های نرمال‌سازی‌نشده تحسین می‌شوند، پایگاه‌های داده رابطه‌ای می‌توانند اسنپ‌شات‌ها را به طور مؤثر از طریق یک ساختار جدول ساده مدیریت کنند. نکته کلیدی این است که اسنپ‌شات را به شناسه ریشه مجموعه (Aggregate Root) پیوند دهید و نسخه آن را ردیابی کنید.

در اینجا یک مثال عملی از نحوه ساختاردهی یک جدول اسنپ‌شات در SQL آورده شده است که برای کار در کنار یک جدول رویدادها طراحی شده است:

CREATE TABLE AggregateSnapshots (
    AggregateId UNIQUEIDENTIFIER NOT NULL,
    EventType VARCHAR(255) NOT NULL,
    SnapshotData NVARCHAR(MAX) NOT NULL,
    Version BIGINT NOT NULL,
    CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    CONSTRAINT PK_AggregateSnapshots PRIMARY KEY (AggregateId)
);

CREATE INDEX IX_AggregateSnapshots_Version 
ON AggregateSnapshots (AggregateId, Version DESC);

در این طرحواره، SnapshotData وضعیت سریال‌شده موجودیت را ذخیره می‌کند (که اغلب در نمونه‌های مدرن SQL Server یا PostgreSQL به صورت JSON است). فیلد Version حیاتی است؛ زیرا به منطق برنامه اجازه می‌دهد تعیین کند که آیا اسنپ‌شات قدیمی‌تر از جریان رویداد فعلی است و نیاز به به‌روزرسانی دارد یا خیر.

قفل‌گذاری خوش‌بینانه و یکپارچگی

یکی از پیچیدگی‌های ظریف به‌روزرسانی اسنپ‌شات‌ها در یک پایگاه داده رابطه‌ای، مدیریت همزمانی است. شما نمی‌توانید به سادگی یک اسنپ‌شات را بازنویسی کنید، زیرا این کار شرایط مسابقه (Race Conditions) ایجاد می‌کند که در آن نوشتن‌های همزمان ممکن است منجر به از دست رفتن داده‌ها شوند. در عوض، باید از قفل‌گذاری خوش‌بینانه (Optimistic Locking) استفاده کنید.

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

پروژکشن‌ها: سمت خواندن رویداد-محوری

اسنپ‌شات‌ها سمت نوشتن و بازسازی وضعیت موجودیت را بهینه می‌کنند. با این حال، آن‌ها به کوئری‌های پیچیده خواندن کمک نمی‌کنند، مانند «همه مشتریان را نشان بده که در سال گذشته بیش از 1000 دلار هزینه کرده‌اند.» برای این کار، به پروژکشن‌ها نیاز داریم.

پروژکشن‌ها مدل‌های بهینه‌شده برای خواندن هستند که از جریان رویداد مشتق می‌شوند. آن‌ها مدل نوشتن را از مدل خواندن جدا می‌کنند. در یک زمینه رابطه‌ای، این اغلب به معنای ایجاد جداول نرمال‌سازی‌نشده است که توسط دستگیرنده‌های رویداد (یا یک موتور پروژکشن جداگانه) هنگام پایدارسازی رویدادهای جدید به‌روزرسانی می‌شوند.

CREATE TABLE CustomerSpendingTotals (
    CustomerId UNIQUEIDENTIFIER PRIMARY KEY,
    TotalSpent DECIMAL(18, 2) DEFAULT 0.00,
    LastOrderDate DATETIME2,
    UpdatedVersion BIGINT
);

-- مثال منطق دستگیرنده رویداد (نوشته شبه‌کد)
public void Handle(OrderPlacedEvent @event) {
    // 1. منطق به‌روزرسانی اسنپ‌شات موجودیت را اینجا قرار دهید...
    
    // 2. به‌روزرسانی پروژکشن
    UPDATE CustomerSpendingTotals 
    SET TotalSpent = TotalSpent + @event.OrderValue,
        LastOrderDate = @event.OrderDate
    WHERE CustomerId = @event.CustomerId;

با نگهداری این جداول پروژکشن، برنامه شما می‌تواند گزارش‌ها و داشبوردهای پیچیده را مستقیماً از SQL سرویس دهد، بدون اینکه نیاز باشد در هر درخواست، جریان رویداد را بازنویسی کند.

نتیجه‌گیری

ترکیب رویداد-محوری با پایگاه‌های داده رابطه‌ای نیازمند رویکردی آگاهانه به ذخیره‌سازی داده‌ها است. اسنپ‌شات‌ها افزایش عملکرد لازم برای بازسازی وضعیت موجودیت را فراهم می‌کنند، در حالی که پروژکشن‌ها کوئری‌های پیچیده و کارآمد را امکان‌پذیر می‌سازند. با بهره‌گیری از قابلیت‌های JSON در SQL Server یا PostgreSQL و پیاده‌سازی کنترل همزمانی خوش‌بینانه، می‌توانید سیستم‌های مقیاس‌پذیر و قابل نگهداری بسازید که از مزایای رویداد-محوری بدون قربانی کردن عملکرد خواندن برخوردار باشند. با رشد برنامه شما، به یاد داشته باشید که جداسازی مدل‌های نوشتن و خواندن نه تنها یک انتخاب معماری است، بلکه ضروری برای عملکرد بلندمدت است.

Share: