داده، SQL، قراردادها و ماشین حالت · فصل ۲ — Isolation، Lock و Concurrency · 63
Locks, Deadlocks و Optimistic Concurrency
میتواند «Locks, Deadlocks و Optimistic Concurrency» را برای یک domain محدود مدل کند و constraint، compatibility یا recovery evidence لازم را مشخص کند.
خلاصهٔ درس
میتواند «Locks, Deadlocks و Optimistic Concurrency» را برای یک domain محدود مدل کند و constraint، compatibility یا recovery evidence لازم را مشخص کند.
خروجی این درس
میتواند «Locks, Deadlocks و Optimistic Concurrency» را برای یک domain محدود مدل کند و constraint، compatibility یا recovery evidence لازم را مشخص کند.
پیشنیازها
۱. صفحه اول — وقتی «منتظر ماندن»، «گیر افتادن» و «رد شدن تغییر قدیمی» سه مسئله متفاوتاند
دو worker قرار است موجودی را بین دو bin جابهجا کنند. T1 ابتدا ردیف A را با SELECT ... FOR UPDATE قفل میکند و بعد به B نیاز دارد. T2 تقریباً همزمان B را میگیرد و بعد به A نیاز پیدا میکند. اگر هر کدام منتظر دیگری بماند، دیگر با یک انتظار ساده طرف نیستیم؛ یک cycle ساختهایم که PostgreSQL باید آن را تشخیص دهد و یکی از transactionها را abort کند. در همان سیستم، یک reservation نیز version=7 دارد. کاربری که نسخهٔ 7 را خوانده ولی بعد از تغییر رقیب هنوز با همان expected version update میزند، باید conflict بگیرد؛ نه اینکه تصمیم قدیمی را بیصدا overwrite کند.
هدف این درس حفظ کردن نام lock modeها نیست. خروجی قابل قبول Concurrency Conflict Decision Record v1 است: برای هر conflict باید schedule پیشبینیشده، lock/version state، observation واقعی، SQLSTATE یا row count، تصمیم prevention/detection/retry، و یک residual risk ثبت شود.
سه مرز مهم از ابتدا روشناند. Row lock بهتنهایی correctness کل business invariant را ثابت نمیکند. consistent lock ordering فقط pattern هدف را کاهش میدهد و اثبات نبود همهٔ deadlockها نیست. application-level optimistic concurrency نیز همان سازوکار PostgreSQL Serializable نیست.
۲. مسیرها — scaffold کم و زیاد میشود، evidence bar نه
مسیر Foundation timeline دو Session و checkpointهای روشن دارد. مسیر Repair فقط misconception تشخیصدادهشده را دوباره میسازد. مسیر Experienced میتواند syntax walkthrough را کوتاه کند، اما prediction، failure evidence، mitigation، conflict policy و transfer مستقل حذفشدنی نیستند.
قبل از اجرای هر schedule چهار چیز را بنویسید: «چه objectی contested است؟»، «هر Session الان چه lock یا versionی دارد؟»، «چه چیزی باعث wait/conflict میشود؟»، و «چه observationی فرض من را رد میکند؟». اگر فقط run موفق را نگه دارید، evidence ناقص است.
۳. مدل ذهنی — ownership، wait edge، cycle و compare-and-swap
Lock ownership یعنی یک transaction روی resource مشخص lockی دارد که با درخواست رقیب ناسازگار است. Blocking زمانی رخ میدهد که T2 برای lock ناسازگار منتظر T1 است، ولی لزوماً T1 منتظر T2 نیست. یک wait edge مثل T2 → T1 داریم، نه cycle.
Deadlock وقتی شکل میگیرد که wait-for graph cycle داشته باشد؛ مثلاً T1 منتظر resource در اختیار T2 و همزمان T2 منتظر resource در اختیار T1 باشد. PostgreSQL چنین cycleای را تشخیص میدهد و یک transaction را abort میکند؛ در lab، evidence مورد انتظار SQLSTATE 40P01 است.
Lock ordering prevention است: اگر هر دو transaction contested objects را با ترتیب یکسان بگیرند، cycle دوردیفیِ مخالف این fixture شکل نمیگیرد. این با retry بعد از deadlock متفاوت است.
Optimistic compare-and-swap lock طولانیمدت را مفروض نمیگیرد. application نسخهای را که خوانده در predicate میآورد: UPDATE ... WHERE id=? AND version=?. اگر row count صفر باشد، expected version دیگر current نیست و برای قرارداد همین درس یک stale-write conflict ثبت میشود.
نمای asset://DAT-004/DAT-004-A1 دو شاخه را کنار هم میگذارد: lock ownership → wait edge → cycle/no-cycle → victim/progress و expected version → conditional UPDATE → affected rows → conflict decision.
۴. لایه پایه — پنج قرارداد فنی
Lock wait با deadlock یکی نیست
یک transaction میتواند کاملاً طبیعی منتظر transaction دیگری بماند و بعد از commit/rollback آن ادامه دهد. برای deadlock باید cycle ناسازگار داشته باشیم. منبع frozen PG18-CONCURRENCY این distinction را در چارچوب PostgreSQL 18 نگه میدارد.
SELECT FOR UPDATE همزمانی را serial نمیکند
Row-level lock میتواند competing writer/locker را منتظر کند، اما تضمین نمیکند business invariant شما درست مدل شده است. اگر invariant چند جدول یا side effect بیرونی دارد، فقط دیدن FOR UPDATE proof کافی نیست.
NOWAIT و SKIP LOCKED دو policy متفاوتاند
در PostgreSQL، NOWAIT بهجای انتظار برای row lock فوراً failure میدهد؛ در این lab محدود، SQLSTATE مورد انتظار 55P03 (lock_not_available) است. SKIP LOCKED row قفلشده را از result کنار میگذارد. منبع PG18-SELECT صریحاً هشدار میدهد که SKIP LOCKED view ناسازگار/ناقصی از داده میدهد و برای queue-like contention مناسب است، نه complete read عمومی.
Deadlock detection با prevention فرق دارد
PostgreSQL میتواند deadlock را تشخیص دهد و یک transaction را abort کند. PG18-SERFAIL، 40P01 را بهعنوان deadlock_detected معرفی میکند و retry را در دستهٔ failureهای قابل بررسی قرار میدهد. اما retry policy باید کل business operation/idempotency boundary را رعایت کند.
Version check یک contract application-level است
در این درس PostgreSQL featureی به نام «optimistic locking» ادعا نمیشود. design pattern ما یک conditional update است. اگر version expected=7 باشد و current version قبلاً 8 شده باشد، UPDATE ... WHERE version=7 صفر row تغییر میدهد. application باید این را conflict بداند و بر اساس domain تصمیم بگیرد: refresh، reject، merge یا retry.
۵. توضیح عمیق — wait-for graph را قبل از SQL حل کن
فرض کنید bin 1 و bin 2 داریم.
-- T1
BEGIN;
SELECT * FROM bins WHERE id = 1 FOR UPDATE;
-- later
SELECT * FROM bins WHERE id = 2 FOR UPDATE;
-- T2
BEGIN;
SELECT * FROM bins WHERE id = 2 FOR UPDATE;
-- later
SELECT * FROM bins WHERE id = 1 FOR UPDATE;
پس از lock اول، T1 مالک 1 و T2 مالک 2 است. درخواست دوم T1 یک edge به T2 میسازد. درخواست دوم T2 edge برگشتی میسازد. cycle کامل است. learner باید قبل از run پیشبینی کند که یکی از transactionها با 40P01 abort میشود و transaction دیگر بعد از آزاد شدن lockها میتواند پیش برود. اینکه دقیقاً کدام victim میشود را بدون evidence runtime قطعی نمیکنیم.
برای repair همان کار را با ترتیب یکسان اجرا کنید: هر دو ابتدا id کوچکتر و سپس بزرگتر را lock کنند. ممکن است یکی منتظر بماند، اما pattern cycle مخالف از fixture حذف میشود. نتیجهٔ صحیح «این targeted deadlock تکرار نشد» است، نه «دیگر هیچ deadlockی ممکن نیست».
برای optimistic path، reservation با version=7 را در نظر بگیرید:
UPDATE reservations
SET note = 'worker decision', version = version + 1
WHERE reservation_id = 1 AND version = 7;
اگر update رقیب قبلاً version را به 8 رسانده باشد، affected rows صفر است. این observation conflict oracle قرارداد ماست. علت business change یا اینکه merge درست است یا نه از row count بهتنهایی اثبات نمیشود.
۶. مثال کامل — یک fixture، پنج probe
lab://DAT-004/DAT004-LAB1 با schema disposable کار میکند.
- Blocking probe: T1 row را
FOR UPDATEنگه میدارد؛ T2 همان row را میخواهد. تفاوت انتظار یکطرفه با cycle را ثبت کنید. - Deadlock probe: T1 و T2 دو bin را با ترتیب مخالف میگیرند. stdout/stderr و
40P01transaction قربانی را نگه دارید. - Ordering repair: هر دو Session rows را با
ORDER BY idیا ترتیب ثابت 1→2 میگیرند. نشان دهید targeted cycle رخ نمیدهد، ولی wait ممکن است باقی بماند. - NOWAIT/SKIP LOCKED: روی row از قبل lockشده،
NOWAITباید failure فوری بدهد؛ روی queue،SKIP LOCKEDrow locked را نادیده میگیرد. این نتیجه را complete view تعبیر نکنید. - Optimistic CAS: یک update با current version باید یک row را تغییر دهد؛ update با stale version باید zero-row conflict ایجاد کند.
برای هر probe prediction و observation را جدا نگه دارید. فایل evidence فقط زمانی کامل است که learner output واقعی PostgreSQL را ثبت کرده باشد. authoring build observation ساختگی تولید نمیکند.
۷. Failure modeها — میانبرهایی که ظاهراً درستاند
«هر wait یعنی deadlock»
نه. wait یک edge است؛ deadlock به cycle نیاز دارد. transaction منتظر ممکن است بعد از پایان owner عادی ادامه دهد.
«FOR UPDATE یعنی invariant امن است»
فقط resource مشخص را lock کردهاید. اگر invariant از داده/عملیات دیگری ساخته میشود، lock selection ممکن است ناکافی باشد.
«Lock ordering همه deadlockها را حذف میکند»
فقط اگر همه paths برای همان resources و همان order discipline سازگار باشند، pattern هدف کاهش مییابد. resourceهای دیگر، triggerها یا paths متفاوت میتوانند cycle دیگری بسازند.
«SKIP LOCKED راه عمومی consistency است»
برعکس، عمداً locked rows را حذف میکند. این feature برای queue-like worker coordination مفید است، اما inventory/report کامل را نباید از آن نتیجه گرفت.
«zero rows یعنی update شکست فنی خورد»
در compare-and-swap این میتواند conflict مورد انتظار باشد. application باید آن را به policy domain وصل کند، نه اینکه کورکورانه تکرار کند.
«Optimistic concurrency همان Serializable است»
نه. conditional update application-level contract است. Serializable یک isolation mechanism دیتابیس با semantics متفاوت است.
۸. پل Experienced — سریعتر برو، اما evidence را حذف نکن
اگر lock modes و transaction syntax را میدانید، مستقیم به matrix بروید: Conflict | T1 Locks/Version | T2 Locks/Version | Wait Edge | Observed SQLSTATE/Row Count | Prevention or Detection | Retry/Conflict Policy | Residual Risk.
حداقل خروجی Experienced همچنان شامل یک blocking observation، یک deadlock واقعی، ordering repair، یکی از NOWAIT/SKIP LOCKED boundaries، یک stale version conflict و transfer مستقل است. هیچ پاسخ آمادهای جای session evidence را نمیگیرد.
۹. Alternatives و trade-offها — conflict policy را از invariant انتخاب کن
Pessimistic row locking وقتی مناسب است که contested state باید قبل از تصمیم write تثبیت شود و انتظار قابل قبول است. هزینهاش wait/throughput و risk deadlock در paths چند-resource است.
Consistent lock ordering technique prevention ارزان برای conflictهای چند-object است، اما نیاز به discipline سراسری در همه paths دارد و proof جامع نیست.
NOWAIT برای workflowهایی مفید است که waiting غیرقابلقبول است و application میتواند failure فوری را مدیریت کند.
SKIP LOCKED برای چند worker که taskهای مستقل queue را claim میکنند مفید است؛ ولی fairness/starvation و incomplete view باید در residual risk ثبت شود.
Optimistic version check وقتی contention کم و conflict قابلنمایش به application است مناسب است. مزیتش نگه نداشتن lock در زمان user think-time است؛ هزینهاش conflict handling، refresh/merge و امکان starvation در retry ساده است.
انتخاب policy بدون اندازهگیری workload قطعی نیست. این درس correctness evidence محدود میسازد، نه benchmark production.
۱۰. تمرین Guided و Independent
Guided
ابتدا blocking schedule را اجرا کنید و فقط یک wait edge رسم کنید. سپس همان fixture را به deadlock تبدیل کنید. قبل از request دوم هر Session، wait-for graph را بنویسید. پس از 40P01 transaction failure را پاک نکنید؛ همان failure evidence است.
بعد ordering repair را اجرا کنید. اگر فقط wait رخ داد و cycle رخ نداد، distinction را در record بنویسید. سپس queue row را در T1 lock کنید و در T2 یک بار NOWAIT و یک بار SKIP LOCKED اجرا کنید.
Independent
reservation را reset کنید. ابتدا expected version current را استفاده کنید و row count را ثبت کنید. سپس یک competing change version را افزایش دهد و دوباره expected قدیمی را بزنید. zero-row result را به conflict policy وصل کنید: refresh/reject/merge/retry؛ دلیل انتخاب باید خودتان بنویسید.
۱۱. Verification و evidence — چهار نوع گزاره را قاطی نکن
در Concurrency Conflict Decision Record v1 هر row باید روشن کند چه چیزی prediction، observation، inference و decision است.
- Prediction: قبل از run انتظار چه lock/wait/version outcomeی دارید.
- Observation: stdout/stderr، SQLSTATE، affected-row count یا row identity واقعی.
- Inference: این observation در محدودهٔ fixture چه چیزی را حمایت میکند.
- Decision: prevention/retry/conflict policy برای invariant چیست.
پنج critical dimension lock_wait, deadlock_cycle, lock_ordering, locking_clause_boundary, optimistic_compare_swap باید traceable باشند. یک failure واقعی، mitigation، changed-context transfer و حداقل یک residual limitation الزامی است.
۱۲. Assessment و transfer — job dispatcher را مستقل حل کن
چند consumer از جدول jobs کارها را claim میکنند و operator میتواند همزمان job را cancel کند. شما باید بین row locking، NOWAIT/SKIP LOCKED یا optimistic version check تصمیم بگیرید و invariant را صریح کنید.
اگر SKIP LOCKED انتخاب میکنید، evidence لازم برای starvation/fairness را توضیح دهید و آن را complete view ننامید. اگر optimistic انتخاب میکنید، stale cancel/claim race و zero-row conflict را طراحی کنید. اگر pessimistic انتخاب میکنید، wait/deadlock surface و ordering policy را مشخص کنید.
پاسخ «چون سریعتر است» یا «چون قویتر است» بدون evidence کافی نیست. final transfer باید بدون scaffold تصمیم نهایی را بگیرد.
۱۳. References و freshness boundary
این candidate فقط از frozen source boundary استفاده میکند:
PG18-CONCURRENCY— PostgreSQL 18 concurrency/explicit locking/deadlock behavior؛ retrieved2026-09-02، review due2026-12-31.PG18-SELECT— locking clauses،NOWAITوSKIP LOCKED؛ retrieved2026-09-02، review due2026-10-17.PG18-SERFAIL—40P01و bounded retry guidance؛ retrieved2026-09-02، review due2026-12-31.
در تاریخ authoring یعنی 2026-09-15 هیچ review_due نرسیده است؛ بنابراین source scope گسترش پیدا نمیکند. رفتارهای نوشتهشده PostgreSQL 18-specific هستند. نتیجهٔ fixture نیز correctness یا performance تمام workloadهای production را ثابت نمیکند.
ارزیابی
پاسخنامهی این درس در بستهی آموزش وجود ندارد؛ خودت جواب بده و صادقانه علامت بزن. «نیاز به مرور» درس را به صف تکرار میفرستد.
DAT-004-S1 retrieval
DAT-004-S2 application
DAT-004-S3 diagnosis
DAT-004-S4 decision
DAT-004-S5 transfer
ملاکهای ارزیابی (rubric)
Passing work preserves concurrency-contract correctness, traceable real evidence, scope discipline, and independent transfer. Blocking failures include: no reproduced concurrent schedule, treating one successful run as proof of race freedom, fabricated SQLSTATE/row-count evidence, describing SKIP LOCKED as a complete view, or copying the final transfer policy from scaffold.
منابع و تازگی
مطالعهٔ موارد (case studies)
فایلهای آزمایشگاه
رسانه و اسلایدهای درس
فایلهای آموزشی (دانلود)