داده، SQL، قراردادها و ماشین حالت · فصل ۲ — Isolation، Lock و Concurrency · 62
Transactions و Isolation Levels
میتواند «Transactions و Isolation Levels» را برای یک domain محدود مدل کند و constraint، compatibility یا recovery evidence لازم را مشخص کند.
خلاصهٔ درس
میتواند «Transactions و Isolation Levels» را برای یک domain محدود مدل کند و constraint، compatibility یا recovery evidence لازم را مشخص کند.
خروجی این درس
میتواند «Transactions و Isolation Levels» را برای یک domain محدود مدل کند و constraint، compatibility یا recovery evidence لازم را مشخص کند.
پیشنیازها
۱. صفحه اول — وقتی هر دستور درست است اما نتیجهٔ همزمانی غلط میشود
فرض کنید فقط یک واحد از یک کالا باقی مانده است. اپراتور T1 مقدار موجودی را میخواند و میبیند 1 واحد آزاد است. تقریباً همزمان T2 هم همان وضعیت را میبیند. اگر مرز Transaction، Snapshot و سیاست retry روشن نباشد، هر دو مسیر میتوانند در تست تککاربره کاملاً منطقی به نظر برسند اما زیر همزمانی به نتیجهای برسند که invariant کسبوکار را نقض کند.
این درس دربارهٔ حفظ کردن چهار برچسب Isolation نیست. خروجی قابل قبول، Transaction Isolation Evidence Record v1 است: برای یک سناریوی محدود باید مرز BEGIN تا COMMIT/ROLLBACK، سطح Isolation، پیشبینی قبل از اجرا، مشاهدهٔ واقعی هر Session، خطای احتمالی مثل SQLSTATE 40001، تصمیم retry، invariant محافظتشده و یک محدودیت باقیمانده را ثبت کنید.
مرز درس نیز صریح است. اینجا طراحی تفصیلی lock، deadlock و optimistic concurrency را جذب نمیکنیم؛ آن outcome متعلق به درس بعدی است. همچنین rollback دیتابیس را معادل rollback پیام، ایمیل، فایل یا هر side effect بیرون از دیتابیس فرض نمیکنیم.
۲. مسیرها — میزان راهنمایی متفاوت است، evidence bar ثابت میماند
مسیر Foundation از timeline دو Session و دستورهای گامبهگام استفاده میکند. مسیر Repair فقط همان misconception تشخیصدادهشده را دوباره آزمایش میکند. مسیر Experienced میتواند توضیح syntax را کوتاه کند، اما اجازه ندارد prediction، failure evidence، retry rationale یا transfer مستقل را حذف کند.
قبل از اجرای هر probe سه جمله بنویسید: «invariant چیست؟»، «T1 و T2 در هر نقطه چه چیزی را باید ببینند؟»، و «چه مشاهدهای پیشبینی من را رد میکند؟». اگر بعد از اجرا فقط خروجی موفق را نگه دارید، evidence ناقص است؛ شکست کنترلشده نیز بخشی از مدرک یادگیری است.
۳. مدل ذهنی — Transaction، Snapshot، Invariant و Retry چهار مفهوم جدا هستند
Transaction boundary میگوید کدام عملیات دیتابیسی باید بهعنوان یک واحد commit یا rollback شوند. این مرز باید با عملیات کسبوکار همراستا باشد، نه با راحتی syntax.
Snapshot/visibility مشخص میکند هر statement یا transaction کدام نسخههای commitشدهٔ داده را میتواند ببیند. این موضوع با atomicity یکی نیست.
Invariant شرطی است که سیستم باید حفظ کند؛ مثلاً «reserved_qty هرگز از capacity بیشتر نشود». بدون invariant، انتخاب Isolation فقط انتخاب یک label است.
Retry boundary تعیین میکند در صورت conflict چه چیزی از نو اجرا میشود. در PostgreSQL، برای serialization failure باید کل transaction logic از ابتدا با ورودیهای تازه اجرا شود؛ تکرار فقط statement شکستخورده میتواند تصمیم قبلی را روی snapshot منقضی نگه دارد. اگر داخل flow یک side effect بیرونی دارید، idempotency آن باید جداگانه طراحی شود.
نمای دیداری این درس در asset://DAT-003/DAT-003-A1 همین زنجیره را نشان میدهد: T1/T2 → BEGIN → snapshot point → read/write → concurrent COMMIT → second observation → COMMIT یا serialization failure → whole-transaction retry.
۴. لایه پایه — پنج قرارداد فنی که باید دقیق بمانند
Transaction مرز یک عملیات دیتابیسی است، نه یک طلسم ACID
BEGIN و COMMIT چند statement را در یک واحد دیتابیسی قرار میدهند؛ ROLLBACK تغییرات دیتابیسی همان transaction را کنار میگذارد. اما این بهخودیخود نمیگوید invariant شما درست انتخاب شده یا side effectهای بیرونی برگردانده میشوند.
Read Committed در PostgreSQL snapshot سراسریِ transaction نمیدهد
طبق منبع frozen PG18-ISO، Read Committed سطح پیشفرض PostgreSQL 18 است و یک SELECT معمولی snapshot را در شروع همان statement میگیرد. بنابراین اگر T2 بین دو SELECT از T1 commit کند، SELECT دوم T1 میتواند دادهٔ تازهتر را ببیند. این رفتار را نباید به همهٔ DBMSها تعمیم داد.
Repeatable Read در PostgreSQL snapshot پایدارتر دارد
در PostgreSQL 18، Repeatable Read یک snapshot پایدار برای transaction ایجاد میکند. در schedule محدود این درس، T1 پس از commit شدن تغییر T2 همچنان مشاهدهٔ سازگار با snapshot قبلی خود دارد. اگر T1 بخواهد روی ردیفی که پس از snapshot او توسط transaction دیگری تغییر کرده update انجام دهد، restart میتواند لازم شود. semantics دقیق Repeatable Read بین محصولات دیتابیس یکسان فرض نمیشود.
Serializable نتیجهٔ قابلserial شدن میخواهد، نه commit تضمینی در اولین تلاش
Serializable در PostgreSQL تلاش میکند اثر transactionهای commitشده با یک اجرای serial سازگار باشد. برای حفظ این قرارداد ممکن است یکی از transactionها abort شود. SQLSTATE 40001 evidence شکست serialization است، نه «خرابی غیرمنتظرهٔ دیتابیس». منبع PG18-SERFAIL صریحاً retry کل transaction را در این دستهٔ failureها مطرح میکند.
READ UNCOMMITTED در PostgreSQL را از روی نام استاندارد تفسیر نکن
منبع PG18-SETTX مشخص میکند PostgreSQL برای READ UNCOMMITTED رفتار Read Committed را بهکار میبرد. تمرین باید رفتار مشاهدهشده را بررسی کند؛ صرف دیدن label در syntax، evidence کافی نیست.
۵. توضیح عمیق — یک schedule دو Session را قبل از اجرا روی کاغذ حل کن
سناریوی ساده را در نظر بگیرید. مقدار اولیهٔ reserved_qty=0 و capacity=1 است.
در Read Committed:
-- T1
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT capacity - reserved_qty AS remaining FROM inventory WHERE item_id = 1;
-- اینجا T2 update و COMMIT میکند
SELECT capacity - reserved_qty AS remaining FROM inventory WHERE item_id = 1;
COMMIT;
اگر T2 بین دو SELECT مقدار reserved_qty را به 1 تغییر دهد و commit کند، انتظار محدود این است که T1 ابتدا 1 و سپس 0 را ببیند. نکته این نیست که Read Committed «بد» است؛ نکته این است که دو مشاهده در یک transaction لزوماً از یک transaction-wide snapshot نمیآیند.
همین schedule را با Repeatable Read تکرار کنید. حالا second read باید نسبت به snapshot T1 پایدار بماند. اگر پس از آن T1 تلاش کند همان ردیف تغییرکرده را update کند، نتیجهٔ conflict را ثبت کنید و بهجای حدس، SQLSTATE/خروجی واقعی را در evidence بنویسید.
برای Serializable یک invariant چندردیفی ایجاد میکنیم: در جدول reservation، دو transaction ابتدا میخوانند که هنوز هیچ reservation فعالی وجود ندارد و سپس از همان guard ظرفیت برای insert روی ردیفهای متفاوت استفاده میکنند. اگر هر دو transaction اولیه commit شوند invariant «حداکثر یک reservation فعال» میشکند. در schedule fixture انتظار داریم حداقل یکی از transactionهای اولیه نیازمند restart شود. نکتهٔ تعیینکننده این است که retry یعنی اجرای دوبارهٔ کل transaction از BEGIN و ارزیابی دوبارهٔ guard؛ بعد از commit رقیب، retry باید state جدید را ببیند و از ایجاد reservation دوم صرفنظر کند، نه اینکه فقط INSERT نهایی را کورکورانه تکرار کند. این انتظار authoring-build evidence واقعی نیست؛ learner باید runtime را اجرا و outcome را ثبت کند.
۶. مثال کامل — از inventory تککاربره تا policy قابل دفاع
fixture lab://DAT-003/DAT003-LAB1 سه probe دارد. ابتدا baseline تکtransaction را اجرا میکنید تا schema و seed را تأیید کنید. سپس visibility probe را زیر Read Committed و Repeatable Read انجام میدهید. در پایان serialization probe را اجرا میکنید و شکست کنترلشده را نگه میدارید.
ترتیب evidence چنین است:
- invariant را قبل از SQL بنویسید؛ مثلاً
active reservations <= capacity. - مرز transaction را مشخص کنید؛ read تصمیمساز و write مربوط به همان تصمیم باید در scope قابل دفاع باشند.
- پیشبینی T1/T2 را قبل از اجرا ثبت کنید.
- Session output واقعی را ذخیره کنید؛ اگر runtime ندارید، placeholder را پر نکنید.
- اگر
40001رخ داد، transaction شکستخورده را بهعنوان evidence نگه دارید و کل transaction را ازBEGINretry کنید؛ فقط statement نهایی را تکرار نکنید. - تأیید کنید retry دوباره guard ظرفیت را ارزیابی کرده است و سپس
active reservations <= capacityرا بررسی کنید. - یک residual risk بنویسید؛ مثلاً «این fixture رفتار PostgreSQL 18 در یک workload محدود را نشان میدهد و دربارهٔ latency/throughput تولید ادعایی ندارد».
هدف «هرچه Isolation قویتر بهتر» نیست. اگر invariant با یک statement اتمیک سادهتر یا constraint مناسب حل میشود، آن گزینه را باید بهعنوان alternative ثبت کنید؛ جزئیات locking را برای DAT-004 نگه دارید.
۷. Failure modeها — پنج میانبر قابلباور اما غلط
A. «Serializable یعنی دیگر retry لازم نیست»
برعکس، یکی از هزینههای correctness در Serializable این است که transaction ممکن است برای جلوگیری از serialization anomaly abort شود. نبود retry path یک defect طراحی است.
B. «Repeatable Read همهجا همین معنا را دارد»
این درس فقط رفتار PostgreSQL 18 را claim میکند. نام استاندارد را به implementationهای دیگر کپی نکنید.
C. «Read Committed یعنی تمام SELECTهای transaction یک تصویر را میبینند»
در PostgreSQL ordinary SELECTهای Read Committed snapshot statement-level دارند. schedule دو SELECT با commit میانی دقیقاً برای شکستن همین تصور طراحی شده است.
D. «ROLLBACK ایمیل یا پیام ارسالشده را هم برمیگرداند»
Database transaction فقط state دیتابیس تحت کنترل خودش را rollback میکند. side effect بیرونی به design جداگانه مثل idempotency/outbox نیاز دارد؛ این درس آن معماری را تکمیل نمیکند.
E. «Isolation قویتر جای invariant را میگیرد»
اگر نتوانید بگویید چه شرطی باید حفظ شود و چه observation آن را نقض میکند، انتخاب Serializable فقط تغییر configuration است، نه استدلال مهندسی.
۸. پل Experienced — syntax را کوتاه کن، evidence را نه
اگر با PostgreSQL و ACID آشنا هستید، توضیح BEGIN/COMMIT را عبور دهید. اما این چهار خروجی حذفشدنی نیستند: timeline دو Session، prediction قبل از run، یک conflict واقعی، و decision+residual risk.
برای fast path مستقیماً matrix را پر کنید: Session | Transaction Boundary | Isolation Level | Statement | Predicted Visibility | Observed Visibility/SQLSTATE | Retry Decision | Protected Invariant | Residual Risk. هر خانهای که runtime میخواهد باید از output واقعی پر شود، نه از پاسخ این درس.
۹. Alternatives و trade-offها — policy را از invariant شروع کن
Read Committed برای بسیاری از workloadها کافی است وقتی هر تصمیم حساس در یک statement اتمیک یا سازوکار صریح دیگری بسته میشود. مزیتش conflict کمتر است، اما اگر application چند read را یک snapshot واحد فرض کند، همین assumption باید اصلاح شود.
Repeatable Read برای reasoning با snapshot پایدارتر مفید است، ولی application باید برای serialization failure آماده باشد و semantics آن را PostgreSQL-specific بداند.
Serializable قویترین isolation level این سه برای جلوگیری از serialization anomaly است، اما retry یک مسیر طبیعی طراحی است و throughput/latency باید جداگانه اندازهگیری شود.
Explicit locking یا optimistic concurrency alternativeهای واقعیاند، اما طراحی دقیق lock mode، deadlock و version check outcome این درس نیست و به DAT-004 منتقل میشود. در اینجا فقط میتوانید بنویسید «نیاز به کنترل صریحتر داریم» و دلیل را ثبت کنید.
۱۰. تمرین Guided و Independent
Guided
برای Read Committed جدول prediction را قبل از اجرا تکمیل کنید: T1 read اول، T2 update+commit، T1 read دوم. سپس lab/DAT003-LAB1 را اجرا و observation را کنار prediction قرار دهید. اگر نتیجه متفاوت بود، اول environment/version و ترتیب واقعی Sessionها را بررسی کنید.
برای Repeatable Read همان schedule را تکرار کنید. بعد T1 را مجبور کنید روی ردیف تغییرکرده write کند و نتیجهٔ transaction را ثبت کنید. پاسخ درست، حفظ کردن متن خطا نیست؛ evidence باید نشان دهد conflict در کدام مرز رخ داده و retry چه scopeای دارد.
Independent
بدون scaffold، Serializable schedule مربوط به reservation را اجرا کنید. قبل از اجرای transaction دوم invariant را صریح بنویسید. سپس outcome هر دو Session، SQLSTATE در صورت failure، transaction retry و check نهایی invariant را ثبت کنید. اگر هر دو transaction بدون conflict commit شدند، آن را «PASS» اعلام نکنید؛ schedule و fixture را بررسی کنید چون observation با انتظار bounded oracle ناسازگار است.
۱۱. Verification و Evidence — مدرک باید prediction را از observation جدا کند
Transaction Isolation Evidence Record v1 پنج dimension دارد: transaction_boundary, read_committed_snapshot, repeatable_read_snapshot, serializable_retry, isolation_decision.
برای هر dimension چهار نوع عبارت را قاطی نکنید:
- Prediction: قبل از اجرا چه انتظار دارید و چرا.
- Observation: stdout/stderr، مقدار row یا SQLSTATE که واقعاً دیده شده.
- Inference: observation دربارهٔ contract چه میگوید.
- Decision: چه Isolation/retry policy را برای invariant محدود انتخاب میکنید.
یک evidence کامل حداقل یک positive observation، یک deliberate failure، یک repair/retry، یک changed-context transfer و یک residual limitation دارد. نبود PostgreSQL runtime در authoring build به معنی نبود evidence واقعی است؛ placeholderهای TO_FILL_FROM_REAL_RUNTIME عمداً دستنخورده میمانند.
۱۲. Assessment و Transfer — ظرفیت صندلی را بدون کپی SQL حل کن
در transfer، یک سرویس رزرو صندلی remaining capacity را میخواند و reservation مینویسد. باید مشخص کنید transaction از کجا شروع و کجا تمام میشود، چه invariantی محافظت میشود، کدام Isolation level را انتخاب میکنید، چه failure/retryای ممکن است رخ دهد و چه observationی انتخاب شما را falsify میکند.
پاسخ «Serializable چون قویتر است» کافی نیست. پاسخ «Read Committed چون سریعتر است» نیز بدون evidence کافی نیست. تصمیم باید بین invariant، schedule، observed behavior و retry boundary trace داشته باشد.
سه failure blocking هستند: application case بدون runtime evidence، diagnosis بدون deliberate failure، و transferی که تصمیم نهایی آن از scaffold کپی شده باشد.
۱۳. References و freshness boundary
این lesson فقط به source boundary frozen زیر تکیه میکند:
PG18-ISO— مستندات PostgreSQL 18 دربارهٔ Transaction Isolation، snapshotها و رفتار levelها؛ retrieved2026-09-02، review due2026-12-31.PG18-SERFAIL— راهنمای PostgreSQL 18 برای serialization/deadlock retry و SQLSTATEها؛ retrieved2026-09-02، review due2026-12-31.PG18-SETTX— syntax فعلیSET TRANSACTIONو رفتار PostgreSQL برایREAD UNCOMMITTED؛ retrieved2026-09-02، review due2026-10-17.
تا تاریخ authoring یعنی 2026-09-15 هیچ review_due نرسیده است؛ بنابراین source scope گسترش داده نمیشود. این درس رفتار PostgreSQL 18 را توضیح میدهد، نه یک قانون جهانی برای همهٔ پایگاههای داده. evidence تولیدی learner نیز فقط همان fixture محدود را اثبات میکند، نه correctness یا performance تمام workloadهای تولید.
ارزیابی
پاسخنامهی این درس در بستهی آموزش وجود ندارد؛ خودت جواب بده و صادقانه علامت بزن. «نیاز به مرور» درس را به صف تکرار میفرستد.
DAT-003-S1 retrieval
State the transaction boundary and one PostgreSQL-specific isolation fact for the bounded inventory case, then name one explicit non-goal.
DAT-003-S2 application
Run the bounded two-session visibility schedule under Read Committed and Repeatable Read. Record prediction, real observation, transaction boundary, and version evidence.
DAT-003-S3 diagnosis
Run the supplied Serializable conflict schedule. Preserve the failed transaction evidence, identify SQLSTATE when present, explain why a whole-transaction retry is required, and rerun it.
DAT-003-S4 decision
Defend an isolation/retry policy for the inventory invariant against at least one plausible alternative. Separate PostgreSQL observation from application-design inference.
DAT-003-S5 transfer
For a seat-allocation service, define the transaction boundary, protected invariant, isolation/retry policy, falsifying concurrent observation, and residual limitation without copying the inventory SQL.
ملاکهای ارزیابی (rubric)
A passing submission preserves all four critical rubric dimensions: concurrency contract correctness, traceable real evidence, scope discipline, and independent transfer. Blocking failure occurs when a required concurrent schedule is not actually reproduced or bounded, one happy-path run is used as sole proof, fabricated runtime output appears, or the final transfer decision is supplied by scaffold rather than learner reasoning.
منابع و تازگی
مطالعهٔ موارد (case studies)
فایلهای آزمایشگاه
رسانه و اسلایدهای درس
فایلهای آموزشی (دانلود)