DDIA·مطالعهSTUDY

نقشه راهRoadmap / Chapter 8

تراکنش‌ها (Transactions)Transactions

تراکنش کارِ چندمرحله‌ای را قابل‌کنترل‌تر می‌کند، اما همهٔ سطح‌های جداسازی تضمینِ یکسان ندارند. این فصل نشان می‌دهد تغییرهای هم‌زمان چگونه قانونِ برنامه را می‌شکنند و هر راه‌حل چه هزینه‌ای دارد. مثال‌های کاربردی کنارِ هر موضوع آمده‌اند. Real data systems are full of failure: the database dies mid-write, the network drops, concurrent clients overwrite one record, and race conditions breed surprising bugs. A transaction groups reads and writes into one logical unit — either all apply or none. This chapter splits in two: concurrency control (isolation levels from read committed and snapshot isolation to serializability) and atomic commit in the distributed case (two-phase commit). With 11 diagrams, a teacher narration for each, and an interview question pack.

۱۱ دیاگرام با روایت گام‌به‌گامdiagrams with step-by-step narration· محور: ACID · سطوح isolation · MVCC · write skew · serializability · 2PCTheme: ACID · isolation levels · MVCC · write skew · serializability · 2PC· زمان مطالعه: حدود ۶۰ دقیقهReading time: ~60 min

0 نقشهٔ راه فصلChapter roadmap

ایدهٔ کلیدی فصلChapter thesis

تراکنش کارِ چندمرحله‌ای را قابل‌کنترل‌تر می‌کند، اما همهٔ سطح‌های جداسازی تضمینِ یکسان ندارند. این فصل نشان می‌دهد تغییرهای هم‌زمان چگونه قانونِ برنامه را می‌شکنند و هر راه‌حل چه هزینه‌ای دارد.

مثال فنی: دو تغییر که باید یک نتیجه بدهند

وضعیت اولیه: صندلی S7 آزاد است و هنوز رزروی برای درخواست r7 نداریم.

اجرای مرحله‌به‌مرحله: تراکنش وضعیت صندلی را رزروشده می‌کند و ردیف رزرو را می‌سازد؛ شکست درج باید تغییر وضعیت را هم برگرداند.

نتیجه و دلیل: رزرو بدون صندلی و صندلیِ گرفته‌شده بدون رزرو، هر دو حالت ناقص‌اند؛ کنترل رقابتِ دو خریدار هم جدا لازم است.

A transaction is an abstraction: the application pretends whole classes of concurrency and failure problems don't exist. A large class of errors collapses into one thing — aborting the transaction — and the app just retries. But “ACID” is mostly a marketing word today; every database implements it differently, and isolation is its vaguest letter.

برای همین فصل دو نیمه دارد. نیمهٔ اول کنترلِ هم‌زمانی است: چه race conditionهایی ممکن است رخ دهد (dirty read، dirty write، read skew، lost update، phantom، write skew) و هر سطحِ isolation جلوی کدام‌ها را می‌گیرد. نیمهٔ دوم اتمیک بودن در حالتِ توزیع‌شده است: وقتی تراکنش به چند node می‌رسد، چطور می‌توان تضمین کرد یا همه commit می‌شوند یا هیچ‌کدام — با two-phase commit.

So the chapter has two halves. The first is concurrency control: which race conditions can occur (dirty reads and writes, read skew, lost updates, phantoms, write skew) and which isolation level stops which. The second is atomicity in the distributed case: when a transaction spans nodes, how to guarantee all commit or none — with two-phase commit.

پنج ایده‌ای که این فصل روی آن‌ها می‌چرخد:

The chapter revolves around five ideas:

  • معنای واقعیِ ACID: atomicity در ACID یعنی «قابلیتِ abort» نه «اتمی در برنامه‌نویسیِ چندریته‌ای»؛ consistency یعنی invarientهای خودِ برنامه؛ isolation یعنی جداسازیِ تراکنش‌های هم‌زمان؛ durability یعنی ماندگاریِ پس از commit.
  • سطوحِ isolation و آنومالی‌ها: هر سطحِ ضعیف، دسته‌ای از race conditionها را باقی می‌گذارد و ترمیمشان را گردنِ برنامه می‌اندازد.
  • MVCC: پایهٔ snapshot isolation — نگه‌داشتنِ چند نسخه از هر ردیف و فیلتر کردنِ آن‌ها با قواعدِ visibility.
  • سه راه به سوی serializability: اجرای سریالی، قفلِ دوفازی (2PL) و snapshot isolation سریال‌پذیر (SSI).
  • توزیع‌شده: مسالهٔ atomic commit، پروتکلِ 2PC، گیر افتادنِ in-doubt، و اینکه چرا idempotency اغلب از XA بهتر است.
  • Real meaning of ACID: atomicity in ACID means “abortability”, not “thread-level atomicity”; consistency means the application's own invariants.
  • Isolation levels and anomalies: each weak level leaves a class of race conditions behind and hands their repair to the application.
  • MVCC: the basis of snapshot isolation — keeping several versions of each row and filtering them with visibility rules.
  • Three roads to serializability: serial execution, two-phase locking (2PL) and serializable snapshot isolation (SSI).
  • Distributed: the atomic-commit problem, the 2PC protocol, getting stuck in-doubt, and why idempotency is often better than XA.
دام‌های اصطلاحی این فصلTerminology traps
  • 2PL ≠ 2PC: 2PL (قفلِ دوفازی) برای serializable isolation است؛ 2PC (commit دوفازی) برای commit اتمیک در دیتابیسِ توزیع‌شده. فقط اسم‌شان شبیه است!
  • «atomic» دو معنا دارد: در چندریته‌ای یعنی «هیچ‌کس نتیجهٔ نیمه‌کاره نمی‌بیند»؛ در ACID یعنی «همه یا هیچ، با قابلیتِ abort». حتی «increment اتمی» هم از نظرِ ACID باید «increment سریال‌پذیر» نامیده شود.
  • «consistency» پنج معنا دارد: سازگاریِ replica، snapshot سازگار، consistent hashing، linearizability در CAP، و invarientهای خودِ برنامه در ACID.
  • «repeatable read» هر جا یک چیز است: در PostgreSQL یعنی snapshot isolation، در MySQL ضعیف‌تر از آن، در Db2 یعنی serializability!
  • 2PL ≠ 2PC: 2PL (two-phase locking) is for serializable isolation; 2PC (two-phase commit) is for atomic commit in a distributed database. Only the names rhyme!
  • “atomic” has two meanings: in threading it means “nobody sees a half-done result”; in ACID it means “all or nothing, with abortability”. Even “atomic increment” should have been called “serializable increment” in ACID terms.
  • “consistency” has five meanings: replica convergence, consistent snapshot, consistent hashing, linearizability in CAP, and the application's own invariants in ACID.
  • “repeatable read” is one thing everywhere — not: in PostgreSQL it means snapshot isolation, in MySQL weaker than that, in Db2 serializability!
نکتهٔ مصاحبهInterview tip

جملهٔ طلایی این فصل: «تراکنش، تعدادِ حالت‌های خطایی که باید به آن فکر کنی را کم می‌کند — نه صفر، ولی خیلی». و نکتهٔ دوم: حتی دیتابیس‌های «ACID» معمولاً isolation ضعیف دارند؛ پس جملهٔ «برای دادهٔ مالی از دیتابیسِ ACID استفاده کن» کافی نیست — باید بدانی دقیقاً کدام سطحِ isolation را گرفته‌ای و کدام آنومالی‌ها هنوز ممکن‌اند.

