Database Engineering

ساخت سیستم‌های با تراکنش بالا و قابل حسابرسی: Event Sourcing و CQRS با PostgreSQL

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

اینجاست که ترکیب جداسازی مسئولیت‌های دستورات و پرس‌وجوها (CQRS) و منبع رویداد (Event Sourcing) درخشش می‌کند. وقتی به درستی با PostgreSQL، یک موتور پایگاه داده رابطه‌ای قدرتمند، پیاده‌سازی شود، می‌توانید به سیستمی دست یابید که نه تنها تحت بار سنگین عملکرد بالایی دارد، بلکه ذاتاً با استانداردهای نظارتی نیز سازگار است. این پست به بررسی الگوهای معماری، استراتژی‌های طراحی پایگاه داده و جزئیات پیاده‌سازی عملی برای ساخت چنین سیستم‌هایی می‌پردازد.

درک الگوهای اصلی

منبع رویداد (Event Sourcing) روش بنیادین ذخیره‌سازی داده را تغییر می‌دهد. به جای ذخیره وضعیت فعلی یک موجودیت (مثلاً سفارشی با وضعیت "ارسال شده")، شما دنباله‌ای از رویدادهای دست‌نخورده را ذخیره می‌کنید که به آن وضعیت منجر شده‌اند (مثلاً "سفارش ایجاد شد"، "پرداخت پردازش شد"، "کالا ارسال شد"). برای بازیابی وضعیت فعلی، این رویدادها را مجدداً پخش می‌کنید. این رویکرد به طور تعریفی یک ردیابی حسابرسی کامل را فراهم می‌کند.

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

طراحی PostgreSQL برای جریان‌های رویداد

PostgreSQL به دلیل انطباق ACID، پشتیبانی قدرتمند از JSONB و کنترل همزمانی عالی، انتخابی عالی برای منبع رویداد است. جدول اصلی برای یک انبار رویداد معمولاً یک لاگ فقط-برای-نوشتن است. در زیر تعریف طرحی وجود دارد که بر عملکرد نوشتن و یکپارچگی داده‌ها اولویت می‌دهد.

CREATE TABLE event_store (
    id BIGSERIAL PRIMARY KEY,
    aggregate_id UUID NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    payload JSONB NOT NULL,
    version INTEGER NOT NULL,
    occurred_at TIMESTAMPTZ DEFAULT NOW()
);

-- ایندکس‌ها برای عملکرد حیاتی هستند
CREATE INDEX idx_event_aggregate ON event_store (aggregate_id, version);
CREATE INDEX idx_event_occurred ON event_store (occurred_at DESC);

به ستون version توجه کنید. این برای کنترل همزمانی خوش‌بینانه ضروری است. هنگام نوشتن یک رویداد، برنامه بررسی می‌کند که آیا نسخه فعلی aggregate با نسخه مورد انتظار مطابقت دارد یا خیر. اگر مطابقت داشته باشد، رویداد الحاق شده و نسخه افزایش می‌یابد. اگر نه، تعارضی رخ می‌دهد و عملیات نوشتن شکست می‌خورد که از شرایط رقابتی جلوگیری می‌کند.

پیاده‌سازی کنترل همزمانی خوش‌بینانه

تراکنش بالا نیاز به مکانیزم‌های قفل‌گذاری کارآمد دارد. به جای قفل‌های سطح سطر که می‌توانند عملیات نوشتن را سریالیزه کنند، منبع رویداد از کنترل همزمانی خوش‌بینانه استفاده می‌کند. در اینجا نحوه پیاده‌سازی یک عملیات نوشتن تراکنشی در PostgreSQL با استفاده از PL/pgSQL آورده شده است.

CREATE OR REPLACE FUNCTION append_event(
    p_aggregate_id UUID,
    p_event_type VARCHAR,
    p_payload JSONB,
    p_expected_version INTEGER
) RETURNS BIGINT AS $$
DECLARE
    v_new_version INTEGER;
    v_row_count INTEGER;
BEGIN
    -- بررسی وجود aggregate و اعتبارسنجی نسخه
    SELECT version INTO v_new_version
    FROM event_store
    WHERE aggregate_id = p_aggregate_id
    ORDER BY version DESC
    LIMIT 1;

    IF v_new_version IS NULL THEN
        v_new_version := 0;
    END IF;

    IF v_new_version != p_expected_version THEN
        RAISE EXCEPTION 'Concurrency conflict: expected version % but found %', 
                        p_expected_version, v_new_version;
    END IF;

    -- درج رویداد جدید
    INSERT INTO event_store (aggregate_id, event_type, payload, version)
    VALUES (p_aggregate_id, p_event_type, p_payload, v_new_version + 1);

    RETURN v_new_version + 1;
END;
$$ LANGUAGE plpgsql;

پروجکشن‌ها برای خواندن با عملکرد بالا

از آنجا که خواندن از جریان رویداد خام از نظر محاسباتی پرهزینه است، CQRS ایجاب می‌کند که پروجکشن‌های (یا نمای‌های متریال‌شده) جداگانه‌ای برای عملیات خواندن حفظ کنیم. این پروجکشن‌ها به صورت ناهمگام یا همگام هنگام ثبت رویدادها به‌روز می‌شوند. برای مثال، ممکن است یک جدول orders_summary غیرعادی (denormalized) را حفظ کنید که امکان پرس‌وجوهای سریع بر اساس مشتری یا تاریخ را بدون نیاز به پخش مجدد کل تاریخچه هر سفارش فراهم می‌کند.

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

نتیجه‌گیری

پیاده‌سازی منبع رویداد و CQRS با PostgreSQL یک راه‌حل جادویی نیست؛ این کار پیچیدگی‌هایی را از نظر منطق برنامه و مدیریت سازگاری نهایی (eventual consistency) معرفی می‌کند. با این حال، برای سیستم‌هایی که به تراکنش بالا و ردیابی‌های حسابرسی دقیق نیاز دارند، این رویکرد پایه معماری برتری ارائه می‌دهد. با بهره‌گیری از ویژگی‌های قدرتمند PostgreSQL برای کنترل همزمانی و ذخیره‌سازی JSONB، توسعه‌دهندگان می‌توانند سیستم‌هایی بسازند که هم مقیاس‌پذیر و هم سازگار باشند. کلید موفقیت، طراحی دقیق پروجکشن‌ها و سرمایه‌گذاری در دست‌اندرکاران رویداد (event handlers) قوی برای همگام‌سازی سمت خواندن با تاریخچه دست‌نخورده ذخیره‌شده در سمت نوشتن است.

Share: