در دنیای سیستمهای توزیعشده، ذخیرهسازی رویدادها (Event Sourcing) به عنوان یک پارادایم قدرتمند برای دستیابی به ثبات زمانی و قابلیت حسابرسی ظهور کرده است. با در نظر گرفتن تغییرات وضعیت به عنوان یک دنباله غیرقابل تغییر از رویدادها، سازمانها میتوانند وضعیت برنامه را در هر نقطه از زمان بازسازی کنند. با این حال، این رویکرد مجموعهای منحصر به فرد از چالشهای مدلسازی داده را به همراه دارد. تنش اصلی در تضاد بین نرمالیزاسیون سختگیرانه مورد نیاز برای انبار رویدادها و نیازهای عملکردی کوئریهای سنگین خواندن نهفته است. موفقیت در مدیریت این مبادله، نشاندهنده یک معماری مستحکم است.
پایه نرمالیزه: بهینهسازی انبار رویداد
در قلب ذخیرهسازی رویدادها، انبار رویدادها قرار دارد؛ یک لاگ تخصصی که هر آنچه رخ داده را ثبت میکند. استراتژی مدلسازی داده در اینجا باید بر غیرقابل تغییر بودن، اتمیک بودن و نرمالیزاسیون سختگیرانه تمرکز کند. برخلاف پایگاههای داده رابطهای سنتی که برای سرعت بخشیدن به خواندن ممکن است از غیرنرمالیزاسیون استفاده کنیم، انبار رویداد باید به شدت نرمالیزه باقی بماند.
چرا؟ زیرا انبار رویداد منبع حقیقت واحد است. معرفی دادههای تکراری یا روابط پیچیده میتواند منجر به مشکلات ثبات هنگام پخش مجدد رویدادها شود. طرح (Schema) یک رویداد باید تخت باشد و تنها شامل دادههای لازم برای توصیف تغییر وضعیت در آن لحظه خاص باشد. به عنوان مثال، ذخیره یک رویداد UserCreated باید تنها شامل شناسه کاربر، ایمیل و زمانمهر باشد، نه اینکه با یک جدول پروفایل جداگانه که ممکن است بعداً تغییر کند، جین (Join) شود.
مدل شبهکد زیر را برای یک پیکربندی رویداد در نظر بگیرید که بر یک ساختار تمیز و نرمالیزه تأکید دارد:
{
"eventId": "evt-12345-abc",
"aggregateId": "order-9876",
"eventType": "OrderCreated",
"timestamp": "2023-10-27T10:00:00Z",
"payload": {
"orderId": "order-9876",
"customerId": "cust-555",
"currency": "USD",
"totalAmount": 150.00
}
}
این ساختار تضمین میکند که لاگ رویدادها به عنوان یک دنباله خطی و فقطافزودنی باقی بماند که اعتبارسنجی و پخش مجدد آن آسان است. نرمالیزاسیون در اینجا از مشکل «تشدید نوشتن» جلوگیری میکند؛ جایی که یک تغییر منطقی واحد نیاز به بهروزرسانی چندین جدول بالقوه ناسازگار دارد.
سمت خواندن: پذیرش غیرنرمالیزاسیون
در حالی که انبار رویداد بر پایه نرمالیزاسیون میدرخشد، دنیای کوئریهای برنامهنویسی نیست. ذخیرهسازی رویدادها تقریباً همیشه با تفکیک مسئولیتهای فرمان و کوئری (CQRS) همراه است. سمت کوئری یا «مدل خواندن»، برای بهینهسازی عملکرد برای موارد استفاده خاص وجود دارد. اینجاست که غیرنرمالیزاسیون نه تنها قابل قبول، بلکه اجتنابناپذیر میشود.
غیرنرمالیزه کردن دادههای خواندن به سیستم اجازه میدهد اطلاعات را از پیش جین کند و از جینهای پرهزینه در زمان کوئری جلوگیری نماید. به جای بازسازی تاریخچه سفارشات کاربر با جین کردن رویدادهای خام، یک پروجکشن (مدل خواندن) میتواند یک ساختار تخت شامل تمام جزئیات سفارش مورد نیاز را متریالایز کند. این فرآیند تبدیل، که به عنوان پروجکشن یا متریالایزاسیون شناخته میشود، به صورت ناهمگام هنگامی که رویدادهای جدید به انبار افزوده میشوند، رخ میدهد.
یک استراتژی پروجکشن معمول شامل ایجاد یک نمایه است که دادههای مورد نیاز برای یک رابط کاربری یا نقطه پایانی API خاص را منعکس میکند. اگر یک داشبورد نیاز به لیستی از سفارشات اخیر با نام مشتریان و مقادیر کل دارد، سرویس پروجکشن رویدادهای OrderCreated، OrderStatusChanged و CustomerUpdated را مصرف کرده تا یک سند JSON از پیش محاسبه شده یا یک جدول رابطهای عریض را بسازد.
ساخت پروجکشنها: پل بین دو جهان
مکانیزمی که انبار رویداد نرمالیزه را به نمایهای خواندن غیرنرمالیزه متصل میکند، پردازنده پروجکشن است. این پردازندهها توابعی هستند که جریانهای رویداد را به مدلهای داده خاص تبدیل میکنند. طراحی این پردازندهها انعطافپذیری سیستم شما را تعیین میکند.
هنگام طراحی این پروجکشنها، توسعهدهندگان باید استراتژیهای مختلف را بر اساس الگوهای دسترسی در نظر بگیرند. یک پروجکشن «جدول تکی» ممکن است برای جستجوهای ساده کافی باشد، در حالی که یک رویکرد «چندجدولی» ممکن است برای تحلیلهای پیچیده مورد نیاز باشد. نکته کلیدی این است که اطمینان حاصل شود که منطق پروجکشن از منطق دامنه جدا است، که به مدل خواندن اجازه میدهد به طور مستقل از تعاریف اصلی رویداد تکامل یابد.
در اینجا یک مثال مفهومی از نحوه پردازش یک رویداد توسط یک پردازنده پروجکشن برای بهروزرسانی یک نمای بهینهشده برای خواندن آورده شده است:
function handleOrderCreated(event) {
const { aggregateId, payload } = event;
// دریافت مدل خواندن موجود (یا راهاندازی یکی جدید)
let readModel = readModels.get(aggregateId);
if (!readModel) {
readModel = {
orderId: aggregateId,
status: 'CREATED',
items: [],
total: 0,
customerInfo: {} // دادههای از پیش جین شده برای بازیابی سریع
};
}
// اعمال وضعیت رویداد به مدل خواندن
readModel.status = 'CREATED';
readModel.total = payload.totalAmount;
// ذخیره نمای بهینهشده
readModels.set(aggregateId, readModel);
}
نتیجهگیری: هنر تعادل
تعادل بین نرمالیزاسیون و نمایهای بهینهشده برای خواندن در سیستمهای مبتنی بر رویداد، یک تصمیم ایستا نیست، بلکه یک رقص معماری مداوم است. انبار رویداد باید به عنوان حقیقتی سختگیر و نرمالیزه باقی بماند تا از یکپارچگی دادهها اطمینان حاصل شود، در حالی که مدلهای خواندن باید به عنوان آثار انعطافپذیر و غیرنرمالیزه طراحی شوند که برای سرعت و نیازهای خاص کاربر بهینه شدهاند.
با جداسازی دقیق این نگرانیها و استفاده از CQRS، توسعهدهندگان میتوانند به سیستمی دست یابند که هم در برابر فساد داده مقاوم است و هم تحت بارهای سنگین عملکردی دارد. نکته کلیدی این است که دادههای خود را برای نوشتن و خواندن به صورت متفاوت مدل کنید. پیچیدگی جریان رویداد را بپذیرید تا رابطهای کوئری ساده، سریع و قابل اعتماد را تقویت کنید. با انجام این کار، شما پتانسیل واقعی ذخیرهسازی رویداد را آزاد میکنید و یک الگوی نظری را به یک راهحل مهندسی عملی و مقیاسپذیر تبدیل میکنید.