The chapter's golden line: “a transaction shrinks the failure states you must think about — not to zero, but a lot.” And second: even “ACID” databases usually have weak isolation; “use an ACID database for financial data” is not enough — you must know exactly which isolation level you have and which anomalies are still possible.

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 ضمانتِ مطلق نیست؛ اگر همهٔ دیسک‌ها و بکاپ‌ها با هم نابود شوند، کاری از دستِ دیتابیس برنمی‌آید
LetterReal meaningWhat it is not
A — AtomicityIf an error strikes mid-write, the transaction aborts and every write so far is rolled back; all-or-nothing with safe retryNothing to do with “thread-level atomicity” (that's isolation). abortability would have been a better name
C — ConsistencyThe application's own invariants always hold; e.g. in accounting, debits and credits always balanceReplica convergence, consistent snapshots, consistent hashing, or linearizability — other meanings of the same word
I — IsolationConcurrent transactions must not trip each other; the classic definition is serializability — the outcome equals serial executionIn practice most databases give weaker levels and accept some anomalies
D — DurabilityAfter a successful commit, data is never forgotten; single-node via fsync and WAL, distributed via copies on several nodesNot 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.

دیاگرام مفهومی · ACID حرف‌به‌حرف — معنای واقعی هر قولConcept diagram · ACID letter by letter — what each promise really means باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام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!

دیاگرام مفهومی · retry امن — چه خطاهایی ارزش دوباره تلاش کردن دارندConcept diagram · safe retry — which failures deserve another try باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام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.

دیاگرام ۶ · Lifecycle — ماشینِ وضعیتِ یک تراکنشDiagram 6 · Lifecycle — a transaction’s state machine باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام ۶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.

دیاگرام ۱ · Dataflow — MVCC: نسخه‌های ردیف، قواعدِ visibility و پاک‌سازیDiagram 1 · Dataflow — MVCC: row versions, visibility rules and cleanup باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام ۱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)
  1. در شروعِ تراکنش، فهرستِ تراکنش‌های در جریان ثبت می‌شود؛ نوشتنِ آن‌ها نادیده گرفته می‌شود حتی اگر بعداً commit کنند.
  2. نوشتنِ تراکنش‌هایی با txid بزرگ‌تر (یعنی بعد از ما شروع شده‌اند) نادیده گرفته می‌شود، committed یا نه.
  3. نوشتنِ تراکنش‌های abortشده همیشه نادیده گرفته می‌شود (پس لازم نیست بلافاصله پاک شوند؛ GC بعداً این کار را می‌کند).
  4. بقیهٔ نوشتن‌ها مرئی‌اند.
  1. At transaction start, the list of in-flight transactions is recorded; their writes are ignored even if they commit later.
  2. Writes of transactions with larger txids (started after us) are ignored, committed or not.
  3. Writes of aborted transactions are always ignored (so they need no immediate cleanup; GC does it later).
  4. 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

دیتابیسنامی که استفاده می‌کندچه چیزی واقعاً می‌دهد
PostgreSQLrepeatable readsnapshot isolation
Oracleserializablesnapshot isolation (نه serializable واقعی!)
MySQL/InnoDBrepeatable readMVCC با سازگاریِ ضعیف‌تر از snapshot isolation
IBM Db2repeatable readserializability
DatabaseName usedWhat it really gives
PostgreSQLrepeatable readsnapshot isolation
Oracleserializablesnapshot isolation (not genuine serializability!)
MySQL/InnoDBrepeatable readMVCC with weaker guarantees than snapshot isolation
IBM Db2repeatable readserializability

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

مثال فنی: نام یکسان، قرارداد را بخوان

وضعیت اولیه: 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.

دیاگرام ۷ · UML — کلاس‌ها و نسخه‌های MVCC (class + object diagram)Diagram 7 · UML — MVCC classes and versions (class + object diagram) باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام ۷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؛ دو فروش از موجودی ۱۰
گامT1T2موجودیِ ثبت‌شده
۱۱۰ را می‌خواند۱۰ را می‌خواند۱۰
۲۹ می‌نویسد و 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 استثنا قائل می‌شود
RemedyIdeaWhen good / when not
Built-in atomic opUPDATE counters SET value = value + 1 WHERE key = 'foo' — no read-modify-write cycleBest option when the logic fits these ops; beware ORMs that silently write read-modify-write!
Explicit lockSELECT ... FOR UPDATE — lock the returned rows, then changeWhen app logic (e.g. game rules) must run in between; deadlock and retry risk
Auto-detect + retryBoth run; if the manager spots a lost update, it aborts the loserPostgreSQL (RR), Oracle (serializable), SQL Server (snapshot) do this; MySQL/InnoDB doesn't!
conditional write / CASWrite 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 برای پزشک AT2 برای پزشک 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.

دیاگرام ۲ · Sequence — write skew: دو پزشک، صفر نفر on-callDiagram 2 · Sequence — write skew: two doctors, zero on call باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام ۲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:

  1. یک SELECT با شرطِ جست‌وجو چک می‌کند آیا الزام برقرار است (حداقل دو پزشک on-call؟ رزروی روی این اتاق در این بازه نیست؟ این خانهٔ صفحهٔ شطرنج خالی است؟ این نام‌کاربری آزاد است؟ موجودی کافی است؟).
  2. برنامه بر اساسِ نتیجه تصمیم می‌گیرد (ادامه یا خطا).
  3. اگر ادامه دهد، می‌نویسد (INSERT/UPDATE/DELETE) و commit می‌کند.
  1. 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?).
  2. The application decides from the result (proceed or error).
  3. 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!

دیاگرام ۳ · Workflow — انتخابِ نگهبان در برابر race conditionDiagram 3 · Workflow — choosing a guard against race conditions باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام ۳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:

سطحِ isolationDirty readsRead skewPhantom readsLost updatesWrite skew
Read uncommitted ✗ ممکن✗ ممکن✗ ممکن✗ ممکن✗ ممکن
Read committed ✓ جلوگیری✗ ممکن✗ ممکن✗ ممکن✗ ممکن
Snapshot isolation ✓ جلوگیری✓ جلوگیری✓ جلوگیری? بستگی دارد✗ ممکن
Serializable ✓ جلوگیری✓ جلوگیری✓ جلوگیری✓ جلوگیری✓ جلوگیری
Isolation levelDirty readsRead skewPhantom readsLost updatesWrite 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

دیاگرام ۴ · Architecture — سه راه به سوی serializabilityDiagram 4 · Architecture — three roads to serializability باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام ۴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.

