1 تراکنش و معنای واقعی ACIDTransactions and the real meaning of ACID
۱.۱ تراکنش چیست و چرا؟1.1 What is a transaction, and why?
تراکنش چند خواندن و نوشتن را به یک واحد منطقی تبدیل میکند. در محدودهٔ تضمین پایگاه داده، یا تغییرات تأیید میشوند یا تراکنش لغو میشود؛ اثرهای بیرونی مثل پیامک خودبهخود برنمیگردند.
مثال فنی: Rollback بعد از خطای درج
وضعیت اولیه: موجودی P7=10 است؛ سفارش ۴۲ هنوز ثبت نشده است.
اجرای مرحلهبهمرحله: داخل BEGIN موجودی به ۹ کم میشود؛ درج سفارش خطا میدهد و ROLLBACK مقدار را به ۱۰ برمیگرداند.
نتیجه و دلیل: atomicity اثر ناقص را حذف میکند؛ پیامک بیرون پایگاه داده با rollback خودکار پس گرفته نمیشود.
In real data systems many things can break: the database software or hardware can die at any moment (even mid-write); the application can crash mid-operation; network cuts separate app from database or nodes from each other; concurrent clients overwrite each other; a client reads half-updated data and race conditions breed bugs.
تراکنش راهی است برای گروهبندیِ چند خواندن و نوشتن در یک واحدِ منطقی: مفهوماً همه بهصورتِ یک عملیاتِ واحد اجرا میشوند؛
یا کلِ تراکنش موفق است (commit) یا شکست میخورد (abort یا
rollback) و برنامه میتواند با خیالِ راحت دوباره تلاش کند. مدیریتِ خطا خیلی سادهتر میشود،
چون دیگر لازم نیست نگرانِ «partial failure» باشی (وضعیتی که بعضی عملیاتها موفق و بعضی ناموفقاند).
A transaction groups reads and writes into one logical unit: conceptually all run as a single operation; either the whole transaction succeeds (commit) or it fails (abort/rollback) and the app can safely retry. Error handling gets far simpler — no more worrying about partial failure, where some operations succeed and others don't.
تراکنش قانونِ طبیعت نیست؛ برای سادهسازیِ مدلِ برنامهنویسی ساخته شد. هر برنامهای به تراکنش نیاز ندارد و گاهی ضعیفکردنِ
ضمانتها (برای کارایی یا دسترسپذیریِ بیشتر) مزیت دارد. اما نبودِ تراکنش یعنی مسوولیتِ همهٔ آن سناریوهای خرابی با خودت است —
و کارِ سختِ تستگیری. (نمونهٔ معروف: رسواییِ Horizon در ادارهٔ پستِ بریتانیا که ریشهٔ فنیاش احتمالاً نبودِ تراکنشهای ACID
در سیستمِ حسابداری بود.)
Transactions aren't a law of nature; they were invented to simplify the programming model. Not every app needs them, and weakening guarantees (for performance or availability) is sometimes the right trade. But without transactions, every failure scenario is your own responsibility — plus the hard testing work. (The famous UK Post Office Horizon scandal was likely rooted in missing ACID transactions in the accounting system.)
۱.۲ چهار حرف ACID دقیقاً یعنی چه1.2 What the four ACID letters really mean
ACID چهار موضوع را جدا میکند: همهیاهیچ بودن، حفظ قواعد داده، کنترل اثر تراکنشهای همزمان و ماندگاری پس از تأیید. نام ACID بهتنهایی جزئیات این تضمینها را مشخص نمیکند.
مثال فنی: چهار تضمین روی رزرو
وضعیت اولیه: برای صندلی S7 یک رزرو و یک تغییر وضعیت در همان پایگاه داده داریم.
اجرای مرحلهبهمرحله: A هر دو تغییر را با هم میپذیرد؛ C با قیدهای درست قانون را حفظ میکند؛ I رقابت را طبق سطح انتخابی کنترل میکند؛ D تغییر تأییدشده را طبق تنظیم دوام نگه میدارد.
نتیجه و دلیل: نوشتن ACID روی محصول نمیگوید هر قانونِ برنامه خودکار اجرا شده؛ قید و سطح isolation و سیاست دوام باید مشخص باشند.
Härder and Reuter coined the acronym in 1983 to pin down fault-tolerance terms. But in practice one database's ACID differs from another's — isolation especially is ambiguous. Non-ACID systems are sometimes called BASE (Basically Available, Soft state, Eventual consistency), which is vaguer still; the only sane definition of BASE is “not ACID”!
| حرف | معنای واقعی | چه چیزی نیست |
| A — Atomicity |
اگر وسطِ چند نوشتن خطا رخ دهد، تراکنش abort میشود و همهٔ نوشتنهای تا آن لحظه برگردانده میشوند؛ یعنی «همه یا هیچ» و قابلیتِ retryِ امن |
ربطی به «اتمی بودنِ چندریتهای» ندارد (آن موضوعِ isolation است). شاید abortability نامِ بهتری بود |
| C — Consistency |
invariantهای خودِ برنامه همیشه درست باشند؛ مثلاً در حسابداری، مجموعِ بدهکار و بستانکار همیشه تراز باشد |
سازگاریِ replica، snapshot سازگار، consistent hashing، یا linearizability — اینها معانیِ دیگرِ همان واژهاند |
| I — Isolation |
تراکنشهای همزمان نباید پای هم را لگد کنند؛ تعریفِ کلاسیک یعنی serializability — نتیجه مثل اجرای سریالی باشد |
در عمل، بیشتر دیتابیسها سطوحِ ضعیفتری میدهند و بعضی آنومالیها را میپذیرند |
| D — Durability |
بعد از commit موفق، داده فراموش نمیشود؛ در تکnode یعنی نوشتن روی fsync و WAL، در توزیعشده یعنی کپی روی چند node |
ضمانتِ مطلق نیست؛ اگر همهٔ دیسکها و بکاپها با هم نابود شوند، کاری از دستِ دیتابیس برنمیآید |
| Letter | Real meaning | What it is not |
| A — Atomicity | If an error strikes mid-write, the transaction aborts and every write so far is rolled back; all-or-nothing with safe retry | Nothing to do with “thread-level atomicity” (that's isolation). abortability would have been a better name |
| C — Consistency | The application's own invariants always hold; e.g. in accounting, debits and credits always balance | Replica convergence, consistent snapshots, consistent hashing, or linearizability — other meanings of the same word |
| I — Isolation | Concurrent transactions must not trip each other; the classic definition is serializability — the outcome equals serial execution | In practice most databases give weaker levels and accept some anomalies |
| D — Durability | After a successful commit, data is never forgotten; single-node via fsync and WAL, distributed via copies on several nodes | Not absolute; if every disk and backup dies together, no database can help |
C در ACID یک جور فرق داردC in ACID is different
سه حرفِ دیگر را دیتابیس تضمین میکند؛ اما consistency به درستنویسیِ خودِ برنامه بستگی دارد.
اگر invariantها را بهصورتِ constraint در schema اعلام کنی (foreign key، unique، check) دیتابیس از آنها نگهداری میکند؛
اما invariantهای پیچیده (مثلاً «حداقل یک پزشک on-call بماند») را نمیتوان با constraintهای معمولی نوشت و تازه ممکن است
با trigger یا materialized view ساخته شوند. اگر invariant را اعلام نکنی، دیتابیس جلوی دادهٔ بد را نمیگیرد.
The database guarantees the other three letters; consistency depends on the application being written correctly. If you declare invariants as schema constraints (foreign keys, unique, check), the database enforces them; but complex invariants (e.g. “at least one doctor stays on call”) can't be written as ordinary constraints. If you don't declare an invariant, the database won't stop bad data.
روایت مدرس از دیاگرامTeacher narration
مثال فنی برای خواندنِ نمودار: چهار تضمین روی رزرو
وضعیت اولیه: برای صندلی S7 یک رزرو و یک تغییر وضعیت در همان پایگاه داده داریم.
اجرای مرحلهبهمرحله: A هر دو تغییر را با هم میپذیرد؛ C با قیدهای درست قانون را حفظ میکند؛ I رقابت را طبق سطح انتخابی کنترل میکند؛ D تغییر تأییدشده را طبق تنظیم دوام نگه میدارد.
نتیجه و دلیل: نوشتن ACID روی محصول نمیگوید هر قانونِ برنامه خودکار اجرا شده؛ قید و سطح isolation و سیاست دوام باید مشخص باشند.
هر کارت را از بالا بخوان: حرف بزرگ، نام، چهار خط «معنا» و دو خط «نیست» با یک برچسب نتیجه. کارت A میگوید atomicity یعنی «همهٔ نوشتنها یا با هم مینشینند یا همه برمیگردند تا retry امن شود» و ربطی به اتمی چندریتهای ندارد؛ کارت C میگوید consistency یعنی invariantهای خود برنامه که باید اعلام شوند تا دیتابیس نگهشان دارد؛ کارت I هشدار میدهد بیشتر دیتابیسها ضعیفتر از serializability میفروشند؛ و کارت D میگوید durability یعنی commit نقطهٔ بیبازگشت است ولی مطلق نیست.
نوار پایین جملهٔ مصاحبه است: ACID یک واژهٔ بازاریابی است — بپرس دقیقاً کدام سطح isolation روشن است و کدام آنومالیها هنوز ممکناند.
Read each card top to bottom: the big letter, the name, four “means” lines and two “is not” lines with a verdict tag. Card A says atomicity means “all writes land together or all roll back, so retry is safe” — nothing to do with thread-level atomicity; card C says consistency means the application's own invariants, which must be declared for the database to keep them; card I warns most databases sell weaker than serializability; card D says durability makes commit the point of no return, though not an absolute one.
The bottom bar is the interview line: ACID is a marketing word — ask exactly which isolation level is on and which anomalies are still possible.
۱.۳ تکشیئی در برابر چندشیئی + خطا و retry1.3 Single-object vs multi-object + errors and retry
اتمی بودن تغییرِ یک رکورد، الزاماً بهمعنی اتمی بودن تغییر چند رکورد نیست. پس از لغوِ قطعی ممکن است کل تراکنش را تکرار کنیم؛ پس از پاسخِ گمشده اول باید نتیجه را روشن کنیم.
مثال فنی: شمارندهٔ پیام خواندهنشده
وضعیت اولیه: پیام تازه برای کاربر ۱۷ باید درج شود و unread_count از ۲ به ۳ برسد.
اجرای مرحلهبهمرحله: اگر فقط درج پیام موفق شود، شمارنده هنوز ۲ است؛ تراکنش چندرکوردی هر دو اثر را یکجا commit یا rollback میکند.
نتیجه و دلیل: اتمی بودنِ هر ردیف بهتنهایی برای سازگاری رابطهٔ دو ردیف کافی نیست.
Atomicity and isolation assume you want to change several objects (rows, documents, records) at once. Classic example: an email app keeping the unread count in a separate denormalized field. Each new mail means both inserting the row and bumping the counter. If the two aren't seen together, the user sees an unread message with a zero counter — exactly what isolation prevents, and atomicity rolls both back if an error strikes mid-way.
تراکنشِ چندشیئی باید بداند کدام عملیاتها به یک تراکنش تعلق دارند؛ در دیتابیسهای رابطهای معمولاً بر اساسِ اتصالِ TCP
(بین BEGIN TRANSACTION و COMMIT) و در دیتابیسهای غیررابطهای اغلب اصلاً چنین چیزی نیست؛
حتی API چندکلیدی (مثل multi-put) لزوماً semantics تراکنشی ندارد و ممکن است بعضی کلیدها
موفق و بعضی ناموفق شوند.
A multi-object transaction must know which operations belong to it; in relational databases that's usually the TCP connection (between BEGIN TRANSACTION and COMMIT), while non-relational stores often have no such thing. Even a multi-key API (like multi-put) doesn't necessarily have transactional semantics — some keys may succeed while others fail.
در سطحِ تکشیء هم atomicity و isolation معنا دارند: اگر وسطِ نوشتنِ یک سندِ JSON بیستکیلوبایتی شبکه قطع شود،
نباید نیمهسندِ خراب ذخیره شود؛ اگر کلاینت دیگری همزمان بخواند، نباید مقدارِ نیمهبهروز ببیند. موتورهای ذخیرهسازی تقریباً
همیشه atomicity و isolation را در سطحِ یک شیء روی یک node میدهند (با log برای بازیابی و قفلِ شیء برای جداسازی).
عملیاتهایی مثل increment اتمی و conditional write (مقایسه-و-تنظیم، CAS) هم مفیدند — اما اینها «تراکنش»
به معنای معمولِ کلمه نیستند (مثلاً lightweight transactions در Cassandra فقط روی یک شیء ضمانت میدهند).
Atomicity and isolation also matter at the single-object level: if the network drops mid-write of a 20 KB JSON document, no corrupt half-document may be stored; a concurrent reader must not see a half-updated value. Storage engines almost always provide single-object, single-node atomicity and isolation (log for recovery, object lock for isolation). Operations like atomic increment and conditional write (compare-and-set, CAS) help too — but they aren't “transactions” in the usual sense (e.g. Cassandra's lightweight transactions only guarantee one object).
چه زمانی تراکنشِ چندشیئی لازم است؟When do you need a multi-object transaction?
اگر یک قانون به چند رکورد وابسته است، تغییرِ هر رکورد بهتنهایی ممکن است وضعیتِ ناقص بسازد.
مثال فنی: انتقال بین دو ظرفیت
وضعیت اولیه: سانس A ظرفیت ۱ و سانس B ظرفیت ۰ دارد؛ رزرو باید از B به A منتقل شود.
اجرای مرحلهبهمرحله: ظرفیت A یک واحد کم و B یک واحد زیاد میشود و رکورد رزرو عوض میشود؛ خطای میانی باید همه را برگرداند.
نتیجه و دلیل: این قانون به چند رکورد وابسته است؛ سه نوشتن مستقل ممکن است ظرفیت را گم یا اضافه کند.
- ارجاعهای بین جدولی: foreign key بین دو جدول، یا یالهای گراف — درجِ چند رکوردِ وابسته باید یکجا درست شود.
- denormalization: وقتی اطلاعاتِ تکراری در چند سند باید همزمان بهروز شوند (مثل شمارندهٔ ایمیل).
- ایندکسهای ثانویه: با هر تغییرِ مقدار، ایندکسها هم باید بهروز شوند؛ بدونِ isolation ممکن است رکورد در یک ایندکس باشد و در دیگری نه.
- Cross-table references: foreign keys between tables, or graph edges — inserting several dependent records must succeed as one.
- denormalization: when duplicated info in several documents must update together (like the email counter).
- Secondary indexes: every value change must update the indexes too; without isolation a record may sit in one index but not another.
retry کردنِ تراکنشِ abortشده — پنج دامRetrying an aborted transaction — five traps
بازتلاش باید خطای گذرا را با ورودیِ معتبر و از ابتدای تراکنش تکرار کند. خطای دائمی، فشارِ اضافه و اثرِ بیرونیِ تکراری با retry حل نمیشوند.
مثال فنی: Retry کل تصمیم با همان هویت
وضعیت اولیه: تراکنشِ ثبت درخواست r7 بهخاطر تعارض abort میشود.
اجرای مرحلهبهمرحله: از ابتدا دادهٔ تازه خوانده و کل تراکنش با همان شناسه تکرار میشود؛ backoff و سقف تلاش داریم و اثر بیرونی تکراری مهار میشود.
نتیجه و دلیل: تکرار فقط SQL آخر با تصمیمِ قبلی میتواند نادرست باشد؛ timeoutِ commit هم با abort قطعی برابر نیست.
- اگر تراکنش واقعاً commit شده ولی پاسخِ شبکه گم شده، retry یعنی اجرای دوباره — مگر deduplication در سطحِ برنامه داشته باشی.
- اگر خطا از overload یا contention باشد، retry بدترش میکند؛ تعدادِ retry محدود + exponential backoff.
- فقط خطاهای گذرا (deadlock، نقضِ isolation، قطعیِ موقت، failover) ارزشِ retry دارند؛ خطای دائمی (نقضِ constraint) را نباید retry کرد.
- اگر تراکنش اثرِ جانبیِ خارج از دیتابیس دارد (مثلاً ارسالِ ایمیل)، abort جلوی آن را نمیگیرد؛ برای آن یا 2PC لازم است یا الگویِ idempotent.
- اگر خودِ کلاینت وسطِ retry بمیرد، دادهای که میخواست بنویسد از بین میرود.
- If the transaction really committed but the network reply was lost, retry means running again — unless you have app-level deduplication.
- If the error comes from overload or contention, retry makes it worse; bounded retry count + exponential backoff.
- Only transient errors (deadlock, isolation violation, brief outage, failover) deserve retry; permanent errors (constraint violations) must not be retried.
- If the transaction has side effects outside the database (e.g. sending email), abort won't stop them; that needs 2PC or an idempotent pattern.
- If the client itself dies mid-retry, the data it meant to write is lost.
نکتهٔ عملی: بیشتر ORMها retry نمیکنندPractically: most ORMs do not retry
فریمورکهای محبوبِ ORM (مثل Rails ActiveRecord و Django) تراکنشِ abortشده را retry نمیکنند؛ خطا بهصورتِ exception
بالا میآید و ورودیِ کاربر دور ریخته میشود. این حیف است، چون کلِ فلسفهِ rollback همین است که retry امن شود!
Popular ORM frameworks (Rails ActiveRecord, Django) do not retry an aborted transaction; the error surfaces as an exception and user input is discarded. A pity — safe retry is the whole philosophy of rollback!
روایت مدرس از دیاگرامTeacher narration
مثال فنی برای خواندنِ نمودار: Retry کل تصمیم با همان هویت
وضعیت اولیه: تراکنشِ ثبت درخواست r7 بهخاطر تعارض abort میشود.
اجرای مرحلهبهمرحله: از ابتدا دادهٔ تازه خوانده و کل تراکنش با همان شناسه تکرار میشود؛ backoff و سقف تلاش داریم و اثر بیرونی تکراری مهار میشود.
نتیجه و دلیل: تکرار فقط SQL آخر با تصمیمِ قبلی میتواند نادرست باشد؛ timeoutِ commit هم با abort قطعی برابر نیست.
کارت سبز را بخوان: deadlock، تضاد isolation، قطعی کوتاه شبکه، failover و timeout قفل — هر پنجتا خودبهخود محو میشوند و تلاش دوباره معمولاً موفق است. کارت صورتی پنج دام است: reply گمشده ولی commit شده (بدون dedup دوباره اجرا میشود)، overload (retry چکش است)، خطای دائمی (باید درست شود نه retry)، اثر جانبی خارج از دیتابیس (ایمیل فرستاده شده؟) و مرگ خود کلاینت وسط retry.
نوار پایین قرارداد retry است: تعداد محدود، backoff نمایی با jitter، و فقط خطاهای گذرا. و یادداشت تلخ: بیشتر ORMها اصلاً retry نمیکنند.
Read the green card: deadlock, isolation conflict, brief network outage, failover and lock timeout — all five vanish on their own, and retrying usually succeeds. The pink card lists five traps: a lost reply with a won commit (re-executed without dedup), overload (retry is a hammer), permanent errors (fix, don't retry), side effects outside the database (email already sent?), and the client itself dying mid-retry.
The bottom bar is the retry contract: bounded count, exponential backoff with jitter, transient errors only. And the bitter note: most ORMs never retry at all.
۱.۴ ماشینِ وضعیتِ یک تراکنش + دیاگرام ۶1.4 The transaction state machine + Diagram 6
تراکنش از شروع به اجرا و سپس به تأیید یا لغو میرود. برنامه باید میان «لغو شد» و «از نتیجه خبر نداریم» فرق بگذارد.
مثال فنی: Commit نامعلوم از دید کلاینت
وضعیت اولیه: پایگاه داده commit سفارش ۴۲ را پایدار کرده؛ قبل از رسیدن پاسخ اتصال قطع شده است.
اجرای مرحلهبهمرحله: کلاینت با request_id=r7 نتیجه را استعلام یا درخواست را با همان شناسه تکرار میکند.
نتیجه و دلیل: تراکنش در سرور committed است، اما کلاینت فعلاً نتیجه را نمیداند؛ خطای اتصال دلیلِ امنی برای ساخت سفارش تازه نیست.
The classic “transaction states” model — the state diagram from database textbooks — is the best mental map for atomicity: every transaction starts Active and ends Committed or Aborted. The fateful moment sits exactly between “everything finished” and “not yet durable”: where the storage engine is making writes persistent on disk.
روایت مدرس از دیاگرام ۶Teacher narration of Diagram 6
مثال فنی برای خواندنِ نمودار: Commit نامعلوم از دید کلاینت
وضعیت اولیه: پایگاه داده commit سفارش ۴۲ را پایدار کرده؛ قبل از رسیدن پاسخ اتصال قطع شده است.
اجرای مرحلهبهمرحله: کلاینت با request_id=r7 نتیجه را استعلام یا درخواست را با همان شناسه تکرار میکند.
نتیجه و دلیل: تراکنش در سرور committed است، اما کلاینت فعلاً نتیجه را نمیداند؛ خطای اتصال دلیلِ امنی برای ساخت سفارش تازه نیست.
شکل چهار لین دارد: Transaction، Durability work، Undo and retry و Terminal. از Active (هنوز هیچچیز پایدار نیست) با فلش COMMIT به Partially Committed میروی (دستورها تمام شده ولی هنوز پایدار نیست)، بعد با make writes durable به Flushing to Disk (لحظهٔ سرنوشتساز: WAL و fsync) و با commit record written به Committed (غیرقابل بازگشت) میرسی.
مسیر خطا را دنبال کن: error or ROLLBACK به Failed و با undo the writes به Aborted میرسد؛ و اگر وسط flush کرش کنی، با crash mid-flush به In Doubt میرسی که log در ریاستارت تعیین تکلیف میکند (رکورد commit بود یعنی Committed، نبود یعنی Aborted). فلش application retries از Aborted به Active برمیگردد: abort یعنی «هیچچیز نشد؛ دوباره تلاش کن». درس شکل: ترتیب همهچیز است — اول داده، بعد رکورد commit؛ همین ماشین پایهٔ فهم 2PC است.
The figure has four lanes: Transaction, Durability work, Undo and retry and Terminal. From Active (nothing durable yet) the COMMIT arrow reaches Partially Committed (statements done, not yet durable), then make writes durable leads to Flushing to Disk (the fateful moment: WAL and fsync), and commit record written reaches Committed (no way back).
Now the error path: error or ROLLBACK goes to Failed, then undo the writes to Aborted; and a mid-flush crash takes crash mid-flush to In Doubt, which the log resolves on restart (commit record found means Committed, absent means Aborted). The application retries arrow loops from Aborted back to Active: abort means “nothing happened; try again”. The lesson: order is everything — data first, commit record second; this same machine underlies 2PC.
2 سطوحِ ضعیفِ isolationWeak isolation levels
سطح جداسازی مشخص میکند تراکنش همزمان چه تغییرهایی را میتواند ببیند. سطح ضعیفتر ممکن است سریعتر یا کمهزینهتر باشد، اما مسئولیت بعضی خطاهای همزمانی را به برنامه واگذار میکند.
مثال فنی: داخل تراکنش، هنوز رقابت هست
وضعیت اولیه: دو تراکنش هر دو موجودی ۱۰ را با SELECT میخوانند.
اجرای مرحلهبهمرحله: برنامهٔ هرکدام ۱ کم میکند و مقدار ثابت ۹ مینویسد؛ در سطحی که مانع این الگو نشود، هر دو commit میشوند.
نتیجه و دلیل: BEGIN/COMMIT بهتنهایی جلوی lost update را نمیگیرد؛ شکل update و isolation تعیینکنندهاند.
If two transactions touch disjoint data, or both are read-only, running them in parallel is safe. Trouble starts only when one reads what another concurrently changes, or both write the same thing. These race conditions rarely show up in tests (they depend on timing), which is why databases use isolation to hide concurrency from the programmer. But serializability costs, and most databases settle for weaker levels — harder to understand, breeding subtle bugs.
۲.۱ read committed2.1 read committed
در Read Committed، خواندن، تغییرِ تأییدنشدهٔ تراکنش دیگر را نمیبیند. با این حال دو خواندنِ پیاپی در یک تراکنش میتوانند نتیجهٔ متفاوتی داشته باشند.
مثال فنی: دو خواندن در Read Committed
وضعیت اولیه: T1 قیمت P7 را ۱۰۰ میخواند؛ T2 آن را ۱۲۰ میکند و commit میشود.
اجرای مرحلهبهمرحله: SELECT بعدی T1 در PostgreSQL Read Committed میتواند ۱۲۰ را ببیند؛ مقدارِ commitنشدهٔ T2 در query اول دیده نمیشود.
نتیجه و دلیل: جلوگیری از dirty read با ثابت ماندن نتیجهٔ چند query فرق دارد؛ snapshot این سطح برای هر statement تازه میشود.
The most basic isolation level (default in Oracle, PostgreSQL and SQL Server) gives two guarantees:
- بدونِ dirty read: فقط دادههای committed را میخوانی؛ نوشتنهای یک تراکنشِ در جریان تا لحظهٔ commit برای دیگران نامرئیاند (و بعد یکجا دیده میشوند).
- بدونِ dirty write: فقط دادههای committed را بازنویسی میکنی؛ اگر دو تراکنش روی یک ردیف بنویسند، دومی منتظرِ commit/abort اولی میماند.
- No dirty reads: you read only committed data; an in-flight transaction's writes stay invisible to others until commit (then appear all at once).
- No dirty writes: you overwrite only committed data; if two transactions write one row, the second waits for the first's commit/abort.
جلوی dirty write معمولاً با قفلِ سطحِ ردیف گرفته میشود: تا پایانِ تراکنش قفل نگه داشته میشود. برای dirty read
دو راه هست: گرفتنِ قفلِ خواندن (که در عمل بد است، چون تراکنشِ نوشتنِ کُند، خوانندهها را هم معطل میکند) یا روشِ رایجتر —
نگهداشتنِ دو نسخه از ردیف: مقدارِ committed قدیمی و مقدارِ جدیدِ تراکنشِ در جریان؛ تا commit نشده، خوانندهها همان قدیمی را میبینند.
Dirty writes are usually stopped with a row-level lock held to end of transaction. For dirty reads there are two options: a read lock (bad in practice — a slow writer stalls readers) or the common approach — keeping two versions of the row: the old committed value and the in-flight transaction's new value; until commit, readers see the old one.
اما read committed جلوی همهچیز را نمیگیرد: مثالِ شمارنده (دو increment همزمان) هنوز خراب میشود، چون نوشتنِ دوم
بعد از commit اولی رخ میدهد و dirty write نیست — پدیدهای به نامِ lost update که بعداً میبینیم.
سطحِ ضعیفترِ read uncommitted هم هست که حتی dirty read را میپذیرد (فقط dirty write را نه).
But read committed doesn't stop everything: the counter example (two concurrent increments) still breaks, because the second write happens after the first commits — not a dirty write. That phenomenon is lost update, covered later. A weaker level, read uncommitted, even accepts dirty reads (only dirty writes are barred).
۲.۲ snapshot isolation و MVCC + دیاگرام ۱2.2 snapshot isolation and MVCC + Diagram 1
در جداسازیِ مبتنی بر تصویر، خواندنها تصویری ثابت از داده میبینند. MVCC با نگه داشتن چند نسخه کمک میکند خواننده و نویسنده کمتر مزاحم یکدیگر شوند.
مثال فنی: گزارش ثابت با MVCC
وضعیت اولیه: پیش از انتقال، موجودی حساب A برابر ۱۰۰ و B برابر ۰ است؛ یک تراکنش ۲۰ واحد جابهجا میکند.
اجرای مرحلهبهمرحله: گزارش روی snapshot قبل از انتقال، A=100 و B=0 را میبیند؛ گزارش روی snapshot بعد، A=80 و B=20 را میبیند.
نتیجه و دلیل: snapshotِ درست نصفِ قبل و نصفِ بعد را مخلوط نمیکند؛ MVCC نسخههای لازم برای خوانندهٔ قدیمی را نگه میدارد.
read committed has a famous flaw: read skew. Imagine $1000 split across two $500 accounts, and a transaction moving $100 between them. Reading both balances mid-move, you might see one before the deposit ($500) and the other after the withdrawal ($400) — as if $100 evaporated! This read skew is a kind of nonrepeatable read, deemed “acceptable” under read committed.
برای بکاپگیری و کوئریهای تحلیلیِ طولانی اما tolerable نیست: بکاپِ چندساعته نباید ترکیبی از نسخههای قدیمی و جدید باشد.
راهحلِ رایج، snapshot isolation است: هر تراکنش از یک snapshot سازگارِ دیتابیس میخواند — یعنی هر آنچه در لحظهٔ
شروعِ تراکنش committed بوده — و تغییراتِ بعدیِ دیگران را نمیبیند. پیادهسازیِ استانداردِ آن
MVCC است: بهجای دو نسخه، چند نسخهٔ committed از هر ردیف نگه داشته میشود و قاعدهٔ visibility تعیین میکند
هر تراکنش کدام نسخه را ببیند. اصلِ طلایی: readers never block writers, writers never block readers.
For backups and long analytical queries that's intolerable: an hours-long backup must not mix old and new versions. The common fix is snapshot isolation: each transaction reads from a consistent snapshot — everything committed at its start — blind to others' later changes. The standard implementation is MVCC: instead of two versions, several committed versions of each row are kept, and visibility rules decide which version each transaction sees. Golden rule: readers never block writers, writers never block readers.
روایت مدرس از دیاگرام ۱Teacher narration of Diagram 1
مثال فنی برای خواندنِ نمودار: گزارش ثابت با MVCC
وضعیت اولیه: پیش از انتقال، موجودی حساب A برابر ۱۰۰ و B برابر ۰ است؛ یک تراکنش ۲۰ واحد جابهجا میکند.
اجرای مرحلهبهمرحله: گزارش روی snapshot قبل از انتقال، A=100 و B=0 را میبیند؛ گزارش روی snapshot بعد، A=80 و B=20 را میبیند.
نتیجه و دلیل: snapshotِ درست نصفِ قبل و نصفِ بعد را مخلوط نمیکند؛ MVCC نسخههای لازم برای خوانندهٔ قدیمی را نگه میدارد.
شکل دو ردیف چهارمرحلهای دارد (Write the Change، Keep Versions، Read a Snapshot، Clean Up)؛ ردیف بالا MVCC (مثل PostgreSQL) و ردیف پایین copy-on-write است. «بهروزرسانی» واقعاً Delete + Insert است: نسخهٔ قدیمی فقط علامت deleted_by = 13 میخورد و نسخهٔ جدید با inserted_by = 13 درج میشود، و همهٔ نسخهها در Version Chain (زنجیرهای در heap) زندگی میکنند.
خواننده با walk the version list در زنجیره قدم میزند تا نسخهٔ مرئی را در Txid Filter پیدا کند: تراکنش ۱۲ همان ۵۰۰ دلار را میبیند (tx 12 sees $500) هرچند ۱۳ آن را ۴۰۰ کرده؛ بقیه با keep only visible rows به Garbage Collection میروند. در ردیف پایین، Copy Changed Pages با copy path to the root به New Tree Root میرسد و Snapshot = Root یعنی هر ریشه خودش یک snapshot است و فیلتر txid لازم نیست؛ تهمانده با drop orphaned pages به Compaction میرود.
درس شکل: نوشتن هرگز بازنویسی نمیکند — یا نسخهٔ تازه میسازد یا صفحهٔ تازه — و «قدیمی بودن» فقط یک فیلتر دید است.
The figure has two four-stage rows (Write the Change, Keep Versions, Read a Snapshot, Clean Up); the top row is MVCC (like PostgreSQL), the bottom row copy-on-write. An “update” is really Delete + Insert: the old version is merely stamped deleted_by = 13 while the new version is inserted with inserted_by = 13, and all versions live in the Version Chain (a heap-resident chain).
The reader walks the version list to find the visible version in the Txid Filter: transaction 12 still sees the $500 (tx 12 sees $500) though 13 changed it to $400; the rest flow via keep only visible rows to Garbage Collection. On the bottom row, Copy Changed Pages reaches a New Tree Root via copy path to the root, and Snapshot = Root means each root is itself a snapshot — no txid filter needed; leftovers go via drop orphaned pages to Compaction.
The lesson: writes never overwrite — they either create a fresh version or a fresh page — and “old” is just a visibility filter.
قواعدِ visibility (خلاصهٔ چهار قاعده)Visibility rules (four rules in short)
- در شروعِ تراکنش، فهرستِ تراکنشهای در جریان ثبت میشود؛ نوشتنِ آنها نادیده گرفته میشود حتی اگر بعداً commit کنند.
- نوشتنِ تراکنشهایی با txid بزرگتر (یعنی بعد از ما شروع شدهاند) نادیده گرفته میشود، committed یا نه.
- نوشتنِ تراکنشهای abortشده همیشه نادیده گرفته میشود (پس لازم نیست بلافاصله پاک شوند؛ GC بعداً این کار را میکند).
- بقیهٔ نوشتنها مرئیاند.
- At transaction start, the list of in-flight transactions is recorded; their writes are ignored even if they commit later.
- Writes of transactions with larger txids (started after us) are ignored, committed or not.
- Writes of aborted transactions are always ignored (so they need no immediate cleanup; GC does it later).
- All other writes are visible.
شکلِ دیگرِ همان قاعده: یک ردیف مرئی است اگر (۱) در لحظهٔ شروعِ خواننده، تراکنشِ درجکننده قبلاً commit کرده باشد،
و (۲) برای حذف علامت نخورده باشد — یا اگر خورده، تراکنشِ حذفکننده هنوز commit نکرده باشد.
Same rule, other form: a row is visible if (1) at the reader's start, the inserting transaction had already committed, and (2) it isn't marked deleted — or if marked, the deleting transaction hasn't committed yet.
۲.۳ سردرگمیِ نامها: repeatable read2.3 Name confusion: repeatable read
| دیتابیس | نامی که استفاده میکند | چه چیزی واقعاً میدهد |
| PostgreSQL | repeatable read | snapshot isolation |
| Oracle | serializable | snapshot isolation (نه serializable واقعی!) |
| MySQL/InnoDB | repeatable read | MVCC با سازگاریِ ضعیفتر از snapshot isolation |
| IBM Db2 | repeatable read | serializability |
| Database | Name used | What it really gives |
| PostgreSQL | repeatable read | snapshot isolation |
| Oracle | serializable | snapshot isolation (not genuine serializability!) |
| MySQL/InnoDB | repeatable read | MVCC with weaker guarantees than snapshot isolation |
| IBM Db2 | repeatable read | serializability |
نامِ یک سطح جداسازی در همهٔ پایگاههای داده به یک رفتار دقیق اشاره نمیکند. باید تضمینِ همان محصول و نسخه را از مستنداتش خواند.
مثال فنی: نام یکسان، قرارداد را بخوان
وضعیت اولیه: T1 در PostgreSQL Repeatable Read یک query میزند؛ T2 ردیف تازهٔ مطابق شرط را درج و commit میکند.
اجرای مرحلهبهمرحله: تکرار query در snapshot T1 ردیف تازه را نشان نمیدهد، اما write skew میان دو تراکنش هنوز ممکن است.
نتیجه و دلیل: از اسم Repeatable Read بهتنهایی تضمین همهٔ موتورها را حدس نمیزنیم؛ جلوگیری از phantom لزوماً serializable بودن نیست.
The confusion is rooted in the SQL standard: it doesn't know snapshot isolation at all (written against 1975's System R) and its repeatable read definition is vague and implementation-dependent. Result: nobody really knows what “repeatable read” means! Always talk about behavior, never the name.
۲.۴ نمای کلاسِ MVCC + دیاگرام ۷2.4 The MVCC class view + Diagram 7
در MVCC، یک ردیف ممکن است چند نسخه داشته باشد. اطلاعاتِ ساخت و حذف نسخهها به موتور کمک میکند تعیین کند هر تراکنش کدام نسخه را ببیند.
مثال فنی: دو نسخهٔ یک ردیف
وضعیت اولیه: نسخهٔ قیمتِ ۱۰۰ برای خوانندهٔ قدیمی معتبر است؛ تراکنش تازه قیمت ۱۲۰ را commit کرده است.
اجرای مرحلهبهمرحله: reader قدیمی از snapshot خودش ۱۰۰ میخواند و reader تازه ۱۲۰؛ نسخهٔ قدیمی تا وقتی لازم است پاک نمیشود.
نتیجه و دلیل: MVCC لازم نیست نویسنده را منتظر پایان همهٔ خوانندهها بگذارد؛ snapshot طولانی میتواند پاکسازی نسخهها را عقب بیندازد.
Diagram 1 showed the data path; this one shows the “structure”: which classes exist in an MVCC engine, their fields, and how they connect — exactly what a UML class diagram shows. The right side is an “object diagram”: the same classes filled with the real accounts example.
روایت مدرس از دیاگرام ۷Teacher narration of Diagram 7
مثال فنی برای خواندنِ نمودار: دو نسخهٔ یک ردیف
وضعیت اولیه: نسخهٔ قیمتِ ۱۰۰ برای خوانندهٔ قدیمی معتبر است؛ تراکنش تازه قیمت ۱۲۰ را commit کرده است.
اجرای مرحلهبهمرحله: reader قدیمی از snapshot خودش ۱۰۰ میخواند و reader تازه ۱۲۰؛ نسخهٔ قدیمی تا وقتی لازم است پاک نمیشود.
نتیجه و دلیل: MVCC لازم نیست نویسنده را منتظر پایان همهٔ خوانندهها بگذارد؛ snapshot طولانی میتواند پاکسازی نسخهها را عقب بیندازد.
نیمهٔ چپ سه کلاس است: RowVersion با inserted_by / deleted_by و next_older / next_newer (زنجیرهٔ نسخهها)، Transaction با txid و snapshot (مجموعهٔ txidهای فعال)، و IndexEntry که فقط به یک نسخه اشاره میکند (points_to : one RowVersion). رابطهٔ «یک به چند» با نام writes / marks آنها را وصل میکند و فلش سبز finds the visible version by walking the chain کار ایندکس را میگوید.
نیمهٔ راست نمودار شیءِ «نسخههای حساب ۲» است: v3 با balance = 400 و برچسب invisible to tx 12، و v2 با balance = 500 و deleted_by = 13 با برچسب visible to tx 12 (حذف فقط یعنی علامت خوردن توسط ۱۳). جعبهٔ زرد همان چهار قاعدهٔ visibility است. درس: MVCC یک مدل دادهٔ مشخص است — اگر این سه کلاس را از حفظ توضیح بدهی، هر «MVCC چطور کار میکند؟» را جواب دادهای.
The left half shows three classes: RowVersion with inserted_by / deleted_by and next_older / next_newer (the version chain), Transaction with txid and snapshot (the set of active txids), and IndexEntry, which points at just one version (points_to : one RowVersion). A one-to-many relation named writes / marks connects them, and the green arrow finds the visible version by walking the chain states the index's job.
The right half is the object diagram of “account 2's versions”: v3 with balance = 400 tagged invisible to tx 12, and v2 with balance = 500 and deleted_by = 13 tagged visible to tx 12 (deleted merely means stamped by 13). The yellow box restates the four visibility rules. Lesson: MVCC is a concrete data model — explain these three classes from memory and you've answered any “how does MVCC work?”.
3 lost update، write skew و phantomLost update, write skew and phantoms
۳.۱ گمشدنِ بهروزرسانی و چهار راهکار3.1 Lost updates and four remedies
گمشدن بهروزرسانی وقتی است که دو عملیات از یک مقدار قدیمی شروع کنند و یکی نتیجهٔ دیگری را بپوشاند. افزایش اتمی، قفل، بررسی نسخه یا جداسازی مناسب میتواند جلوی آن را بگیرد.
مثال فنی: دو کاهش، فقط یکی ثبت میشود
وضعیت اولیه: موجودی ۱۰ است؛ T1 و T2 هر دو ۱۰ را میخوانند و در برنامه ۹ حساب میکنند.
اجرای مرحلهبهمرحله: T1 مقدار ۹ را مینویسد و سپس T2 هم ۹ مینویسد؛ نتیجه ۹ است، ولی بعد از دو فروش باید ۸ باشد.
ترتیبِ خطای lost update؛ دو فروش از موجودی ۱۰| گام | T1 | T2 | موجودیِ ثبتشده |
|---|
| ۱ | ۱۰ را میخواند | ۱۰ را میخواند | ۱۰ |
| ۲ | ۹ مینویسد و commit میکند | هنوز بر اساسِ ۱۰ تصمیم دارد | ۹ |
| ۳ | تمام شده | ۹ مینویسد و commit میکند | ۹؛ باید ۸ میشد |
برای همین کاهشِ ساده در PostgreSQL، هر درخواست میتواند این دستور را اجرا کند و موفقیت را از ردیفِ برگشتی بفهمد:
UPDATE stock
SET qty = qty - 1
WHERE product_id = 7 AND qty > 0
RETURNING qty;
در Read Committed، دو تغییرِ رقیب روی همین ردیف هماهنگ میشوند؛ اگر موجودی ابتدا ۱۰ باشد، دو کاهشِ موفق آن را به ۸ میرسانند. اگر هیچ ردیفی برنگردد، برنامه نباید فروش را موفق اعلام کند. ثبت سفارش و کاهش موجودی، در صورت نیاز، در همان تراکنش قرار میگیرند.
نتیجه و دلیل: SET qty=qty-1 با شرط مناسب، قفلِ read-modify-write یا CAS راههای کنترلاند؛ نوشتن مقدار ثابتِ محاسبهشده از خواندن قدیمی علت خطاست.
lost update is the read-modify-write cycle: read a value, change it, write it back. Two concurrent transactions doing this lose one change (the second write covers the first). Examples: bumping a counter, updating an account balance, appending to a list inside a JSON document, or two users saving one wiki page at once.
| راهکار | ایده | کی خوب است / کی نه |
| عملیاتِ اتمیِ built-in |
UPDATE counters SET value = value + 1 WHERE key = 'foo' — بدونِ چرخهٔ read-modify-write |
بهترین گزینه اگر منطق با این عملیات بیان شود؛ مراقبِ ORM باشید که ناخواسته read-modify-write مینویسد! |
| قفلِ صریح |
SELECT ... FOR UPDATE — قفل روی ردیفهای برگشتی، سپس تغییر |
وقتی منطقِ برنامه (مثلاً قوانینِ بازی) باید وسطش اجرا شود؛ خطرِ deadlock و retry |
| تشخیصِ خودکار + retry |
هر دو اجرا میشوند؛ اگر مدیرِ تراکنش lost update ببیند، بازنده را abort میکند |
PostgreSQL (RR)، Oracle (serializable)، SQL Server (snapshot) این کار را میکنند؛ MySQL/InnoDB نه! |
| conditional write / CAS |
نوشتن فقط اگر مقدار از زمانِ خواندن تغییر نکرده باشد؛ یا ستونِ version (optimistic locking) |
برای سیستمهای بدونِ تراکنش (مثل فروشگاههای key-value)؛ نسخهای از MVCC برای WHERE استثنا قائل میشود |
| Remedy | Idea | When good / when not |
| Built-in atomic op | UPDATE counters SET value = value + 1 WHERE key = 'foo' — no read-modify-write cycle | Best option when the logic fits these ops; beware ORMs that silently write read-modify-write! |
| Explicit lock | SELECT ... FOR UPDATE — lock the returned rows, then change | When app logic (e.g. game rules) must run in between; deadlock and retry risk |
| Auto-detect + retry | Both run; if the manager spots a lost update, it aborts the loser | PostgreSQL (RR), Oracle (serializable), SQL Server (snapshot) do this; MySQL/InnoDB doesn't! |
| conditional write / CAS | Write only if the value hasn't changed since read; or a version column (optimistic locking) | For transaction-less systems (like key-value stores); one MVCC variant exempts WHERE |
دامِ replicationThe replication trap
قفل و CAS فرض میکنند یک نسخهٔ بهروز از داده وجود دارد. در دیتابیسهای multi-leader یا leaderless چند نوشتنِ همزمان
داریم و نسخههای متضاد (siblings) ساخته میشود؛ آنجا باید بعداً ادغام کرد (مثل CRDTها برای عملیاتِ جابجاییپذیر).
روشِ پیشفرضِ LWW در بسیاری از این دیتابیسها خودش مستعدِ lost update است.
Locks and CAS assume one up-to-date copy of the data. In multi-leader or leaderless databases, concurrent writes create conflicting versions (siblings) that must later merge (e.g. CRDTs for commutative operations). The default LWW in many such databases is itself lost-update-prone.
۳.۲ write skew + دیاگرام ۲3.2 write skew + Diagram 2
در write skew، تراکنشها ردیفهای متفاوت را تغییر میدهند، اما تصمیمشان بر یک شرط مشترک استوار است. بنابراین حتی بدون بازنویسیِ یک ردیف، قانون کلی میتواند شکسته شود.
مثال فنی: دو پزشک، دو ردیف، یک قانون
وضعیت اولیه: A.on_call=true و B.on_call=true؛ قانون: تعداد پزشکان کشیک حداقل ۱ باشد.
اجرای مرحلهبهمرحله: T1 و T2 هر دو count=2 میخوانند؛ T1 فقط A و T2 فقط B را false میکنند. در snapshot isolation میتوانند بدون تعارضِ نوشتن روی یک ردیف commit شوند.
ترتیبِ write skew در snapshot isolation| گام | T1 برای پزشک A | T2 برای پزشک B |
|---|
| ۱. خواندن | دو پزشک حاضرند: count=2 | دو پزشک حاضرند: count=2 |
| ۲. تصمیم | «B میماند، پس من میروم» | «A میماند، پس من میروم» |
| ۳. نوشتن | فقط ردیف A را false میکند | فقط ردیف B را false میکند |
| ۴. نتیجه | نوشتنها روی دو ردیف جدا هستند؛ هر دو میتوانند commit شوند و count نهایی صفر شود. |
در این الگو هیچکس مقدارِ نوشتهشدهٔ دیگری را بازنویسی نکرده است؛ تصمیم هرکدام به خواندنِ ردیفی وابسته بود که دیگری تغییرش داد. در PostgreSQL Serializable، این رقابت باید با مدیریتِ خطای serialization و تکرارِ کل تصمیم پوشش داده شود.
نتیجه و دلیل: count نهایی صفر است؛ این write skew است. Serializable با retry یا قفلِ منبع مشترکِ قانون میتواند از پذیرش این نتیجه جلوگیری کند.
Suppose you're writing hospital on-call scheduling: the rule is at least one doctor stays on call, but any doctor may take leave if a colleague covers. Aaliyah and Bryce both fall ill and press “I'm off call” nearly simultaneously. Each transaction first checks (2 doctors on call — fine) then updates its own record. Both commit — and now nobody is on call!
این آنومالی را write skew مینامند: نه dirty write است و نه lost update، چون دو تراکنش دو شیءِ متفاوت را تغییر میدهند.
بهترین تعریف: تعمیمِ lost update — تصمیمی که بر اساسِ دادهای قدیمی گرفته شده و تا لحظهٔ نوشتن، پیشفرضش دیگر درست نیست.
This anomaly is write skew: neither dirty write nor lost update, since the two transactions change two different objects. Best definition: a generalized lost update — a decision made on stale data whose premise is no longer true by write time.
روایت مدرس از دیاگرام ۲Teacher narration of Diagram 2
مثال فنی برای خواندنِ نمودار: دو پزشک، دو ردیف، یک قانون
وضعیت اولیه: A.on_call=true و B.on_call=true؛ قانون: تعداد پزشکان کشیک حداقل ۱ باشد.
اجرای مرحلهبهمرحله: T1 و T2 هر دو count=2 میخوانند؛ T1 فقط A و T2 فقط B را false میکنند. در snapshot isolation میتوانند بدون تعارضِ نوشتن روی یک ردیف commit شوند.
نتیجه و دلیل: count نهایی صفر است؛ این write skew است. Serializable با retry یا قفلِ منبع مشترکِ قانون میتواند از پذیرش این نتیجه جلوگیری کند.
چهار ستون (Tx: Aaliyah، Tx: Bryce، Database با snapshot isolation، و Invariant) و سه نوار: Both read the premise، Both act on the premise و Both commit — the invariant breaks. در بخش اول هر دو SELECT doctors on call میزنند و «۲ پزشک» میبینند — هر کدام از snapshot خودش.
در بخش دوم هر کدام فقط ردیف خودش را مینویسد (UPDATE Aaliyah/Bryce set on_call = false)؛ پس نه dirty write رخ میدهد و نه قفلی سر راه است. در بخش سوم هر دو COMMIT میزنند (locks only Aaliyah's row و no conflict detected here) و فلش چینچین 0 doctors on call به Invariant میرسد. درس: write skew در لایهٔ منطق تصمیم رخ میدهد نه مقدار یک ردیف — برای همین نه قفل شیء جلویش را میگیرد نه تشخیص lost update؛ فقط serializability.
Four columns (Tx: Aaliyah, Tx: Bryce, a snapshot-isolated Database, and the Invariant) and three bands: Both read the premise, Both act on the premise and Both commit — the invariant breaks. In the first band both issue SELECT doctors on call and see “2 doctors” — each from its own snapshot.
In the second band each writes only its own row (UPDATE Aaliyah/Bryce set on_call = false); so no dirty write happens and no lock stands in the way. In the third band both COMMIT (locks only Aaliyah's row, no conflict detected here) and the dashed 0 doctors on call arrow reaches the Invariant. Lesson: write skew lives in the decision-logic layer, not in one row's value — which is why neither object locks nor lost-update detection stop it; only serializability does.
۳.۳ phantom و materializing conflicts + دیاگرام ۳3.3 phantoms and materializing conflicts + Diagram 3
خطای مربوط به رکوردهای جدید زمانی رخ میدهد که تصمیم بر «نبودنِ رکورد مطابق شرط» بنا شود. قفل کردن یک ردیفِ موجود، لزوماً جلوی درجِ ردیف تازه را نمیگیرد.
مثال فنی: رزروِ بازهای که هنوز ردیف ندارد
وضعیت اولیه: برای اتاق ۷ و بازهٔ [10:00,11:00) هیچ رزروی نیست.
اجرای مرحلهبهمرحله: T1 و T2 هر دو queryِ همپوشانی را خالی میبینند و هرکدام ردیف تازه درج میکنند؛ FOR UPDATE روی نتیجهٔ خالی ردیفی برای قفل ندارد.
برای دو بازهٔ نیمهباز، شرطِ همپوشانی چنین است:
SELECT id FROM bookings
WHERE room_id = 7
AND start_at < :requested_end
AND end_at > :requested_start
FOR UPDATE;
اگر نتیجه خالی باشد، این دستور هیچ ردیف رزروی را قفل نمیکند. یک راهِ مشخص در Read Committed این است که همهٔ مسیرهای رزرو، پیش از این query ردیفِ موجودِ اتاق ۷ در جدولِ اتاقها را قفل کنند و تا درج و commit نگه دارند؛ یا قیدِ معتبرِ جلوگیری از overlap و روش مناسبِ isolation طراحی شود.
نتیجه و دلیل: قید جلوگیری از overlap، Serializable یا قفلِ ردیف مشترک اتاق لازم است؛ unique سادهٔ ساعت شروع برای همهٔ همپوشانیها کافی نیست.
Almost every write-skew example follows a three-step pattern:
- یک
SELECT با شرطِ جستوجو چک میکند آیا الزام برقرار است (حداقل دو پزشک on-call؟ رزروی روی این اتاق در این بازه نیست؟ این خانهٔ صفحهٔ شطرنج خالی است؟ این نامکاربری آزاد است؟ موجودی کافی است؟).
- برنامه بر اساسِ نتیجه تصمیم میگیرد (ادامه یا خطا).
- اگر ادامه دهد، مینویسد (INSERT/UPDATE/DELETE) و commit میکند.
- A
SELECT with a search condition checks whether the requirement holds (at least two doctors on call? no booking overlapping this room in this slot? this chess square empty? this username free? balance sufficient?).
- The application decides from the result (proceed or error).
- If proceeding, it writes (INSERT/UPDATE/DELETE) and commits.
اما همین نوشتن، پیشفرضِ مرحلهٔ ۲ را باطل میکند: اگر همان SELECT را دوباره بزنی، جوابِ دیگری میگیری.
اثری که نوشتنِ یک تراکنش روی نتیجهٔ جستوجوی تراکنشِ دیگر میگذارد را phantom مینامند.
snapshot isolation جلوی phantom در کوئریهای فقط-خواندنی را میگیرد، اما در تراکنشِ نوشتنی نه.
But that very write invalidates step 2's premise: re-running the same SELECT gives a different answer. The effect one transaction's write has on another's search result is called a phantom. Snapshot isolation stops phantoms in read-only queries — but not in writing transactions.
نکتهٔ ظریف: در مثالِ پزشکان، ردیفی که تغییر میکند همان ردیفی است که SELECT برگردانده — پس
SELECT ... FOR UPDATE میتواند قفل بگیرد. اما در بقیهٔ مثالها، چک «نبودِ ردیف» را بررسی میکند و نوشتن
ردیفِ جدیدی میسازد که آن شرط را میشکند؛ وقتی SELECT هیچ ردیفی برنمیگرداند، FOR UPDATE چیزی ندارد که قفل کند!
Subtle point: in the doctors example the changed row is one the SELECT returned — so SELECT ... FOR UPDATE could lock it. But in the other examples the check tests for the absence of a row, and the write creates a new row breaking the condition; when SELECT returns nothing, FOR UPDATE has nothing to lock!
روایت مدرس از دیاگرام ۳Teacher narration of Diagram 3
مثال فنی برای خواندنِ نمودار: رزروِ بازهای که هنوز ردیف ندارد
وضعیت اولیه: برای اتاق ۷ و بازهٔ [10:00,11:00) هیچ رزروی نیست.
اجرای مرحلهبهمرحله: T1 و T2 هر دو queryِ همپوشانی را خالی میبینند و هرکدام ردیف تازه درج میکنند؛ FOR UPDATE روی نتیجهٔ خالی ردیفی برای قفل ندارد.
نتیجه و دلیل: قید جلوگیری از overlap، Serializable یا قفلِ ردیف مشترک اتاق لازم است؛ unique سادهٔ ساعت شروع برای همهٔ همپوشانیها کافی نیست.
ردیف بالا Decision و سه ردیف پایین سه نگهباناند (Atomic Operations، Explicit Locking، Detect and Retry). از Race Condition Found شروع کن: سؤال اول Expressible as Atomic Op? — اگر بله، Atomic Write or CAS (بهترین گزینه)؛ اگر نه، سؤال دوم Rows Known in Advance? — اگر بله، Explicit Locking با SELECT ... FOR UPDATE (مراقب deadlock).
اگر ردیفی برای قفل کردن نیست (phantom)، سؤال سوم: Conflict Auto-Detected? — اگر دیتابیس خودش abort میکند، Detect and Retry؛ وگرنه آخر خط است: Serializable Isolation با برچسب materialize conflicts last. درس: از ارزانترین نگهبان شروع کن و بالا برو؛ کارت صورتی هشدار میدهد materializing conflicts فقط راه آخر است.
The top row is the Decision path; the three rows below are three guards (Atomic Operations, Explicit Locking, Detect and Retry). Start at Race Condition Found: first question Expressible as Atomic Op? — yes means Atomic Write or CAS (best option); no leads to the second question Rows Known in Advance? — yes means Explicit Locking with SELECT ... FOR UPDATE (watch for deadlocks).
If there is no row to lock (a phantom), third question: Conflict Auto-Detected? — if the database aborts the loser itself, Detect and Retry; otherwise it's the end of the line: Serializable Isolation, tagged materialize conflicts last. Lesson: start from the cheapest guard and climb; the pink card warns that materializing conflicts is only the last resort.
materializing conflicts — آخرین راهحلmaterializing conflicts — the last resort
اگر مشکلِ phantom این است که شیئی برای قفل کردن نیست، شاید بتوانیم شیءِ قفل را بسازیم: مثلاً جدولی از
«اتاق × بازهٔ ۱۵ دقیقهای» که از قبل (مثلاً برای شش ماه) پر شده است. تراکنشِ رزرو اول ردیفِ مربوطه را
SELECT ... FOR UPDATE میکند، بعد رزرو را ثبت میکند. آن جدول برای نگهداریِ اطلاعاتِ رزرو نیست —
صرفاً مجموعهای از قفلهاست. روش کار میکند اما پیدا کردنش سخت و خطاست و مکانیزمِ کنترلِ همزمانی را به مدلِ داده نشت میدهد.
If the phantom problem is “no object to lock”, perhaps we can build the lock object: e.g. a table of “room × 15-minute slot” rows pre-filled for six months. The booking transaction first SELECT ... FOR UPDATEs the relevant row, then records the booking. That table holds no booking info — it's purely a set of locks. It works, but finding the right design is hard and error-prone, leaking concurrency control into the data model.
ماتریسِ آنومالیها × سطوحِ isolation (جدولِ ۸-۱ کتاب)Anomaly-by-isolation matrix (Table 8-1)
جمعبندیِ همهٔ آنومالیهایی که تا اینجا دیدیم در یک جدول — همین ماتریس، پُرکاربردترین جدولِ فصل در مصاحبههاست:
All anomalies seen so far, summarized in one table — the most interview-used table of the chapter:
| سطحِ isolation | Dirty reads | Read skew | Phantom reads | Lost updates | Write skew |
| Read uncommitted |
✗ ممکن | ✗ ممکن | ✗ ممکن | ✗ ممکن | ✗ ممکن |
| Read committed |
✓ جلوگیری | ✗ ممکن | ✗ ممکن | ✗ ممکن | ✗ ممکن |
| Snapshot isolation |
✓ جلوگیری | ✓ جلوگیری | ✓ جلوگیری | ? بستگی دارد | ✗ ممکن |
| Serializable |
✓ جلوگیری | ✓ جلوگیری | ✓ جلوگیری | ✓ جلوگیری | ✓ جلوگیری |
| Isolation level | Dirty reads | Read skew | Phantom reads | Lost updates | Write skew |
| Read uncommitted | ✗ possible | ✗ possible | ✗ possible | ✗ possible | ✗ possible |
| Read committed | ✓ prevented | ✗ possible | ✗ possible | ✗ possible | ✗ possible |
| Snapshot isolation | ✓ prevented | ✓ prevented | ✓ prevented | ? depends | ✗ possible |
| Serializable | ✓ prevented | ✓ prevented | ✓ prevented | ✓ prevented | ✓ prevented |
نکتههای خواندنِ جدول: dirty write جایی در جدول ندارد چون تقریباً همهٔ پیادهسازیها (با قفلِ ردیف) جلویش را میگیرند؛
ستونِ lost updates علامتِ «?» دارد چون بعضی پیادهسازیهای snapshot isolation (مثل PostgreSQL) خودشان تشخیص میدهند
و بعضی (مثل MySQL/InnoDB) نه؛ و write skew فقط با serializability واقعی بسته میشود.
Reading notes: dirty writes aren't in the table because nearly every implementation (with row locks) prevents them; the lost-updates column carries “?” because some snapshot-isolation implementations (PostgreSQL) detect them and others (MySQL/InnoDB) don't; and write skew is closed only by genuine serializability.
تعریفِ یکخطیِ هر آنومالی (جمعبندیِ پایانِ فصل)One-line definitions (end-of-chapter recap)
- Dirty reads: کلاینتِ دیگر، نوشتنِ هنوز-commit-نشده را میخواند — read committed و بالاتر جلویش را میگیرند.
- Dirty writes: کلاینتِ دیگر، نوشتنِ هنوز-commit-نشده را بازنویسی میکند — تقریباً همه جلویش را میگیرند.
- Read skew: کلاینت بخشهایی از دیتابیس را در لحظههای متفاوت میبیند (نوعی nonrepeatable read) — درمان: snapshot isolation با MVCC.
- Phantom reads: نوشتنِ دیگری نتیجهٔ جستوجوی یک تراکنش را عوض میکند — snapshot isolation جلوی نسخهٔ فقط-خواندنیاش را میگیرد؛ phantom در write skew به index-range lock و اینها نیاز دارد.
- Lost updates: دو کلاینت همزمان read-modify-write میکنند و یکی تغییرِ دیگری را میپوشاند — بعضی snapshot isolationها خودشان جلویش را میگیرند، بعضی
SELECT FOR UPDATE میخواهند.
- Write skew: تراکنش بر اساسِ چیزی که خوانده تصمیم میگیرد و مینویسد، اما پیشفرضِ تصمیم تا لحظهٔ نوشتن باطل شده — فقط serializability جلویش را میگیرد.
- Dirty reads: another client reads a not-yet-committed write — read committed and above stop them.
- Dirty writes: another client overwrites a not-yet-committed write — nearly everyone stops them.
- Read skew: a client sees parts of the database at different moments (a nonrepeatable read) — cure: snapshot isolation with MVCC.
- Phantom reads: another's write changes a transaction's search result — snapshot isolation stops its read-only version; phantoms in write skew need index-range locks and the like.
- Lost updates: two clients read-modify-write concurrently and one covers the other's change — some snapshot isolations stop it themselves, others need
SELECT FOR UPDATE.
- Write skew: a transaction decides on what it read and writes, but the decision's premise is void by write time — only serializability stops it.
4 serializability و سه روشِ پیادهسازیSerializability and three implementations
سریالپذیری یعنی نتیجهٔ تراکنشهای موفق معادلِ یک اجرای پشتسرهم باشد، حتی اگر در عمل همزمان اجرا شده باشند. این تضمین دربارهٔ ترتیب منطقی است.
مثال فنی: نتیجهٔ معادل اجرای یکییکی
وضعیت اولیه: موجودی یک واحد است؛ دو تراکنش هرکدام «اگر موجودی مثبت بود، یک سفارش بساز» را اجرا میکنند.
اجرای مرحلهبهمرحله: در اجرای serial، اولی موجودی را صفر میکند و دومی دیگر سفارش نمیسازد؛ نتیجهٔ concurrentِ serializable باید با یک ترتیب serial سازگار باشد.
نتیجه و دلیل: دو سفارش موفق با موجودی منفی معادل این منطقِ serial نیست؛ ممکن است لازم شود یکی abort و از ابتدا retry شود.
The unpleasant state of affairs: isolation levels are hard to grasp and inconsistent, telling whether application code is safe at a level is difficult, and no good tool finds race conditions (testing them is non-deterministic). Researchers' answer since the 1970s is simple: serializable isolation — guaranteeing the outcome equals a serial, concurrency-free execution, stopping every race. So why doesn't everyone use it? Cost. Three main techniques exist today.
۴.۱ دیاگرام ۴ + روایت مدرس4.1 Diagram 4 + teacher narration
روایت مدرس از دیاگرام ۴Teacher narration of Diagram 4
مثال فنی برای خواندنِ نمودار: نتیجهٔ معادل اجرای یکییکی
وضعیت اولیه: موجودی یک واحد است؛ دو تراکنش هرکدام «اگر موجودی مثبت بود، یک سفارش بساز» را اجرا میکنند.
اجرای مرحلهبهمرحله: در اجرای serial، اولی موجودی را صفر میکند و دومی دیگر سفارش نمیسازد؛ نتیجهٔ concurrentِ serializable باید با یک ترتیب serial سازگار باشد.
نتیجه و دلیل: دو سفارش موفق با موجودی منفی معادل این منطقِ serial نیست؛ ممکن است لازم شود یکی abort و از ابتدا retry شود.
از OLTP Workload (تراکنشهای کوتاه و سریع) سه فلش به سه جعبه میرود: run one at a time به Serial Execution (مثل VoltDB و Redis)، lock before access به Two-Phase Locking (مثل MySQL و SQL Server)، و proceed, check at commit به Serializable Snapshot (مثل PostgreSQL و CockroachDB) — هر سه داخل یک قاب: «هر سه serializability کامل میدهند و فقط در هزینه فرق دارند».
ستون راست بهای هر روش است: Single-Core Ceiling (یک تراکنش کند همه را میبندد)، Waits and Deadlocks (latency ناپایدار زیر بار)، و Aborts Under Contention (retryهای خوشبینانه). درس: هر سه جاده به یک مقصد میرسند؛ انتخاب، انتخاب نوع درد است.
From the OLTP Workload (short, fast transactions) three arrows run to three boxes: run one at a time to Serial Execution (e.g. VoltDB, Redis), lock before access to Two-Phase Locking (e.g. MySQL, SQL Server), and proceed, check at commit to Serializable Snapshot (e.g. PostgreSQL, CockroachDB) — all three inside one frame: “all three give full serializability; they differ only in cost”.
The right column is each method's price: Single-Core Ceiling (one slow transaction stalls all), Waits and Deadlocks (unstable latency under load), and Aborts Under Contention (optimistic retries). Lesson: all three roads reach one destination; the choice is which pain to pick.
۴.۲ اجرای سریالی و stored procedure4.2 Serial execution and stored procedures
اجرای سریالی، تراکنشها را یکییکی اجرا میکند. کوتاه بودن عملیات و نزدیک بودن منطق به داده میتواند انتظار را کمتر کند؛ کارِ طولانی یا وابسته به شبکه این روش را کند میکند.
مثال فنی: کار کوتاه روی مسیر سریالی
وضعیت اولیه: procedure رزرو، موجودی را بررسی، کم و سفارش را ثبت میکند.
اجرای مرحلهبهمرحله: اجرای آن مثلاً ۲ میلیثانیه است؛ منتظر ماندن ۳۰ثانیهای برای ورودی کاربر داخل همان مسیر، نوبت بقیه را متوقف میکند.
نتیجه و دلیل: اجرای سریالی به تراکنشهای کوتاه و دادهٔ در دسترس نیاز دارد؛ فرم یا RPC کند نباید حلقهٔ اصلی را معطل کند.
The simplest cure for concurrency is removing it: one transaction at a time, on one thread. Then no conflicts exist to detect, and isolation is serializable by definition. The idea wasn't practical until the 2000s; two shifts enabled it: cheap RAM (the whole active dataset fits in memory) and short OLTP transactions with few reads/writes. Long analytical queries are read-only anyway and can run outside the serial loop on a snapshot.
اما تراکنشِ تعاملی (یک جمله SQL، خواندنِ جواب، جملهٔ بعدی...) بیشترِ وقتش را در ارتباطِ شبکه با برنامه میگذراند؛
اگر فقط یک تراکنش در هر لحظه داشته باشی، throughput افتضاح میشود. پس این سیستمها تراکنشِ تعاملیِ چندجملهای را
قبول نمیکنند: یا هر تراکنش یک جمله است، یا کلِ تراکنش از قبل بهصورتِ stored procedure به دیتابیس تحویل میشود.
با دادهی در حافظه و بدونِ انتظارِ I/O، اجرای سریالی روی یک thread میتواند throughput بسیار خوبی داشته باشد.
But an interactive transaction (one SQL statement, read the answer, next statement…) spends most time on network round-trips with the application; with one transaction at a time, throughput collapses. So these systems reject multi-statement interactive transactions: each transaction is either a single statement or delivered whole as a stored procedure. With in-memory data and no I/O waits, single-threaded serial execution can sustain excellent throughput.
stored procedureها سابقهٔ بدی دارند: زبانِ اختصاصیِ هر vendor (PL/SQL، T-SQL، PL/pgSQL) زشت و قدیمی شده،
مدیریتشان (دیباگ، version control، تست، مانیتورینگ) سخت است، و یک procedure بد میتواند کلِ دیتابیسِ مشترک را زمین بزند.
اما نسخههای مدرن از زبانهای عمومی استفاده میکنند: VoltDB با Java/Groovy، Datomic با Java/Clojure، Redis با Lua، MongoDB با JavaScript.
ضمناً VoltDB برای replication بهجای کپیِ نوشتنها، همان procedure را روی همهٔ replicaها اجرا میکند (state machine replication)
و بنابراین procedure باید deterministic باشد.
Stored procedures have a bad reputation: each vendor's proprietary language (PL/SQL, T-SQL, PL/pgSQL) grew ugly, and managing them (debugging, version control, testing, monitoring) is hard — one bad procedure can floor a shared database. But modern versions use general-purpose languages: VoltDB with Java/Groovy, Datomic with Java/Clojure, Redis with Lua, MongoDB with JavaScript. VoltDB even replicates by running the same procedure on every replica (state machine replication), so procedures must be deterministic.
برای مقیاسپذیری هم میتوان داده را shard کرد (فصل ۷): اگر هر تراکنش فقط در یک shard بخواند و بنویسد، هر shard threadِ
خودش را دارد و throughput با تعدادِ هسته خطی رشد میکند. تراکنشِ چندshardی اما هماهنگیِ lockstep میخواهد و بسیار کُند است
(VoltDB حدود ۱۰۰۰ نوشتنِ چندshardی در ثانیه — چند مرتبه کمتر از تکshardی).
جمعبندی: تراکنشها باید کوچک و سریع باشند، دادهٔ فعال در حافظه جا شود، و throughput نوشتن در توانِ یک هسته — یا shardشده بدونِ هماهنگیِ بینshardی.
Scaling out shards the data (Chapter 7): if each transaction reads and writes a single shard, every shard gets its own thread and throughput grows linearly with cores. But cross-shard transactions need lockstep coordination and are very slow (VoltDB manages ~1000 multi-shard writes per second — orders of magnitude below single-shard). Bottom line: transactions must be small and fast, the active data must fit in memory, and write throughput must fit one core — or be shardable without cross-shard coordination.
۴.۳ two-phase locking4.3 two-phase locking
قفل دوفازی یا 2PL با نگه داشتن قفلهای لازم، ترتیبِ دسترسیِ تراکنشها را کنترل میکند. ممکن است انتظار یا بنبست ایجاد شود و یکی از تراکنشها نیاز به تکرار داشته باشد.
مثال فنی: قفلِ ردیفِ مشترک
وضعیت اولیه: T1 روی ردیف اتاق ۷ قفل میگیرد، overlap را بررسی و رزرو را درج میکند.
اجرای مرحلهبهمرحله: T2 پیش از بررسی همان اتاق منتظر همان قفل میماند؛ بعد از پایان T1، وضعیت تازه را بررسی میکند.
نتیجه و دلیل: قفل باید منبعِ مشترکِ تصمیم را پوشش دهد؛ قفل روی دو ردیفِ مستقل، قانونِ مشترک را خودکار حفظ نمیکند.
For ~30 years the only common serializability algorithm was two-phase locking (2PL). The rule is simple: concurrent reads of an object are free while nobody writes; but a write needs exclusive access — a writer blocks other writers and readers alike, and a reader blocks writers. (Exactly the opposite of snapshot isolation's slogan.)
پیادهسازی: قفلِ shared برای خواندن (چند خواننده همزمان) و قفلِ exclusive برای نوشتن (هیچکس دیگر)،
با قابلیتِ upgrade از shared به exclusive. نامِ «دوفازی» از دو فازِ آن میآید: فازِ رشد (گرفتنِ قفلها حینِ اجرا)
و فازِ کاهش (رها کردنِ همه در پایان)؛ بعد از اولین آزادسازی، دیگر قفلِ جدیدی نمیتوان گرفت.
deadlock (انتظارِ متقابل) را دیتابیس خودش تشخیص میدهد و یکی را abort میکند تا بقیه پیش بروند.
Implementation: shared locks for reads (many concurrent readers), exclusive for writes (nobody else), with shared-to-exclusive upgrade. “Two-phase” comes from its two phases: growth (taking locks during execution) and shrinkage (releasing all at the end); after the first release, no new lock may be taken. Deadlock (mutual waiting) is detected by the database, which aborts one party so the rest proceed.
بهای 2PL کارایی است: فقط overhead قفلها نیست، کاهشِ همزمانی مهمتر است. تراکنشی که کلِ جدول را میخواند
(بکاپ، کوئریِ تحلیلی، بررسیِ سلامت) باید قفلِ sharedِ کلِ جدول بگیرد: اول منتظرِ نوشتنهای در جریان میماند،
بعد تا پایانِ خواندنش همهٔ نویسندهها قفلاند — عملاً دیتابیس برای مدتی به نوشتن بسته میشود. latency ناپایدار میشود
و deadlockها (بهویژه در serializable) زیاد و گران (retry و از سر گرفتنِ کار).
2PL's price is performance: not just lock overhead, but reduced concurrency matters more. A transaction reading a whole table (backup, analytical query, health check) must take a shared lock on the whole table: first it waits for in-flight writes, then all writers stall until its read ends — the database effectively closes for writes for a while. Latency turns unstable and deadlocks (especially at serializable) grow frequent and costly (retry and redo).
برای جلوگیری از phantom هم دو ابزار هست: predicate lock (قفلِ مفهومی روی همهٔ اشیایی که با شرطِ جستوجو جور میشوند —
حتی آنهایی که هنوز وجود ندارند؛ اما تطبیقِ قفلها کُند است) و تقریبِ عملیِ آن، index-range locking
(next-key locking): قفلِ shared روی ورودیِ ایندکس یا بازهای از ایندکس که شرط را پوشش میدهد. تقریب یعنی گاهی بازهای
بزرگتر از لازم قفل میشود، اما overhead بسیار کمتر است. اگر ایندکسِ مناسبی نباشد، دیتابیس به قفلِ کلِ جدول برمیگردد.
Two tools stop phantoms: predicate locks (a conceptual lock on every object matching the search condition — even ones not yet existing; but lock matching is slow) and its practical approximation, index-range locking (next-key locking): a shared lock on the index entry or index range covering the condition. Approximation sometimes locks more than needed, but overhead is far lower. Without a suitable index, the database falls back to a whole-table lock.
۴.۴ SSI و مقایسهٔ سه روش4.4 SSI and comparing the three
در SSI، خواندن از تصویر با بررسیِ وابستگی تراکنشها ترکیب میشود. اگر ادامهٔ همهٔ آنها نتیجهای غیرسریالی بسازد، موتور یکی را لغو میکند تا برنامه دوباره تلاش کند.
مثال فنی: SSI کدام وابستگی را میبیند؟
وضعیت اولیه: T1 خوانده که B کشیک است و T2 خوانده که A کشیک است.
اجرای مرحلهبهمرحله: T1، A را حذف از کشیک میکند و T2، B را؛ SSI وابستگیهای read-write را دنبال میکند و در الگوی خطرناک ممکن است یکی را abort کند.
نتیجه و دلیل: خواندن snapshot ادامه دارد، اما برنامه باید serialization failure و retry کل تراکنش را مدیریت کند؛ SSI همان قفلگذاریِ مسدودکنندهٔ همهٔ خواندنها نیست.
Are serializability and good performance inherently at odds? Apparently not: serializable snapshot isolation (SSI), introduced in 2008, gives full serializability at a small penalty over snapshot isolation. SSI is optimistic: transactions proceed unblocked and are checked at commit; if their execution wasn't serializable, they're aborted. (2PL is pessimistic by contrast: it waits whenever danger is sensed.)
SSI روی snapshot isolation ساخته شده و دو نوع تضاد را دنبال میکند. اول، خواندنِ نسخهٔ کهنهٔ MVCC:
اگر تراکنشی نوشتنِ تراکنشِ دیگری را بهخاطر visibility نادیده گرفته باشد و آن تراکنش تا لحظهٔ commitِ ما commit کرده باشد،
یعنی بر اساسِ «پیشفرضِ کهنه» تصمیم گرفتهایم و باید abort شویم. چرا فوراً abort نکند؟ چون ممکن است تراکنشِ فقط-خواندنی
باشد (write skew نمیسازد) یا آن نوشتنِ دیگر هنوز commit نشده باشد — این تاخیر، حمایت از خواندنهای طولانی را حفظ میکند.
SSI builds on snapshot isolation and tracks two conflict types. First, reading a stale MVCC version: if a transaction ignored another's write via visibility rules and that transaction has committed by our commit time, we decided on a “stale premise” and must abort. Why not abort immediately? The transaction may be read-only (no write skew possible) or the other write may still be uncommitted — this delay keeps long reads safe.
دوم، نوشتنی که خواندنِ قبلی را تحتِ تاثیر قرار میدهد: با تکنیکی شبیهِ index-range lockها — اما بدونِ مسدود کردن —
ثبت میشود که چه تراکنشهایی چه دادههایی را خواندهاند. وقتی نوشتنی به همان بازه میرسد، بهجای قفل یک «نخِ اطلاعرسانی»
(tripwire) عمل میکند: خوانندهها خبردار میشوند که دیدشان ممکن است کهنه باشد. در نهایت، تراکنشی که دیرتر commit میکند
و تضادش با تراکنشِ commitشده اثبات شده، abort میشود.
Second, a write affecting an earlier read: using a technique resembling index-range locks — but without blocking — it records which transactions read what data. When a write lands in that range, instead of a lock a “tripwire” fires: readers learn their view may be stale. Finally, the transaction committing later whose conflict with a committed transaction is proven gets aborted.
| معیار | اجرای سریالی | 2PL | SSI |
| اصلِ کار | حذفِ همزمانی (یک thread) | بدبینانه: قفلِ shared/exclusive | خوشبینانه: بررسی در commit |
| مسدود شدن | در صفِ یک thread | خواننده و نویسنده هم را مسدود میکنند | هیچکس کسی را مسدود نمیکند |
| phantom | ندارد (سریالی است) | با predicate یا index-range lock | با ردیابیِ تضادِ خواندن/نوشتن |
| بزرگترین ضعف | سقفِ یک هسته؛ تراکنشِ کُند کل سیستم را میبندد | latency ناپایدار، deadlock، بسته شدنِ نوشتنها | abort زیرِ contention زیاد؛ overhead دفترداری |
| نمونهها | VoltDB، Datomic، Redis | MySQL/InnoDB و SQL Server (سطحِ serializable) | PostgreSQL، Hekaton، HyPer، CockroachDB، FoundationDB |
| Criterion | Serial execution | 2PL | SSI |
| How it works | Kills concurrency (one thread) | Pessimistic: shared/exclusive locks | Optimistic: checks at commit |
| Blocking | In the single thread's queue | Readers and writers block each other | Nobody blocks anybody |
| Phantoms | None (it's serial) | Via predicate or index-range locks | Via read/write conflict tracking |
| Biggest weakness | One-core ceiling; a slow transaction stalls everything | Unstable latency, deadlocks, stalled writes | Aborts under heavy contention; bookkeeping overhead |
| Examples | VoltDB, Datomic, Redis | MySQL/InnoDB and SQL Server (serializable level) | PostgreSQL, Hekaton, HyPer, CockroachDB, FoundationDB |
5 تراکنش توزیعشده و 2PCDistributed transactions and 2PC
۵.۱ مسالهٔ commit اتمیک + دیاگرام ۵5.1 The atomic-commit problem + Diagram 5
در تأیید اتمیکِ توزیعشده، چند شرکتکننده باید بر یک تصمیم مشترک برسند: همه تأیید کنند یا همه لغو کنند. 2PC این تصمیم را در مرحلهٔ آمادهسازی و سپس اعلام نتیجه هماهنگ میکند.
مثال فنی: 2PC روی دو منبع
وضعیت اولیه: ثبت رزرو در پایگاه A و کم کردن ظرفیت در پایگاه B باید تصمیم مشترک داشته باشد.
اجرای مرحلهبهمرحله: coordinator از هر دو prepare میخواهد؛ پس از رأی مثبتِ همه، تصمیم commit را پایدار و ارسال میکند؛ رأی منفی پیش از تصمیم نهایی به abort میانجامد.
نتیجه و دلیل: هر participant نمیتواند مستقلاً با timeout تصمیم دلخواه بگیرد؛ شرط، پشتیبانی واقعی منابع از پروتکل است.
In a single-node transaction, the storage engine implements atomicity itself: first it writes data durably (usually to the WAL), then appends the commit record. A mid-way crash recovers from the log on restart: commit record present means committed, absent means rolled back. The decision moment is finishing the commit record's write on one disk.
اما در تراکنشِ توزیعشده (چند shard، یا ایندکسِ سراسری روی node دیگر) نمیتوان به هر node گفت «خودت commit کن»:
ممکن است روی بعضی موفق و روی بعضی ناموفق شود — بعضی nodeها نقضِ constraint ببینند، بعضی درخواستها در شبکه گم شوند،
بعضی قبل از نوشتنِ رکوردِ commit crash کنند. اگر بعضی commit و بعضی abort شوند، nodeها ناسازگار میشوند؛
و دادهای که روی یک node committed شد و دیگران خواندند را دیگر نمیتوان پس گرفت.
نامِ این مساله atomic commitment است: یا همه commit، یا همه abort.
But in a distributed transaction (several shards, or a global index on another node), nodes can't each be told “commit yourself”: some may succeed and others fail — constraint violations here, messages lost in the network there, crashes before writing the commit record elsewhere. If some commit and others abort, nodes diverge permanently; data committed on one node and read by others can never be taken back. This is atomic commitment: all commit, or all abort.
روایت مدرس از دیاگرام ۵Teacher narration of Diagram 5
مثال فنی برای خواندنِ نمودار: 2PC روی دو منبع
وضعیت اولیه: ثبت رزرو در پایگاه A و کم کردن ظرفیت در پایگاه B باید تصمیم مشترک داشته باشد.
اجرای مرحلهبهمرحله: coordinator از هر دو prepare میخواهد؛ پس از رأی مثبتِ همه، تصمیم commit را پایدار و ارسال میکند؛ رأی منفی پیش از تصمیم نهایی به abort میانجامد.
نتیجه و دلیل: هر participant نمیتواند مستقلاً با timeout تصمیم دلخواه بگیرد؛ شرط، پشتیبانی واقعی منابع از پروتکل است.
چهار ستون (Application، Coordinator، و دو Database بهعنوان participant) و سه نوار: Phase 1 — prepare and vote، The commit point و Phase 2 — commit, crash, in doubt. در فاز اول coordinator دو فلش PREPARE — can you commit? میفرستد و دو YES — I promise to commit میگیرد؛ یادداشت کلیدی: data written to disk; the right to abort is waived — قول یعنی واگذاری حق abort.
نوار وسط نقطهٔ commit است: تصمیم اول در log خود coordinator پایدار میشود (the decision is written to the log first — the commit point) بعد ابلاغ میشود. در نوار سوم coordinator قبل از رسیدن COMMIT — phase 2 به Database 1 میمیرد و Database 1 با what was the decision? در no reply — db1 stays in doubt and holds its locks میماند. درس: 2PC دو نقطهٔ بازگشتناپذیر دارد — رای yes و تصمیم coordinator — و بعد از تصمیم، تنها راه برگشتن coordinator و خواندن log است.
Four columns (Application, Coordinator, and two Database participants) and three bands: Phase 1 — prepare and vote, The commit point and Phase 2 — commit, crash, in doubt. In phase one the coordinator sends two PREPARE — can you commit? arrows and collects two YES — I promise to commit replies; the key note: data written to disk; the right to abort is waived — a promise means surrendering the abort right.
The middle band is the commit point: the decision is first made durable in the coordinator's own log (the decision is written to the log first — the commit point), then announced. In the third band the coordinator dies before COMMIT — phase 2 reaches Database 1, which asks what was the decision? and remains in no reply — db1 stays in doubt and holds its locks. Lesson: 2PC has two points of no return — the yes vote and the coordinator's decision — and after deciding, the only way forward is the coordinator's return and its log.
۵.۲ in-doubt، خرابیِ coordinator و XA5.2 in-doubt, coordinator failure and XA
شرکتکنندهٔ آماده، قول داده تصمیم نهایی را اجرا کند و ممکن است منابع را نگه دارد. اگر هماهنگکننده در این فاصله از دسترس خارج شود، وضعیت نامشخص میتواند کارهای دیگر را هم معطل کند.
مثال فنی: Prepared و بیخبر از تصمیم
وضعیت اولیه: participant A آماده شده و تعهد داده نتیجهٔ coordinator را اجرا کند؛ ارتباط با coordinator قطع میشود.
اجرای مرحلهبهمرحله: A ممکن است قفلها و وضعیت پایدار را نگه دارد تا تصمیم معتبر بازیابی شود.
نتیجه و دلیل: timeout نمیگوید coordinator commit کرده یا abort؛ آزاد کردن خودسرانهٔ تعهد میتواند atomic commit را بشکند.
2PC is a blocking protocol: after voting yes, a participant can only wait for the coordinator. Even a timeout doesn't help — if Database 1 aborts unilaterally while Database 2 committed, they diverge; unilateral commit is unsafe for the same reason. And while in doubt, the transaction holds its locks: if the coordinator takes twenty minutes to return, nobody can touch that data for twenty minutes. If the coordinator's log is fully destroyed, the locks stay forever — a database restart won't fix it, since 2PC correctness demands in-doubt locks survive restart. The only exit is a sysadmin's manual decision.
XA (eXtended Architecture) استانداردِ پیادهسازیِ 2PC روی فناوریهای ناهمگن است (از ۱۹۹۱؛
در جاوا از طریقِ JTA/JDBC/JMS). اما مشکلاتش جدی است: coordinator معمولاً کتابخانهای داخلِ خودِ process برنامه است،
یعنی crash برنامه یعنی ناپدید شدنِ coordinator و log او؛ دیتابیس نمیتواند مستقیم با coordinator حرف بزند و همهچیز از
مسیرِ کدِ برنامه میگذرد (پس خودِ کدِ برنامه تکنقطهٔ خرابی است)؛ و چون XA باید کفِ مشترکِ همهٔ سیستمها باشد،
نه تشخیصِ deadlockِ بینسیستمی دارد و نه با SSI کار میکند. در هرجومرجِ عملیاتی، بسیاری از سرویسهای ابری اصلاً
تراکنشِ توزیعشده پیاده نمیکنند.
XA (eXtended Architecture) is the standard for 2PC across heterogeneous tech (since 1991; in Java via JTA/JDBC/JMS). But its problems are serious: the coordinator is usually a library inside the application's own process, so an app crash loses the coordinator and its log; the database can't talk to the coordinator directly — everything flows through application code (so the app code itself is a single point of failure); and since XA must be the common floor of all systems, it has neither cross-system deadlock detection nor SSI support. Amid operational chaos, many cloud services simply don't implement distributed transactions.
heuristic decision — فرارِ اضطراری که ضمانت را میشکندheuristic decision — the emergency exit that breaks the guarantee
بسیاری از پیادهسازیهای XA راهِ فراری دارند: اجازه میدهند participant بدونِ تصمیمِ قطعیِ coordinator، یکجانبه commit یا abort کند.
نامِ «heuristic» در واقع اصطلاحی مودبانه برای «احتمالاً atomicity را میشکنی» است؛ فقط برای فاجعههاست، نه استفادهٔ روزمره.
Many XA implementations offer an escape hatch: letting a participant unilaterally commit or abort without the coordinator's firm decision. The name “heuristic” is politely saying “you'll probably break atomicity”; for disasters only, never daily use.
در مقابل، تراکنشهای توزیعشدهٔ داخلی (همهٔ participantها یک دیتابیسِ واحد با نرمافزارِ یکسان: CockroachDB، TiDB،
Spanner، FoundationDB، YugabyteDB، یا Kafka) وضعیتِ بسیار بهتری دارند: همچنان از 2PC استفاده میکنند اما coordinator را
با consensus تکثیر میکنند (failover خودکار)، اجازه میدهند coordinator مستقیم با shardها حرف بزند، خودِ shardها را هم
تکثیر میکنند، و commit اتمیک را با کنترلِ همزمانیِ توزیعشده (تشخیصِ deadlock، خواندنِ سازگار) جفت میکنند.
By contrast, internal distributed transactions (all participants one database with identical software: CockroachDB, TiDB, Spanner, FoundationDB, YugabyteDB, or Kafka) fare far better: they still use 2PC but replicate the coordinator via consensus (automatic failover), let the coordinator talk to shards directly, replicate the shards themselves, and pair atomic commit with distributed concurrency control (deadlock detection, consistent reads).
۵.۳ exactly-once و idempotency5.3 exactly-once and idempotency
اثرِ یکباره یعنی تکرار درخواست، نتیجهٔ کسبوکاری را دوبار ایجاد نکند. برای این کار، شناسهٔ ثابتِ درخواست و ثبت اتمیکِ نتیجه و جلوگیری از تکرار لازم است.
مثال فنی: تکرار پیام، یک اثر
وضعیت اولیه: پیام event_id=e7 باید موجودی را یک واحد کم کند.
اجرای مرحلهبهمرحله: داخل یک تراکنش، شناسه در جدول با قید unique درج و موجودی کم میشود؛ تحویل دوبارهٔ e7 از قید عبور نمیکند و کاهش دوباره انجام نمیشود.
نتیجه و دلیل: ثبت شناسه و اثر باید اتمیک باشند؛ شناسه تا پایان افق retry/replay حفظ میشود و اثر بیرونیِ جدا قرارداد دیگری میخواهد.
One major use of distributed transactions: a queue message must be processed “exactly once” — the queue ack only once the message's resulting database writes commit. But 2PC isn't really needed; a simpler pattern needs only a transaction inside the database:
- هر پیام شناسهٔ یکتایی دارد و جدولی از شناسههای پردازششده در دیتابیس داری. شروعِ پردازش = شروعِ تراکنش و چکِ شناسه؛ اگر تکراری بود، فقط ack میکنی و رها میکنی.
- اگر نبود، شناسه را در همان تراکنش درج میکنی، پیام را پردازش میکنی (نوشتنهای دیگر هم در همان تراکنش) و commit میکنی.
- بعد از commitِ موفق، پیام را به صف ack میکنی.
- شناسه تا پایانِ دورهٔ ممکنِ تحویلِ دوباره، بازتلاش یا بازپخش حفظ میشود؛ تأییدِ پیام بهتنهایی مجوزِ حذفِ فوریِ شناسه نیست.
- Each message has a unique id, and you keep a table of processed ids in the database. Starting processing = starting a transaction and checking the id; if duplicated, just ack and drop.
- If new, insert the id in the same transaction, process the message (all further writes in that same transaction) and commit.
- After a successful commit, ack the message to the queue.
- After acking, delete the id (in a separate transaction) so the table stays small.
-- Pattern skeleton: the processed-messages table is the idempotency key
-- اسکلتِ الگو: جدولِ پیامهای پردازششده = idempotency key
BEGIN TRANSACTION;
INSERT INTO processed_messages (message_id) VALUES ($1); -- unique constraint
-- ... process the message: any number of further writes in this same transaction ...
-- ... پردازشِ پیام: هر تعداد نوشتنِ دیگر در همین تراکنش ...
COMMIT;
-- then ack to the queue, and later delete message_id in a separate transaction
-- سپس ack به صف، و بعداً حذفِ message_id در تراکنشی جدا
اگر وسطِ کار crash شود، تراکنش abort و پیام دوباره ارسال میشود؛ تلاشِ دوم شناسه را میبیند و فقط ack میکند.
این یعنی پردازشِ پیام idempotent شده: تکرارش اثرِ جانبیِ دوباره نمیسازد. (اگر retry حتی قبل از abortِ
تراکنشِ قبلی برسد، unique constraint جلوی درجِ دوباره را میگیرد.) همین الگو در چارچوبهای پردازشِ جریانی مثل
Kafka Streams برای exactly-once استفاده میشود.
A mid-way crash aborts the transaction and the message is redelivered; the second attempt sees the id and only acks. Message processing has become idempotent: repetition creates no repeated side effect. (If a retry arrives even before the earlier transaction aborts, the unique constraint blocks the duplicate insert.) Stream frameworks like Kafka Streams use this same pattern for exactly-once.
جمعبندیِ نیمهٔ توزیعشدهDistributed-half recap
اگر همهٔ nodeها نرمافزارِ یکسانِ دیتابیس داشته باشند، تراکنشِ توزیعشده میتواند خوب کار کند. اما روی فناوریهای
ناهمگن (XA) بسیار شکننده است: به خرابیِ coordinator و کدِ برنامه حساس است و با کنترلِ همزمانی بد جفت میشود.
خوشبختانه برای مسالهٔ «دقیقاً یکبار»، اغلب idempotency کافی است و به commit اتمیکِ بینِ سیستمها نیازی نیست.
If all nodes run identical database software, distributed transactions can work well. But over heterogeneous tech (XA) they're fragile: sensitive to coordinator and application-code failure, and badly paired with concurrency control. Fortunately “exactly once” usually just needs idempotency — no cross-system atomic commit.
۵.۴ ماشینِ وضعیتِ 2PC از دیدِ participant + دیاگرام ۸5.4 The 2PC participant state machine + Diagram 8
از دید شرکتکنندهٔ 2PC، «آماده» یک تعهد پایدار است. پس از آن باید تصمیمِ ثبتشدهٔ هماهنگکننده دنبال شود، حتی اگر شرکتکننده متوقف و دوباره راهاندازی شده باشد.
مثال فنی: چرخهٔ participant در 2PC
وضعیت اولیه: participant در حالت active تغییرها را موقت دارد.
اجرای مرحلهبهمرحله: پس از prepare موفق، وضعیت prepared را پایدار میکند؛ با تصمیم معتبر commit به committed و با تصمیم معتبر abort به aborted میرود.
نتیجه و دلیل: بعد از رأی yes، restart باید تعهد prepared را به یاد بیاورد؛ آن را مثل تراکنش عادیِ بیتصمیم دور نمیریزد.
If Diagram 5 was the “message flow”, this one shows each participant's “inner states” — and the crux sits exactly here: the Prepared state. Before it, a participant may abort any time; after it, it has surrendered the right to decide and must wait for the coordinator.
روایت مدرس از دیاگرام ۸Teacher narration of Diagram 8
مثال فنی برای خواندنِ نمودار: چرخهٔ participant در 2PC
وضعیت اولیه: participant در حالت active تغییرها را موقت دارد.
اجرای مرحلهبهمرحله: پس از prepare موفق، وضعیت prepared را پایدار میکند؛ با تصمیم معتبر commit به committed و با تصمیم معتبر abort به aborted میرود.
نتیجه و دلیل: بعد از رأی yes، restart باید تعهد prepared را به یاد بیاورد؛ آن را مثل تراکنش عادیِ بیتصمیم دور نمیریزد.
چهار لین (Participant، Waiting on the coordinator، Coordinator recovery، Terminal). از Working (هنوز abort مجاز است) با PREPARE received به Prepared میروی (voted yes — نقطهٔ قول). از آنجا سه راه است: COMMIT received به Committed، ABORT received به Aborted، و coordinator crashes به In Doubt (locks are still held).
بازیابی: coordinator restarts به Replaying the Log میرسد و commit record found به Committed یا no commit record به Aborted میبرد؛ اگر خود log از بین رفته باشد، log lost به Orphaned In Doubt (manual decision) میرسد. درس: بین «رای yes» و «رکورد تصمیم» participant نه میتواند جلو برود نه عقب — فقط صبر و نگهداشتن قفل؛ هر طراحی مطمئن یا همین صبر را تحملپذیر میکند (coordinator تکثیرشده) یا مساله را با idempotency دور میزند.
Four lanes (Participant, Waiting on the coordinator, Coordinator recovery, Terminal). From Working (abort still allowed), PREPARE received leads to Prepared (voted yes — the promise point). Three exits leave it: COMMIT received to Committed, ABORT received to Aborted, and coordinator crashes to In Doubt (locks are still held).
Recovery: coordinator restarts reaches Replaying the Log, and commit record found goes to Committed or no commit record to Aborted; if the log itself is gone, log lost ends in Orphaned In Doubt (manual decision). Lesson: between the “yes vote” and the “decision record” a participant can move neither forward nor back — only wait, holding locks; every robust design either makes this wait tolerable (a replicated coordinator) or sidesteps the problem with idempotency.
روایت مدرس از نقشهٔ فصلTeacher narration of the chapter map
مثال فنی برای خواندنِ نمودار: دو تغییر که باید یک نتیجه بدهند
وضعیت اولیه: صندلی S7 آزاد است و هنوز رزروی برای درخواست r7 نداریم.
اجرای مرحلهبهمرحله: تراکنش وضعیت صندلی را رزروشده میکند و ردیف رزرو را میسازد؛ شکست درج باید تغییر وضعیت را هم برگرداند.
نتیجه و دلیل: رزرو بدون صندلی و صندلیِ گرفتهشده بدون رزرو، هر دو حالت ناقصاند؛ کنترل رقابتِ دو خریدار هم جدا لازم است.
گرهٔ وسط (Transactions) را مرکز بگیر و یالها را دنبال کن: بالا قولها هستند (ACID، سطوح isolation، MVCC)، دو طرف تهدیدها (آنومالیها) و راهحلهای قوی (سه جادهٔ serializability)، و پایین دنیای توزیعشده (atomic commit با 2PC، بنبست in-doubt و XA، و راه فرار idempotency).
این نقشه کل فصل را در یک جمله خلاصه میکند: قولهای بالا زیر race conditionهای کناری میشکنند، و پایین یا commit اتمیک میخواهد یا idempotency.
Take the center node (Transactions) and follow the edges: promises on top (ACID, isolation levels, MVCC), threats on the sides (anomalies) with the strong fixes (the three serializability roads), and the distributed world below (atomic commit with 2PC, the in-doubt trap and XA, and the idempotency escape).
The map compresses the chapter into one sentence: the promises on top break under the races on the sides, and the bottom needs either atomic commit or idempotency.
7 تمرینهای مصاحبه و آزمایشگاهInterview exercises and hands-on labs
این تمرینها برای مسیر Backend / Distributed Systems در ۱۴ اکتبر ۲۰۲۶ بازبینی شدند. سطح مربوط به نسخهٔ مصاحبهای است؛ زمان آزمایشگاه جداست.Reviewed on 14 October 2026 for Backend / Distributed Systems. Level refers to the interview brief; full-lab time is separate. روش ارزیابی و مسیر مطالعهMethod and study path
تمرین ۱ — ناهنجاریها را زنده بسازExercise 1 — reproduce the anomalies live
L2 · SeniorL2 · Seniorاولویت اصلیCore priority40 دقیقه بحث40 min discussion
آزمایشها برای PostgreSQL 18 هستند: READ UNCOMMITTED مانند READ COMMITTED است، RR جلوی phantom را میگیرد و write skew هنوز ممکن است.Tests target PostgreSQL 18: READ UNCOMMITTED behaves as READ COMMITTED, RR prevents phantoms, and write skew can still occur.
نسخهٔ بازبینیشده برای مصاحبهReviewed interview brief
روی PostgreSQL مشخص، history دو اتصال برای read skew، lost update، phantom و write skew بساز و isolation را مقایسه کن. برای dirty read/write آزمون عدم وقوع و blocking بنویس. ناهنجاری را با SQL دقیق توضیح بده و رفتار محصول را از حداقل استاندارد SQL جدا کن.Create two-session SQL histories for read skew, lost update, phantom and write skew on a named PostgreSQL version. Test dirty-read/write prevention and blocking. Compare implemented guarantees with the SQL standard.
معیار پاسخ خوبWhat a good answer demonstrates
- عدم وقوع هم نتیجهٔ معتبر آزمایش استValid prevention results
- lost update ناشی از read-modify-write را درست نشان میدهدCorrect read-modify-write histories
- RR در PostgreSQL phantom را حذف میکند ولی write skew ممکن استProduct-specific isolation guarantees
پیگیری مصاحبه:Interview follow-up: UPDATE balance=balance+1 چه تفاوتی با SELECT و UPDATE مقدار ثابت دارد؟How does UPDATE balance=balance+1 differ from SELECT followed by an absolute-value UPDATE?
آزمایشگاه تکمیلی · زمان تقریبی ۳–۶ ساعتExtended hands-on laboratory · estimated 3–6 hours
روی PostgreSQL 18 با دو session، history دقیق SQL بساز. read skew و read-modify-write lost update را روی حسابها، phantom را روی پرسوجوی مجموعه، و write skew را روی invariant دو پزشک on-call آزمایش کن. سطح isolation، ترتیب statementها و commit/rollback را ثبت کن. برای dirty read نتیجه باید عدم رؤیت دادهٔ commitنشده باشد و برای dirty write انتظار روی row lock را ثبت کن. phantom در RR این محصول منع میشود؛ برای write skew، RR را با Serializable و retry کامل تراکنش مقایسه کن. شمارش نتیجهٔ ممنوعشده بهعنوان «جلوگیری موفق» بخشی از آزمون است.
Use two sessions on PostgreSQL 18 and record exact SQL histories. Test account read skew, read-modify-write lost updates, phantom set queries and a two-doctor on-call write-skew invariant. Record isolation, statement order and commit/rollback. Assert dirty-read prevention and dirty-write row-lock waiting. PostgreSQL RR prevents phantoms; compare RR write skew with Serializable and whole-transaction retry. Prevention of a forbidden outcome is a successful test result.
برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمیرود.The full laboratory is optional depth work and is not expected within the discussion time.
ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · PostgreSQL 18 · transaction isolation
تمرین ۲ — یک سیستمِ رزروِ بدونِ double-booking طراحی کنExercise 2 — a reservation system with no double-booking
L2 · SeniorL2 · Seniorاولویت اصلیCore priority45 دقیقه بحث45 min discussion
رزروها بازهٔ نیمهباز [start,end) دارند؛ قفل روی سطر موجود اتاق در همان تراکنش گرفته میشود و بررسی overlap بعد از اخذ قفل است.Bookings use half-open intervals [start,end); lock the existing room row within the transaction and check overlap after acquiring it.
نسخهٔ بازبینیشده برای مصاحبهReviewed interview brief
برای رزرو بازهای یک اتاق، invariant عدم overlap را تعریف کن. check+insert، READ COMMITTED، lock روی ردیف ثابت اتاق، SERIALIZABLE و exclusion constraint را مقایسه کن. برای پنجاه درخواست همزمان retry، contention و p99 را طراحی کن؛ قید UNIQUE فقط برای slot ثابت کافی است.Enforce no overlapping room reservations. Compare check/insert, Read Committed, a stable room-row lock, Serializable and exclusion constraints. Design 50-way contention tests and report retry and latency; UNIQUE fits discrete fixed slots.
معیار پاسخ خوبWhat a good answer demonstrates
- قفل ردیف موجود اتاق را از قفل رزرو خالی جدا میکندExisting room-row lock
- بازه زمانی با equality اشتباه نشدهOverlap versus equality constraints
- abort، retry کل تراکنش و fairness دیده شدهاندWhole-transaction retry and contention
پیگیری مصاحبه:Interview follow-up: کاربر رزرو را تمدید کند و همزمان رزرو بعدی ساخته شود؛ قیدت هنوز درست است؟A user extends a booking while another user reserves the next interval. Is the constraint still correct?
آزمایشگاه تکمیلی · زمان تقریبی ۳–۶ ساعتExtended hands-on laboratory · estimated 3–6 hours
برای هر اتاق، نبود دو بازهٔ commitشده با شرط start1
Enforce no committed pair for a room satisfying start1
برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمیرود.The full laboratory is optional depth work and is not expected within the discussion time.
ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · PostgreSQL 18 · transaction isolation · PostgreSQL 18 · SELECT locking clauses · PostgreSQL 18 · ranges and exclusion constraints
تمرین ۳ — exactly-once با idempotency، نه با 2PCExercise 3 — exactly-once with idempotency, not 2PC
L2 · SeniorL2 · Seniorاولویت اصلیCore priority45 دقیقه بحث45 min discussion
اثر محلی و کلید dedup در یک تراکنشاند؛ ACK بعد از commit است و رکورد dedup تا پایان افق معتبر replay حفظ میشود.The local effect and dedup key share one transaction; ACK follows commit and dedup records survive the supported replay horizon.
نسخهٔ بازبینیشده برای مصاحبهReviewed interview brief
صف و پردازشگر با processed_messages و قید UNIQUE بساز. ثبت ID و اثر محلی را در یک تراکنش و ACK را پس از commit بگذار. در سه نقطه crash تزریق کن؛ نسخهٔ بدون dedup را مقایسه کن. retention شناسه و side effect بیرونی را صریح تعیین کن.Implement an idempotent queue consumer using a unique processed-message ID and local effects in one transaction, then ACK. Inject three crash points, compare with no dedup, and define retention and external-effect boundaries.
معیار پاسخ خوبWhat a good answer demonstrates
- commit-before-ack و redelivery را توضیح میدهدCommit before ACK
- تکراری با payload متفاوت رد میشودPayload binding
- حد تضمین محلی و افق dedup مشخص استDefined local boundary and dedup horizon
پیگیری مصاحبه:Interview follow-up: اپراتور پیام ششماهه را replay کند ولی dedup بعد از یک ماه حذف شده باشد چه میشود؟An operator replays a six-month-old message after dedup was deleted at one month. What happens?
آزمایشگاه تکمیلی · زمان تقریبی ۲–۴ ساعتExtended hands-on laboratory · estimated 2–4 hours
صف at-least-once و جدول processed_messages با کلید یکتا در همان DB اثر محلی بساز. درج شناسه و اثر را یکجا commit کن، سپس ACK بده. crash قبل از commit، بعد از commit و قبل از ACK، و بعد از ACK را تزریق کن و مجموع اثر را assert کن. افق replay را از retry کاربر، نگهداری صف، DLQ و restore تعیین کن؛ ACK مجوز پاککردن کلید نیست. replay یک پیام پس از GC را عمداً آزمایش و سیاست رد/بازیابی آن را مشخص کن. ارسال SMS یا برداشت از سرویس بیرونی را خارج از تضمین تراکنش بدان و کلید idempotency مقصد را جدا طراحی کن. نسخهٔ بدون dedup را برای مقایسه اجرا کن.
Build an at-least-once queue and a unique processed_messages key in the same database as the local effect. Commit ID and effect together, then ACK. Inject crashes before commit, after commit before ACK and after ACK; assert total effect. Derive the replay horizon from client retries, queue retention, DLQ and restores; ACK does not authorize key deletion. Test replay after GC explicitly and specify rejection/recovery. External SMS or provider charges need destination idempotency outside this local transaction. Run a no-dedup comparison.
برای یادگیریِ عمیق است؛ انجام کاملش در زمان بحثِ مصاحبه انتظار نمیرود.The full laboratory is optional depth work and is not expected within the discussion time.
ارزیابی، سطح و شواهد این تمرینAssessment, level and evidence · حل آموزشی این تمرینWorked solution · AWS · safe retries with idempotent APIs · Kafka 4.1 · design and delivery semantics