معیاراجرای سریالی2PLSSI
اصلِ کارحذفِ هم‌زمانی (یک thread)بدبینانه: قفلِ shared/exclusiveخوش‌بینانه: بررسی در commit
مسدود شدندر صفِ یک threadخواننده و نویسنده هم را مسدود می‌کنندهیچ‌کس کسی را مسدود نمی‌کند
phantomندارد (سریالی است)با predicate یا index-range lockبا ردیابیِ تضادِ خواندن/نوشتن
بزرگ‌ترین ضعفسقفِ یک هسته؛ تراکنشِ کُند کل سیستم را می‌بنددlatency ناپایدار، deadlock، بسته شدنِ نوشتن‌هاabort زیرِ contention زیاد؛ overhead دفترداری
نمونه‌هاVoltDB، Datomic، RedisMySQL/InnoDB و SQL Server (سطحِ serializable)PostgreSQL، Hekaton، HyPer، CockroachDB، FoundationDB
CriterionSerial execution2PLSSI
How it worksKills concurrency (one thread)Pessimistic: shared/exclusive locksOptimistic: checks at commit
BlockingIn the single thread's queueReaders and writers block each otherNobody blocks anybody
PhantomsNone (it's serial)Via predicate or index-range locksVia read/write conflict tracking
Biggest weaknessOne-core ceiling; a slow transaction stalls everythingUnstable latency, deadlocks, stalled writesAborts under heavy contention; bookkeeping overhead
ExamplesVoltDB, Datomic, RedisMySQL/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.

دیاگرام ۵ · Sequence — two-phase commit: قول‌ها، نقطهٔ commit و in-doubtDiagram 5 · Sequence — two-phase commit: promises, the commit point and in-doubt باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام ۵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:

  1. هر پیام شناسهٔ یکتایی دارد و جدولی از شناسه‌های پردازش‌شده در دیتابیس داری. شروعِ پردازش = شروعِ تراکنش و چکِ شناسه؛ اگر تکراری بود، فقط ack می‌کنی و رها می‌کنی.
  2. اگر نبود، شناسه را در همان تراکنش درج می‌کنی، پیام را پردازش می‌کنی (نوشتن‌های دیگر هم در همان تراکنش) و commit می‌کنی.
  3. بعد از commitِ موفق، پیام را به صف ack می‌کنی.
  4. شناسه تا پایانِ دورهٔ ممکنِ تحویلِ دوباره، بازتلاش یا بازپخش حفظ می‌شود؛ تأییدِ پیام به‌تنهایی مجوزِ حذفِ فوریِ شناسه نیست.
  1. 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.
  2. If new, insert the id in the same transaction, process the message (all further writes in that same transaction) and commit.
  3. After a successful commit, ack the message to the queue.
  4. 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.

دیاگرام ۸ · Lifecycle — وضعیت‌های participant در 2PCDiagram 8 · Lifecycle — participant states in 2PC باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از دیاگرام ۸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.

نقشهٔ فصل ۸ — تراکنش‌ها در یک نگاهChapter 8 map — transactions at a glance

نقشهٔ فصل ۸ · گرهٔ وسط تراکنش است، یال‌ها قول‌ها، raceها و راه‌حل‌هاChapter 8 map · transactions at the center, promises, races and fixes on the edges باز کردن در صفحهٔ جدا ↗Open standalone ↗
روایت مدرس از نقشهٔ فصل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.

6 بستهٔ مصاحبهInterview pack

۶.۱ همه‌چیز در یک نگاه6.1 Everything at a glance

اصطلاحتعریف یک‌خطیکلیدواژهٔ مصاحبه
Transactionگروه‌بندیِ چند خواندن/نوشتن در یک واحدِ منطقیِ همه‌یا-هیچکاهشِ حالت‌های خطایی که باید به آن فکر کنی
ACIDAtomicity, Consistency, Isolation, Durabilityبیشتر یک واژهٔ بازاریابی؛ جزئیاتِ هر دیتابیس فرق دارد
Atomicity (ACID)همه یا هیچ؛ abort یعنی برگرداندنِ همهٔ نوشتن‌هابهتر بود نامش abortability باشد
Consistency (ACID)حفظِ invariantهای خودِ برنامهمسئولیتِ مشترکِ برنامه و دیتابیس؛ با معانیِ دیگرِ consistency اشتباه نگیر
Isolationجداسازیِ تراکنش‌های هم‌زمان؛ تعریفِ کلاسیک = serializabilityدر عمل اغلب سطوحِ ضعیف‌تر
Durabilityدادهٔ committed از بین نمی‌رودfsync و WAL یا تکثیر؛ ضمانتِ مطلق وجود ندارد
BASEBasically Available, Soft state, Eventual consistencyتنها تعریفِ معقول: not ACID
Multi-Object Transactionچند شیء (ردیف/سند) در یک تراکنشبرای foreign key، denormalization و ایندکس‌های ثانویه
Dirty Readخواندنِ نوشتنِ هنوز-commit-نشدهread committed و بالاتر جلویش را می‌گیرند
Dirty Writeبازنویسیِ نوشتنِ هنوز-commit-نشدهتقریباً همه جلویش را می‌گیرند (قفلِ ردیف)
Read Skewدیدنِ بخش‌هایی از دیتابیس در لحظه‌های متفاوتnonrepeatable read؛ درمان = snapshot isolation
Snapshot Isolationهر تراکنش از snapshotِ لحظهٔ شروع می‌خواندreaders never block writers
MVCCنگه‌داشتنِ چند نسخهٔ committed از هر ردیفinserted_by / deleted_by؛ قواعدِ visibility
Lost Updateپوشاندنِ نوشتنِ دیگری در چرخهٔ read-modify-write۴ درمان: اتمی، قفل، تشخیص+retry، CAS
Write Skewتصمیم بر اساسِ دادهٔ کهنه، با نوشتنِ شیءِ دیگرتعمیمِ lost update؛ فقط serializability جلویش را می‌گیرد
Phantomنوشتنِ تراکنشی، نتیجهٔ جست‌وجوی تراکنشِ دیگر را عوض می‌کندSELECT FOR UPDATE روی ردیفِ ناموجود بی‌فایده است
Materializing Conflictsساختنِ ردیفِ قفل‌شدنیِ مصنوعی برای phantomآخرین راه‌حل؛ کثیف و خطاآفرین
Serializabilityنتیجه برابرِ اجرای سریالیِ تراکنش‌هاقوی‌ترین isolation؛ جلوی همهٔ آنومالی‌ها
Stored Procedureتحویلِ کلِ کدِ تراکنش به دیتابیس از قبلشرطِ اجرای سریالیِ تک‌threadه؛ باید deterministic باشد برای SMR
Two-Phase Lockingقفلِ shared/exclusive با فازِ رشد و کاهش2PL ≠ 2PC؛ deadlock و latency ناپایدار
Predicate Lockقفل روی همهٔ اشیای منطبق با شرط — حتی آیندهدقیق اما کُند
Index-Range Lockتقریبِ ارزانِ predicate lock روی بازهٔ ایندکسnext-key locking؛ بدونِ ایندکس → قفلِ کلِ جدول
SSIخوش‌بینانه: بررسیِ تضاد در لحظهٔ commitPostgreSQL، CockroachDB، FoundationDB
Atomic Commitیا همهٔ nodeها commit یا همه abortمسالهٔ اصلیِ تراکنشِ توزیع‌شده
2PCprepare → vote → commit point → commitblocking؛ دو نقطهٔ بازگشت‌ناپذیر
In-Doubtوضعیتِ participant پس از رایِ yes و بدونِ خبرِ coordinatorقفل‌ها تا تعیینِ تکلیف نگه داشته می‌شوند
XAاستانداردِ 2PC روی سیستم‌های ناهمگنcoordinator داخلِ process برنامه = تک‌نقطهٔ خرابی
Heuristic Decisionتصمیمِ یک‌جانبهٔ participant در حالتِ بحراناحتمالاً atomicity می‌شکند
Exactly-Onceاثرِ هر پیام دقیقاً یک‌بار اعمال شودبا idempotency و بدونِ 2PC هم ممکن است
Idempotencyتکرارِ عمل، اثرِ تکراری نسازدmessage_id یکتا در دیتابیس
TermOne-line definitionInterview keyword
TransactionGrouping several reads/writes into one all-or-nothing logical unitShrinks the failure states to think about
ACIDAtomicity, Consistency, Isolation, DurabilityMostly a marketing word; details differ per database
Atomicity (ACID)All or nothing; abort rolls back every writeBetter named abortability
Consistency (ACID)Preserving the application's own invariantsShared app/database duty; don't confuse with other consistencies
IsolationSeparating concurrent transactions; classic definition = serializabilityOften weaker levels in practice
DurabilityCommitted data is never lostfsync and WAL or replicas; no absolute guarantee
BASEBasically Available, Soft state, Eventual consistencyOnly sane definition: not ACID
Multi-Object TransactionSeveral objects (rows/documents) in one transactionFor foreign keys, denormalization, secondary indexes
Dirty ReadReading a not-yet-committed writeRead committed and above stop it
Dirty WriteOverwriting a not-yet-committed writeNearly everyone stops it (row lock)
Read SkewSeeing parts of the database at different momentsNonrepeatable read; cure = snapshot isolation
Snapshot IsolationEach transaction reads the snapshot of its startReaders never block writers
MVCCKeeping several committed versions of each rowinserted_by / deleted_by; visibility rules
Lost UpdateCovering another's write in a read-modify-write cycle4 cures: atomic, lock, detect+retry, CAS
Write SkewDeciding on stale data, writing a different objectGeneralized lost update; only serializability stops it
PhantomOne transaction's write changes another's search resultSELECT FOR UPDATE is useless on nonexistent rows
Materializing ConflictsBuilding artificial lockable rows for phantomsLast resort; dirty and error-prone
SerializabilityOutcome equals serial execution of the transactionsStrongest isolation; stops every anomaly
Stored ProcedureDelivering the whole transaction code upfrontSerial single-threaded condition; must be deterministic for SMR
Two-Phase LockingShared/exclusive locks with growth and shrinkage2PL ≠ 2PC; deadlocks and unstable latency
Predicate LockLocking every object matching a condition — even future onesPrecise but slow
Index-Range LockCheap approximation of predicate locks on index rangesNext-key locking; no index → whole-table lock
SSIOptimistic: conflict checks at commit timePostgreSQL, CockroachDB, FoundationDB
Atomic CommitEither all nodes commit or all abortThe core distributed-transaction problem
2PCprepare → vote → commit point → commitBlocking; two points of no return
In-DoubtParticipant state after voting yes with no coordinator newsLocks held till decided
XAThe 2PC standard across heterogeneous systemsIn-process coordinator = single point of failure
Heuristic DecisionParticipant's unilateral decision in crisisProbably breaks atomicity
Exactly-OnceEach message's effect applies exactly oncePossible with idempotency, no 2PC
IdempotencyRepeating an act creates no repeated effectUnique message_id in the database

۶.۲ سوالات پرتکرار مصاحبه (برای دیدن جواب کلیک کن)6.2 Frequent interview questions (click for answers)

۱. atomicity در ACID با «عملیاتِ اتمیِ چندریته‌ای» چه فرقی دارد؟1. How does ACID atomicity differ from “thread-level atomic”?

اتمی بودنِ عملیات در هم‌زمانی یعنی دیگران تغییر را نیمه‌کاره نبینند. اتمی بودنِ تراکنش در ACID یعنی مجموعهٔ تغییرها در صورتِ شکست، همه‌یا‌هیچ باشد؛ جداسازی موضوعِ جداست.

مثال فنی: Rollback بعد از خطای درج

وضعیت اولیه: موجودی P7=10 است؛ سفارش ۴۲ هنوز ثبت نشده است.

اجرای مرحله‌به‌مرحله: داخل BEGIN موجودی به ۹ کم می‌شود؛ درج سفارش خطا می‌دهد و ROLLBACK مقدار را به ۱۰ برمی‌گرداند.

نتیجه و دلیل: atomicity اثر ناقص را حذف می‌کند؛ پیامک بیرون پایگاه داده با rollback خودکار پس گرفته نمی‌شود.

Thread-level atomic means no other thread sees a half-done result — a concurrency matter. In ACID, atomicity has nothing to do with concurrency (that's isolation); it means a multi-write failure aborts the transaction and everything is rolled back. A better name would have been abortability.

۲. چرا حرفِ C در ACID با بقیه فرق دارد؟2. Why is the C in ACID unlike the rest?

C یعنی قواعدِ معتبرِ برنامه برقرار بمانند. موتور بعضی قواعدِ اعلام‌شده را اجرا می‌کند، اما منطقِ لازم و سطحِ جداسازی را طراح باید انتخاب کند؛ این معنا با سازگاریِ replica یکی نیست.

مثال فنی: C به منطق دامنه وابسته است

وضعیت اولیه: برنامه مبلغ را در هر دو حساب درست و اتمیک ثبت می‌کند، اما اشتباهاً از A صد واحد کم و به B دویست واحد اضافه می‌کند.

اجرای مرحله‌به‌مرحله: پایگاه داده می‌تواند همین دستورات غلط را بدون خرابی و با isolation قوی commit کند، مگر قید مناسب مانع شود.

نتیجه و دلیل: ACID فرمول درست انتقال را اختراع نمی‌کند؛ قانون بقای مبلغ باید در منطق و قیدهای مناسب طراحی شود.

Because the database alone can't guarantee consistency: your application's invariants (like balanced debits and credits) must hold. Some is declarable via constraints (foreign key, unique, check); the rest (like “at least one doctor on call”) is the code's job. And the word has five different database meanings that mustn't be mixed.

۳. dirty read و dirty write چیستند و چرا جلویشان را می‌گیرند؟3. What are dirty reads and dirty writes, and why stop them?

Dirty read دیدنِ تغییرِ هنوز تأییدنشده است؛ dirty write بازنویسیِ تغییرِ تأییدنشدهٔ دیگری است. اگر تراکنشِ اول لغو شود، تصمیم یا بازیابیِ دیگری می‌تواند نادرست شود.

مثال فنی: Dirty read و dirty write روی همان قیمت

وضعیت اولیه: T1 قیمت P7 را از ۱۰۰ به ۱۲۰ تغییر داده، ولی هنوز commit نکرده است.

اجرای مرحله‌به‌مرحله: T2 اگر ۱۲۰ را بخواند dirty read است؛ اگر پیش از تصمیم T1 روی همان مقدار بنویسد dirty write است. rollbackِ T1 نباید دادهٔ تأییدنشده را مبنای تصمیم یا بازنویسیِ نامشخص بگذارد.

نتیجه و دلیل: Read Committed خواندنِ دادهٔ commitنشدهٔ تراکنش دیگر را منع می‌کند؛ کنترل تعارض نوشتن هم باید مانع بازنویسیِ تغییرِ تأییدنشده شود.

A dirty read reads data whose writer hasn't committed; if that transaction aborts, the reader saw something that never existed. A dirty write overwrites an uncommitted value, producing odd results (like a wrong car-sale invoice). Read committed stops both: the first by keeping two versions, the second with a row lock.

۴. snapshot isolation چه ضمانتی می‌دهد و چگونه با MVCC پیاده می‌شود؟4. What does snapshot isolation guarantee, and how does MVCC implement it?

جداسازیِ مبتنی بر تصویر، خواندن را به تصویرِ سازگارِ تراکنش وصل می‌کند. MVCC چند نسخه نگه می‌دارد و قواعدِ رؤیت تعیین می‌کنند کدام نسخه دیده شود؛ جزئیاتِ نسخه‌ها به موتور وابسته است.

مثال فنی: گزارش ثابت با MVCC

وضعیت اولیه: پیش از انتقال، موجودی حساب A برابر ۱۰۰ و B برابر ۰ است؛ یک تراکنش ۲۰ واحد جابه‌جا می‌کند.

اجرای مرحله‌به‌مرحله: گزارش روی snapshot قبل از انتقال، A=100 و B=0 را می‌بیند؛ گزارش روی snapshot بعد، A=80 و B=20 را می‌بیند.

نتیجه و دلیل: snapshotِ درست نصفِ قبل و نصفِ بعد را مخلوط نمی‌کند؛ MVCC نسخه‌های لازم برای خوانندهٔ قدیمی را نگه می‌دارد.

Each transaction sees the whole database as of its start; concurrent changes by others stay invisible. Implementation: every write creates a new version stamped inserted_by/deleted_by (update = delete+insert), versions form chains, and reads show only versions whose inserter committed before our start and whose deleter didn't. Rule: readers never block writers.

۵. چرا «repeatable read» در دیتابیس‌های مختلف معانی متفاوتی دارد؟5. Why does “repeatable read” mean different things in different databases?

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

مثال فنی: نام یکسان، قرارداد را بخوان

وضعیت اولیه: T1 در PostgreSQL Repeatable Read یک query می‌زند؛ T2 ردیف تازهٔ مطابق شرط را درج و commit می‌کند.

اجرای مرحله‌به‌مرحله: تکرار query در snapshot T1 ردیف تازه را نشان نمی‌دهد، اما write skew میان دو تراکنش هنوز ممکن است.

نتیجه و دلیل: از اسم Repeatable Read به‌تنهایی تضمین همهٔ موتور‌ها را حدس نمی‌زنیم؛ جلوگیری از phantom لزوماً serializable بودن نیست.

Because the SQL standard doesn't know snapshot isolation and its repeatable-read definition is vague and implementation-dependent. PostgreSQL names snapshot isolation that way, MySQL/InnoDB a weaker MVCC variant, IBM Db2 serializability. So never trust the name — ask about behavior.

۶. lost update چیست و چهار راهکارِ مقابله با آن کدامند؟6. What is a lost update, and what are the four remedies?

Lost update یعنی دو کار از مقدارِ قدیمی شروع کنند و یکی تغییرِ دیگری را بپوشاند. چهار راه: دستورِ اتمیک، قفلِ مناسب، تشخیصِ تعارض و تکرار، یا نوشتنِ شرطی با نسخه.

مثال فنی: دو کاهش، فقط یکی ثبت می‌شود

وضعیت اولیه: موجودی ۱۰ است؛ T1 و T2 هر دو ۱۰ را می‌خوانند و در برنامه ۹ حساب می‌کنند.

اجرای مرحله‌به‌مرحله: T1 مقدار ۹ را می‌نویسد و سپس T2 هم ۹ می‌نویسد؛ نتیجه ۹ است، ولی بعد از دو فروش باید ۸ باشد.

نتیجه و دلیل: SET qty=qty-1 با شرط مناسب، قفلِ read-modify-write یا CAS راه‌های کنترل‌اند؛ نوشتن مقدار ثابتِ محاسبه‌شده از خواندن قدیمی علت خطاست.

A read-modify-write cycle where the second write covers the first. Remedies: (1) built-in atomic ops like UPDATE … SET v = v+1, (2) explicit locking with SELECT FOR UPDATE, (3) automatic conflict detection by the database plus app retry (PostgreSQL/Oracle/SQL Server yes, MySQL/InnoDB no), (4) conditional writes or a version column (optimistic locking), handy in transaction-less systems.

۷. write skew چیست و چرا از lost update سخت‌تر است؟7. What is write skew, and why is it harder than lost update?

Write skew شرطِ مشترکی را با تغییرِ ردیف‌های جدا می‌شکند؛ بنابراین تشخیصِ بازنویسیِ همان ردیف کافی نیست. قفلِ همهٔ دادهٔ مؤثر در تصمیم یا جداسازیِ سریال‌پذیر می‌تواند کمک کند.

مثال فنی: دو پزشک، دو ردیف، یک قانون

وضعیت اولیه: 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 یا قفلِ منبع مشترکِ قانون می‌تواند از پذیرش این نتیجه جلوگیری کند.

A transaction decides based on reading one condition and writes a different object; its write invalidates the decision's premise — and since the two transactions change different objects, it's neither dirty write nor lost update. Cheap cures (atomic ops, auto-detection) fail; either lock the rows the decision depends on, or go serializable.

۸. phantom چیست و چرا SELECT FOR UPDATE در مثالِ رزرو اتاق کار نمی‌کند؟8. What is a phantom, and why does SELECT FOR UPDATE fail in the room-booking example?

Phantom به تغییرِ مجموعهٔ مطابقِ شرط مربوط است. قفلِ ردیف‌های برگشتی لزوماً از درجِ ردیفِ تازه جلوگیری نمی‌کند؛ در جست‌وجوی خالی ممکن است ردیفی برای قفل نباشد.

مثال فنی: رزروِ بازه‌ای که هنوز ردیف ندارد

وضعیت اولیه: برای اتاق ۷ و بازهٔ [10:00,11:00) هیچ رزروی نیست.

اجرای مرحله‌به‌مرحله: T1 و T2 هر دو queryِ هم‌پوشانی را خالی می‌بینند و هرکدام ردیف تازه درج می‌کنند؛ FOR UPDATE روی نتیجهٔ خالی ردیفی برای قفل ندارد.

نتیجه و دلیل: قید جلوگیری از overlap، Serializable یا قفلِ ردیف مشترک اتاق لازم است؛ unique سادهٔ ساعت شروع برای همهٔ هم‌پوشانی‌ها کافی نیست.

A phantom means one transaction's write changes another's search result. In room booking the check is “no overlapping booking exists” and the write creates a new row; when SELECT returns nothing, FOR UPDATE has nothing to lock. Snapshot isolation stops phantoms for pure reads, not writing transactions. Fixes: serializability or materializing conflicts.

۹. materializing conflicts یعنی چه و چرا آخرین راه‌حل است؟9. What does materializing conflicts mean, and why is it the last resort?

Materializing conflicts یعنی یک رکوردِ مشترکِ قابل‌قفل بسازیم تا تصمیم‌های متعارض بر آن هماهنگ شوند. پیدا کردنِ دامنهٔ درستِ این رکورد سخت است و کنترلِ هم‌زمانی را واردِ مدلِ داده می‌کند.

مثال فنی: از شرط به ردیف قفل‌پذیر

وضعیت اولیه: query رزرو اتاق خالی برمی‌گردد و ردیفی برای FOR UPDATE ندارد.

اجرای مرحله‌به‌مرحله: یک ردیف پایدار برای خودِ اتاق ۷ از قبل داریم؛ همهٔ رزروکننده‌ها پیش از بررسی بازه، آن ردیف را قفل می‌کنند.

نتیجه و دلیل: رقابت روی شرط به رقابت روی یک شیء موجود تبدیل می‌شود؛ رزروهای مستقل همان اتاق هم ممکن است بی‌دلیل سریالی شوند.

Building artificial rows purely to lock: e.g. a pre-filled “room × 15-minute slot” table whose row the booking transaction locks first. Problem: finding the right design is hard and error-prone, leaking concurrency control into the data model; if serializability is available, prefer it.

۱۰. سه روشِ پیاده‌سازیِ serializability را مقایسه کن.10. Compare the three serializability implementations.

اجرای سریالی کارها را پشت‌سرهم می‌کند؛ 2PL با قفل منتظر می‌ماند؛ SSI با تصویر و بررسیِ وابستگی، اجرای ناسازگار را لغو می‌کند. ظرفیت، انتظار، بن‌بست و نرخِ تکرار هزینه‌های متفاوت‌اند.

مثال فنی: نتیجهٔ معادل اجرای یکی‌یکی

وضعیت اولیه: موجودی یک واحد است؛ دو تراکنش هرکدام «اگر موجودی مثبت بود، یک سفارش بساز» را اجرا می‌کنند.

اجرای مرحله‌به‌مرحله: در اجرای serial، اولی موجودی را صفر می‌کند و دومی دیگر سفارش نمی‌سازد؛ نتیجهٔ concurrentِ serializable باید با یک ترتیب serial سازگار باشد.

نتیجه و دلیل: دو سفارش موفق با موجودی منفی معادل این منطقِ serial نیست؛ ممکن است لازم شود یکی abort و از ابتدا retry شود.

Serial execution: kills concurrency with one thread plus stored procedures; simple and fast (in-memory data) but capped at one core and sensitive to slow transactions. 2PL: shared/exclusive locks in two phases; serializable but with waiting, deadlocks and unstable latency. SSI: optimistic over snapshot isolation with commit-time conflict checks; no blocking, but aborts under contention and bookkeeping overhead.

۱۱. «دو فازِ» قفلِ دوفازی یعنی چه و 2PL با 2PC چه فرقی دارد؟11. What do the “two phases” of two-phase locking mean, and how does 2PL differ from 2PC?

در 2PL ابتدا قفل‌ها گرفته می‌شوند و پس از شروعِ آزادسازی، قفلِ تازه گرفته نمی‌شود؛ در نوعِ سخت‌گیرانه قفل‌های مهم تا پایان نگه داشته می‌شوند. 2PC دربارهٔ تصمیمِ تأییدِ چند شرکت‌کننده است.

مثال فنی: دو فازِ گرفتن و آزاد کردن قفل

وضعیت اولیه: در مدل 2PL، T1 ابتدا قفل‌های موردنیازش را می‌گیرد.

اجرای مرحله‌به‌مرحله: پس از شروع آزادسازی، قفل تازه نمی‌گیرد؛ در strict 2PL دست‌کم قفل‌های نوشتن تا پایان نگه داشته می‌شوند.

نتیجه و دلیل: این دو فاز مربوط به isolation هستند؛ prepare/commit در 2PC مسئلهٔ تصمیم اتمیک بین منابع را حل می‌کنند.

Phase one (growth) takes locks during execution; phase two (shrinkage) releases them all together at the end, and after the first release no new lock may be taken. It has nothing to do with two-phase commit (2PC): 2PL is an isolation mechanism, 2PC an atomic-commit protocol for distributed databases.

۱۲. predicate lock و index-range lock چه می‌کنند؟12. What do predicate locks and index-range locks do?

قفلِ شرط، دامنهٔ رکوردهای مطابقِ یک شرط، از جمله درجِ آینده، را پوشش می‌دهد. قفلِ بازهٔ ایندکس تقریبِ اجراییِ آن است و ممکن است دامنهٔ بیشتری از حدِ لازم را محدود کند.

مثال فنی: قفل روی شرط جست‌وجو

وضعیت اولیه: T1 رزروهای اتاق ۷ در بازهٔ ۱۰ تا ۱۱ را می‌خواند و نتیجه خالی است.

اجرای مرحله‌به‌مرحله: predicate lock منطقی، همین شرط را پوشش می‌دهد؛ index-range lock بازهٔ متناظرِ ایندکس را پوشش می‌دهد و ممکن است گسترده‌تر باشد. درجِ رقیب در آن دامنه باید طبق روش isolation کنترل شود.

نتیجه و دلیل: در روش قفلی، درج می‌تواند منتظر بماند؛ در PostgreSQL SSI، predicate tracking برای تشخیص وابستگی و abort استفاده می‌شود و همان قفلِ مسدودکنندهٔ درج نیست.

A predicate lock conceptually locks every object matching the search condition — even future ones — fully stopping phantoms but with slow lock matching. Index-range (next-key) locking is its cheap approximation: a shared lock on the covering index entry or range; sometimes over-locks, but cheap. No suitable index means a whole-table lock.

۱۳. SSI چگونه تضادها را تشخیص می‌دهد و چرا فوراً abort نمی‌کند؟13. How does SSI detect conflicts, and why not abort immediately?

SSI وابستگیِ خواندن و نوشتنِ تراکنش‌ها را دنبال می‌کند و الگوهای ناسازگار را تشخیص می‌دهد. همهٔ خواندن‌های قدیمی خطا نیستند؛ لغوِ فوریِ هرکدام هزینهٔ تکرارِ بی‌لازم می‌سازد.

مثال فنی: SSI کدام وابستگی را می‌بیند؟

وضعیت اولیه: T1 خوانده که B کشیک است و T2 خوانده که A کشیک است.

اجرای مرحله‌به‌مرحله: T1، A را حذف از کشیک می‌کند و T2، B را؛ SSI وابستگی‌های read-write را دنبال می‌کند و در الگوی خطرناک ممکن است یکی را abort کند.

نتیجه و دلیل: خواندن snapshot ادامه دارد، اما برنامه باید serialization failure و retry کل تراکنش را مدیریت کند؛ SSI همان قفل‌گذاریِ مسدودکنندهٔ همهٔ خواندن‌ها نیست.

It tracks two things: (1) reads of versions ignored via MVCC rules whose owner has since committed; (2) later writes that stale another transaction's earlier read — via an index-range-like tripwire technique, without blocking. Delayed abort is deliberate: read-only transactions need no abort, and the other write may still be uncommitted; long reads stay safe that way.

۱۴. چرا stored procedure برای اجرای سریالی لازم است؟14. Why are stored procedures required for serial execution?

تحویلِ کلِ کار در یک فراخوانی، انتظارِ رفت‌وبرگشتِ شبکه را از مسیرِ اجرای سریالی کم می‌کند. Stored procedure یکی از راه‌هاست؛ اصل، کوتاه و غیرتعاملی بودنِ کار است.

مثال فنی: کار کوتاه روی مسیر سریالی

وضعیت اولیه: procedure رزرو، موجودی را بررسی، کم و سفارش را ثبت می‌کند.

اجرای مرحله‌به‌مرحله: اجرای آن مثلاً ۲ میلی‌ثانیه است؛ منتظر ماندن ۳۰ثانیه‌ای برای ورودی کاربر داخل همان مسیر، نوبت بقیه را متوقف می‌کند.

نتیجه و دلیل: اجرای سریالی به تراکنش‌های کوتاه و دادهٔ در دسترس نیاز دارد؛ فرم یا RPC کند نباید حلقهٔ اصلی را معطل کند.

In the interactive model each statement round-trips between app and database, burning most time on the network; with one transaction at a time, throughput collapses. Stored procedures deliver the whole transaction upfront for I/O-free in-memory execution. Modern versions use general languages (Java, Lua, JavaScript), and state-machine replication demands deterministic procedures.

۱۵. مسالهٔ atomic commit چیست و چرا «هر node خودش commit کند» جواب نمی‌دهد؟15. What is the atomic-commit problem, and why is “each node commits itself” not enough?

Commit اتمیک می‌خواهد همهٔ شرکت‌کننده‌ها تصمیمِ مشترک داشته باشند. دستورِ مستقل به هر گره کافی نیست، چون بعضی ممکن است موفق و بعضی ناموفق شوند.

مثال فنی: 2PC روی دو منبع

وضعیت اولیه: ثبت رزرو در پایگاه A و کم کردن ظرفیت در پایگاه B باید تصمیم مشترک داشته باشد.

اجرای مرحله‌به‌مرحله: coordinator از هر دو prepare می‌خواهد؛ پس از رأی مثبتِ همه، تصمیم commit را پایدار و ارسال می‌کند؛ رأی منفی پیش از تصمیم نهایی به abort می‌انجامد.

نتیجه و دلیل: هر participant نمی‌تواند مستقلاً با timeout تصمیم دلخواه بگیرد؛ شرط، پشتیبانی واقعی منابع از پروتکل است.

A multi-node transaction must commit everywhere or abort everywhere. Asking all nodes to commit directly isn't enough: some may succeed while others fail (constraint violations, lost network messages, crashes before the commit record). Worse: data committed on one node and read by others can never be taken back — permanent divergence.

۱۶. 2PC چگونه کار می‌کند و «دو نقطهٔ بازگشت‌ناپذیر» کدامند؟16. How does 2PC work, and which are the “two points of no return”?

در 2PC، آماده‌سازی و رأی انجام می‌شود؛ سپس هماهنگ‌کننده تصمیم را پایدار و اعلام می‌کند. رأیِ مثبت حقِ لغوِ مستقل را محدود می‌کند و ثبتِ تصمیمِ نهایی، مسیرِ نتیجه را تثبیت می‌کند.

مثال فنی: 2PC روی دو منبع

وضعیت اولیه: ثبت رزرو در پایگاه A و کم کردن ظرفیت در پایگاه B باید تصمیم مشترک داشته باشد.

اجرای مرحله‌به‌مرحله: coordinator از هر دو prepare می‌خواهد؛ پس از رأی مثبتِ همه، تصمیم commit را پایدار و ارسال می‌کند؛ رأی منفی پیش از تصمیم نهایی به abort می‌انجامد.

نتیجه و دلیل: هر participant نمی‌تواند مستقلاً با timeout تصمیم دلخواه بگیرد؛ شرط، پشتیبانی واقعی منابع از پروتکل است.

Phase one: the coordinator sends PREPARE to all participants; each durably writes its data and, if it can definitely commit, votes YES (first point: surrendering the abort right). Phase two: if all voted yes, the coordinator writes the commit decision in its own log (second point: the commit point) then announces it; any no (or timeout) aborts everyone. After deciding, announcement must be retried to success.

۱۷. وضعیتِ in-doubt چیست و چرا timeout کمکی نمی‌کند؟17. What is the in-doubt state, and why doesn’t a timeout help?

شرکت‌کننده رأیِ مثبت داده ولی تصمیم را نمی‌داند. Timeout ثابت نمی‌کند بقیه لغو کرده‌اند؛ تصمیمِ مستقل می‌تواند با نتیجهٔ طرفِ دیگر اختلاف بسازد.

مثال فنی: Prepared و بی‌خبر از تصمیم

وضعیت اولیه: participant A آماده شده و تعهد داده نتیجهٔ coordinator را اجرا کند؛ ارتباط با coordinator قطع می‌شود.

اجرای مرحله‌به‌مرحله: A ممکن است قفل‌ها و وضعیت پایدار را نگه دارد تا تصمیم معتبر بازیابی شود.

نتیجه و دلیل: timeout نمی‌گوید coordinator commit کرده یا abort؛ آزاد کردن خودسرانهٔ تعهد می‌تواند atomic commit را بشکند.

After voting YES, a dead coordinator or cut network leaves a participant not knowing whether the transaction committed — it just waits. Unilateral abort after a timeout could diverge from another participant that committed; unilateral commit is unsafe likewise. Meanwhile the transaction's locks stay held, freezing part of the app; if the coordinator's log is gone, only a sysadmin's manual call remains.

۱۸. چرا 2PC را blocking می‌نامند و 3PC چرا جواب نداد؟18. Why is 2PC called blocking, and why didn’t 3PC fix it?

2PC ممکن است منابع را تا روشن شدنِ تصمیم نگه دارد. 3PC برای پیشرفت به فرض‌های زمانی و شبکه‌ایِ قوی‌تر نیاز دارد؛ قطعی و مکثِ واقعی می‌توانند آن فرض‌ها را بشکنند.

مثال فنی: Prepared و بی‌خبر از تصمیم

وضعیت اولیه: participant A آماده شده و تعهد داده نتیجهٔ coordinator را اجرا کند؛ ارتباط با coordinator قطع می‌شود.

اجرای مرحله‌به‌مرحله: A ممکن است قفل‌ها و وضعیت پایدار را نگه دارد تا تصمیم معتبر بازیابی شود.

نتیجه و دلیل: timeout نمی‌گوید coordinator commit کرده یا abort؛ آزاد کردن خودسرانهٔ تعهد می‌تواند atomic commit را بشکند.

Blocking means the whole system may stall waiting for one node's recovery. 3PC was designed nonblocking but assumes bounded network delay and node response times; in real systems (unbounded delay, process pauses) it can't guarantee atomicity. The practical fix: replacing the single-node coordinator with a replicated consensus protocol — with automatic failover.

۱۹. XA چیست و بزرگ‌ترین مشکلاتش کدامند؟19. What is XA, and what are its biggest problems?

XA قراردادی برای هماهنگیِ تراکنش میان منابعِ پشتیبانِ آن است. پایداریِ هماهنگ‌کننده، بازیابیِ وضعیتِ آماده و تفاوتِ تضمین‌های منابع، مدیریتِ آن را دشوار می‌کنند.

مثال فنی: XA مرز کدام منابع را می‌گیرد؟

وضعیت اولیه: یک transaction manager می‌خواهد پایگاه داده و brokerِ پشتیبان XA را هماهنگ کند.

اجرای مرحله‌به‌مرحله: هر دو به‌عنوان resource manager در prepare/commit شرکت می‌کنند؛ API عادیِ یک سرویس وب خودبه‌خود participant XA نیست.

نتیجه و دلیل: مرز پروتکل و بازیابیِ coordinator و اتصال‌ها باید پشتیبانی شود؛ ادعای تراکنش سراسری برای هر API درست نیست.

A standard (since 1991) for 2PC across heterogeneous systems — e.g. a database and a message broker from different vendors — implemented in Java via JTA. Problems: the coordinator is usually an in-process library, and its log becomes part of the system's durable state; an app crash loses the coordinator and strands participants; all communication flows through app code (so the app is a single point of failure); and as the lowest common denominator, XA detects no cross-system deadlocks and doesn't work with SSI.

۲۰. تراکنشِ توزیع‌شدهٔ داخلیِ دیتابیس‌های NewSQL چه فرقی با XA دارد؟20. How do NewSQL internal distributed transactions differ from XA?

داخلِ یک محصول، هماهنگ‌کننده و شاردها می‌توانند با پروتکلِ واحد، تکثیر، بازیابی و کنترلِ هم‌زمانیِ یکپارچه طراحی شوند. در XA منابعِ مستقل باید روی قراردادِ مشترک کار کنند؛ تضمینِ دقیق همچنان محصول‌محور است.

مثال فنی: تراکنش داخلیِ چندشاردی

وضعیت اولیه: دو shard از یک پایگاه دادهٔ توزیع‌شده باید یک انتقال را commit کنند.

اجرای مرحله‌به‌مرحله: موتور می‌تواند هماهنگی تراکنش را با replication و بازیابیِ shardهای خودش یکپارچه کند.

نتیجه و دلیل: این با هماهنگ کردن دو محصول مستقل از راه XA یکسان نیست؛ تضمین و مسیر شکست باید از همان موتور خوانده شود.

All nodes run identical software, so no lowest-common-denominator: the coordinator is consensus-replicated with automatic failover, talks to shards directly, shards are replicated too, and atomic commit pairs with distributed concurrency control (deadlock detection, cross-shard consistent reads). Multi-shard snapshot isolation or SSI becomes possible — e.g. CockroachDB, TiDB, Spanner, FoundationDB, YugabyteDB.

۲۱. چگونه بدونِ 2PC به exactly-once می‌رسیم؟21. How do we reach exactly-once without 2PC?

شناسهٔ پیام و اثرِ آن را با قیدِ یکتا در یک تراکنش ثبت می‌کنیم و پس از commit تأییدِ صف می‌فرستیم. تکرار، اثرِ تازه نمی‌سازد؛ شناسه تا پایانِ دورهٔ ممکنِ بازتلاش یا بازپخش باید حفظ شود.

مثال فنی: تکرار پیام، یک اثر

وضعیت اولیه: پیام event_id=e7 باید موجودی را یک واحد کم کند.

اجرای مرحله‌به‌مرحله: داخل یک تراکنش، شناسه در جدول با قید unique درج و موجودی کم می‌شود؛ تحویل دوبارهٔ e7 از قید عبور نمی‌کند و کاهش دوباره انجام نمی‌شود.

نتیجه و دلیل: ثبت شناسه و اثر باید اتمیک باشند؛ شناسه تا پایان افق retry/replay حفظ می‌شود و اثر بیرونیِ جدا قرارداد دیگری می‌خواهد.

With idempotency: each message carries a unique id, and a “processed messages” table puts the id insert and the processing writes in one transaction; after commit, the message is acked. A repeated message finds its id and only acks. Processing is idempotent — repeats create no duplicate effect; the unique constraint also stops races between two simultaneous retries. This pattern underlies exactly-once in Kafka Streams.

۲۲. «ACID» یعنی برای دادهٔ مالی همه‌چیز درست است؟22. Does “ACID” mean everything is fine for financial data?

نه؛ باید قواعدِ کسب‌وکار، سطحِ جداسازی، دوام، بازتلاش و اثرهای بیرونی روشن باشند. ACID نامِ مجموعه‌ای از موضوع‌هاست و طراحیِ درستِ کلِ برنامه را به‌تنهایی ثابت نمی‌کند.

مثال فنی: C به منطق دامنه وابسته است

وضعیت اولیه: برنامه مبلغ را در هر دو حساب درست و اتمیک ثبت می‌کند، اما اشتباهاً از A صد واحد کم و به B دویست واحد اضافه می‌کند.

اجرای مرحله‌به‌مرحله: پایگاه داده می‌تواند همین دستورات غلط را بدون خرابی و با isolation قوی commit کند، مگر قید مناسب مانع شود.

نتیجه و دلیل: ACID فرمول درست انتقال را اختراع نمی‌کند؛ قانون بقای مبلغ باید در منطق و قیدهای مناسب طراحی شود.

No! Even relational “ACID” databases usually run weak isolation, so lost updates or write skew can still strike. You must know exactly which isolation level is active, which anomalies remain possible, and what countermeasure (locks, constraints, serializability) you have. And in some domains (like banking) audit and human oversight matter more than ACID itself.

6.3 اشتباهات رایج در جواب‌های مصاحبهCommon interview mistakes

  • یکی دانستنِ 2PL و 2PC — یکی isolation است و دیگری atomic commit.
  • تعریفِ atomicityِ ACID مثلِ «اتمی بودنِ چندریته‌ای» — آن مالِ isolation است؛ atomicity یعنی همه‌یا-هیچ با abort.
  • گفتنِ «consistency یعنی سازگاریِ replicaها» در تعریفِ ACID — در ACID یعنی invariantهای خودِ برنامه.
  • اینکه «هر دیتابیسِ ACID جلوی همهٔ race conditionها را می‌گیرد»؛ در عمل isolation معمولاً ضعیف است.
  • پیشنهادِ «از ACID استفاده کن» برای مشکلِ مالی، بدونِ پرسیدنِ سطحِ isolation.
  • فراموش کردنِ اینکه lost update با نوشتنِ بعد از commit هم رخ می‌دهد و dirty write نیست.
  • گفتنِ «SELECT FOR UPDATE همیشه write skew را حل می‌کند» — در phantom، ردیفی برای قفل کردن نیست.
  • گفتنِ «snapshot isolation جلوی همهٔ آنومالی‌ها را می‌گیرد»؛ write skew کاملاً ممکن است و lost update بستگی به پیاده‌سازی دارد.
  • یکی دانستنِ «repeatable read» بینِ دیتابیس‌های مختلف (PostgreSQL ≠ MySQL ≠ Db2).
  • یکی دانستنِ «atomic increment» با atomicityِ ACID؛ آن در اصطلاحِ چندریته‌ای اتمی است.
  • فراموش کردنِ C در ACID: اگر invariant را به‌صورتِ constraint اعلام نکنی، دیتابیس جلوی دادهٔ بد را نمی‌گیرد.
  • گفتنِ «2PC تضمین می‌کند همیشه commit شود»؛ 2PC فقط atomicity می‌دهد و می‌تواند کلاً abort کند یا گیر بیفتد.
  • ندیدنِ مسالهٔ in-doubt: participant بعد از رایِ yes قفل‌هایش را تا تعیینِ تکلیف نگه می‌دارد و timeout هم کمکی نمی‌کند.
  • گفتنِ «برای exactly-once حتماً به تراکنشِ توزیع‌شده نیاز است»؛ idempotency با تراکنشِ داخلِ دیتابیس کافی است.
  • پیشنهادِ XA برای هر سناریویِ چندسیستمی، بدونِ اشاره به تک‌نقطهٔ خرابی بودنِ coordinator و هزینه‌های عملیاتی.
  • فراموش کردنِ اینکه retryِ تراکنشِ abortشده بدونِ deduplication می‌تواند اثرِ دوباره بسازد و retry زیرِ overload وضع را بدتر می‌کند.
  • گفتنِ «SSI یعنی قفل‌های سبک‌تر»؛ SSI اصلاً قفلِ مسدودکننده ندارد و خوش‌بینانه است.
  • اینکه «اجرای سریالی یعنی دیتابیسِ کُند»؛ با stored procedure و داده‌ی در حافظه می‌تواند بسیار سریع باشد — تا وقتی تراکنش‌ها کوچک‌اند.
  • Mixing up 2PL and 2PC — one is isolation, the other atomic commit.
  • Defining ACID atomicity like “thread-level atomicity” — that belongs to isolation; atomicity means all-or-nothing with abort.
  • Saying “consistency means replica convergence” when defining ACID — in ACID it means the application's own invariants.
  • Believing “every ACID database stops all race conditions”; in practice isolation is usually weak.
  • Prescribing “use ACID” for a money problem without asking the isolation level.
  • Forgetting lost update happens with post-commit writes too — it isn't a dirty write.
  • Saying “SELECT FOR UPDATE always fixes write skew” — with phantoms there's no row to lock.
  • Saying “snapshot isolation stops every anomaly”; write skew is fully possible and lost update depends on implementation.
  • Equating “repeatable read” across databases (PostgreSQL ≠ MySQL ≠ Db2).
  • Equating “atomic increment” with ACID atomicity; that's thread-sense atomic.
  • Forgetting the C in ACID: undeclared invariants let the database store bad data.
  • Saying “2PC guarantees commit”; 2PC only gives atomicity and may abort everything or get stuck.
  • Missing in-doubt: after voting yes a participant holds its locks till decided, and timeouts don't help.
  • Saying “exactly-once needs distributed transactions”; idempotency with an in-database transaction suffices.
  • Recommending XA for every multi-system scenario, ignoring the coordinator single point of failure and operational cost.
  • Forgetting that retrying an aborted transaction without deduplication can duplicate effects, and retry under overload worsens things.
  • Saying “SSI means lighter locks”; SSI has no blocking locks at all — it's optimistic.
  • Believing “serial execution means a slow database”; with stored procedures and in-memory data it can be very fast — while transactions stay small.

۶.۴ چک‌لیست مرور ۵ دقیقه‌ای قبل از مصاحبه6.4 Five-minute pre-interview checklist

  1. تعریفِ تراکنش و چهار حرفِ ACID + اینکه C یعنی invariantهای برنامه و «abortability» نامِ بهترِ A است.
  2. شش آنومالی را نام ببر و بگو هر کدام در کدام سطحِ isolation ممکن است (جدولِ زیرِ سوالات).
  3. MVCC را در سه جمله توضیح بده: update = delete+insert، زنجیرهٔ نسخه‌ها، قواعدِ visibility.
  4. چهار راهکارِ lost update + اینکه کدام دیتابیس‌ها خودشان تشخیص می‌دهند.
  5. write skew را با مثالِ پزشکان تعریف کن و بگو چرا قفلِ شیء کمک نمی‌کند.
  6. سه روشِ serializability + یک مزیت و یک بهای هرکدام.
  7. 2PC را در شش گام تعریف کن و دو نقطهٔ بازگشت‌ناپذیر را مشخص کن؛ 2PL ≠ 2PC.
  8. مسالهٔ in-doubt و گرهِ XA را در دو جمله، و جایگزینِ idempotency برای exactly-once را در یک جمله.
  1. Define transactions and the four ACID letters — C means the app's invariants and “abortability” is the better name for A.
  2. Name the six anomalies and say at which isolation level each is possible (table below the questions).
  3. Explain MVCC in three sentences: update = delete+insert, version chains, visibility rules.
  4. The four lost-update remedies + which databases detect it themselves.
  5. Define write skew with the doctors example and say why object locks don't help.
  6. The three serializability methods + one benefit and one price each.
  7. Define 2PC in six steps and mark the two points of no return; 2PL ≠ 2PC.
  8. The in-doubt problem and the XA knot in two sentences, and the idempotency alternative for exactly-once in one.

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