Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ১১/১৯অ্যাডভান্সড১ ঘণ্টা

Isolation Levels ও Anomalies — কতটা 'একা' দরকার

Isolation Levels and Anomalies

Dirty read, phantom, write skew — প্রতিটা anomaly-র দুই-session নাটক, চার-ধাপ ladder কোথায় কী আটকায়, PostgreSQL আর MySQL কেন একই নামে ভিন্ন গল্প বলে, আর SERIALIZABLE-এর SSI জাদু।

এই লেসন শেষে আপনি পারবেন

  • পাঁচটা প্রধান anomaly — dirty read, non-repeatable read, phantom, lost update, write skew — প্রতিটার দুই-session timeline আঁকতে আর বাস্তব workload-এ চিনতে পারবেন
  • SQL-এর চার-স্তরের ladder-এ কোন level কোন anomaly আটকায় matrix ভরাট করে বলতে পারবেন, আর standard-এর ফাঁকগুলোও ধরতে পারবেন
  • PostgreSQL dirty read দেখায় না, তার REPEATABLE READ lock নয় snapshot, MySQL default কেন ভিন্ন — standard বনাম implementation-এর এই ফারাকগুলো ব্যাখ্যা করতে পারবেন
  • Snapshot timing — statement-per-snapshot বনাম transaction-per-snapshot — কীভাবে একই নামে ভিন্ন আচরণ বানায়, দুই-session trace-এ প্রমাণ করতে পারবেন
  • Business-tolerance অনুযায়ী isolation level বাছাইয়ের ছক বানাতে পারবেন, আর ScheduleValidator দিয়ে interleaving-এ anomaly ও serializability program-এ যাচাই করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

পরশ লেসনে transaction-এর সীমানা শিখলাম; একটা প্রশ্ন ঝুলে ছিল: চলন্ত transaction একে অন্যকে কী দেখবে? Ideal উত্তর — “যেন সে একা”। কিন্তু নিখুঁত একাকীত্বের দাম সবকিছু serial-চালানো — throughput- মৃত্যু। আসল প্রশ্ন: application কতটা ‘একা’-ভাব সহ্য করতে পারে, আর সীমা ছাড়ালে কোন ভুল উত্তরটা ঘুরে আসবে?

সেই ভুল উত্তরদের নাম আছে — dirty read, non-repeatable read, phantom, lost update, write skew; প্রত্যেকটা reproducible নাটক: দুই actor, কয়েক statement, এক নীরব ভুল। আজ পাঁচটাই মঞ্চে, আর চার-ধাপ ladder — READ UNCOMMITTED থেকে SERIALIZABLE — কোথায় কী থামায়, চোখে দেখবেন।

সাথে বড় সতর্কতা: standard আর বাস্তব database একই নামে ভিন্ন গল্প বলে। PostgreSQL dirty read অসম্ভব, আর তার REPEATABLE READ lock- জগৎ নয় — snapshot। ফারাক না জানলে interview আর production — দুই জায়গায়ই বিপদ।

মূল ধারণা

Anomaly catalog — পাঁচটা নাটক (প্রতিটায় দুই session, সময় উপর-থেকে-নিচ)

১. Dirty read — অর্ধেক-সম্পন্ন কাজ দেখা

সময়   Session A                              Session B
────  ─────────────────────────────────     ────────────────────────────────
t1    BEGIN;
t2    UPDATE acc SET bal = bal - 1000
      WHERE id='alice';
t3                                           SELECT bal ... -- 700! (আসলে 1700)
t4    ROLLBACK;

B অসম্পূর্ণ, পরে-বাতিল মান পড়ল — t3-ভিত্তিক সিদ্ধান্ত দাঁড়াল এমন “ঘটনার” উপর যার অস্তিত্বই ছিল না।

২. Non-repeatable read — একই row, দুই উত্তর

t1    BEGIN;
t2    SELECT bal WHERE id='alice';   -- 1700
t3                                           UPDATE bal=700; COMMIT;
t4    SELECT bal WHERE id='alice';   -- 700 !!

একই tx-এ একই row দুইবার, উত্তর দুই রকম — report-এর শুরু-শেষ মেলে না।

৩. Phantom — নতুন ghost-row ঢুকে পড়া

t1    BEGIN;
t2    SELECT count(*) WHERE dest='DAC';   -- 12
t3                                           INSERT ('DAC',...); COMMIT;
t4    SELECT count(*) WHERE dest='DAC';   -- 13 !!

Non-repeatable-এর সেতুবন্ধ রূপ: নতুন row দলে ঢুকেছে — আগে-নেই- এমন row-এর lock নেওয়া যায় না, তাই row-locking অসহায়।

৪. Lost update — দুইজনের লেখার একটা বাষ্প

t0                                                          counter = 10
t1    SELECT x -- 10                  SELECT x -- 10         10
t2    UPDATE x = 10+1                 UPDATE x = 10+1        11 ??
t3    COMMIT                          COMMIT                 11 ??

দুইজনই ১০ পড়ে ১১ লিখল — একটা increment হারাল। Read-modify-write- এর ফাঁকটাই খুনি — race-condition-এর database-প্রতিচ্ছবি।

৫. Write skew — দুই সঠিক সিদ্ধান্তের ভাঙা যৌথ ফল

নিয়ম: অন্তত একজন ডাক্তার on-call থাকবেন। Alice আর Bob — দুইজনই on-call:

t1    BEGIN;                                BEGIN;
t2    on_call >= 1? -- হ্যাঁ                on_call >= 1? -- হ্যাঁ
t3    self.off_duty();                      self.off_duty();
t4    COMMIT;                               COMMIT;   -- on_call = ০!

প্রত্যেকে সঠিক তথ্য দেখে সিদ্ধান্ত নিল — তবু invariant মরল। Lost update-চেয়ে সূক্ষ্ম: কেউ একই row লেখেনি, ভাঙছে cross-row predicate। (Read skew = তার পাঠ-সংস্করণ: পড়ার মাঝে transfer ঢুকলে reader এমন ছবি দেখে যা কোনো মুহূর্তে বাস্তবেই ছিল না。)

SQL ladder — কোন ধাপে কী আটকায়

LevelDirtyNon-repPhantomLost updWrite skew
READ UNCOMMITTEDসম্ভবসম্ভবসম্ভবসম্ভবসম্ভব
READ COMMITTEDআটকায়সম্ভবসম্ভবসম্ভব*সম্ভব
REPEATABLE READআটকায়আটকায়সম্ভব*আটকায়*সম্ভব
SERIALIZABLEআটকায়আটকায়আটকায়আটকায়আটকায়

তারকা-চিহ্নগুলোই আজকের কেন্দ্রীয় সূক্ষ্মতা — standard বলছে এক, বাস্তব engine অন্য।

আরেকটা বড় ফারাক: PG-র REPEATABLE READ lock-based নয়, snapshot- based — tx-জন্মের ছবি থেকে পড়ে। তাই standard-এর “সম্ভব*” phantom-ও PG-র RR-এ আটকে যায় — নতুন row এলেও পুরনো ছবিতে জায়গা নেই। Ladder হলো ন্যূনতম চুক্তি। Default-বিতর্কও এখানেই: MySQL (InnoDB) default REPEATABLE READ, PG READ COMMITTED — InnoDB-র gap-lock-সহ ঐতিহাসিক পছন্দ, PG-র RC দ্রুত contention-কম OLTP-পছন্দ; কোনটা “সঠিক” নেই — দুই trade-off-এর নাম।

Snapshot timing — এক নামের দুই আচরণ

RC snapshot নেয় প্রতি statement-এর শুরুতে (fresh data — non-repeatable সম্ভব); RR/SER নেয় প্রথম statement-এ একবার (এক ছবি — repeatable, phantom-free)। নাম মুখস্থ যথেষ্ট নয় — mechanism জানা লাগে।

SERIALIZABLE এক পর্দায় — SSI টিজার

SERIALIZABLE-এর প্রতিশ্রুতি: concurrent execution-এর ফল কোনো- না-কোনো এক-এক-করে-চালানো order-এর সমান। পুরনো উপায় lock (2PL — পরের লেসন); PG-র আধুনিক উপায় SSI: সবাই snapshot থেকে পড়ে, কিন্তু engine খেয়াল রাখে কে পড়া data-র ভিত্তিতে লিখছে (rw-dependency) — বৃত্তের আশঙ্কায় একজনকে abort:

ERROR: could not serialize access due to read/write dependencies
HINT:  The transaction might succeed if retried.

Write-skew নাটকে ঠিক এই error আসবে; retry-তে দ্বিতীয়জন নতুন বাস্তবতা দেখে সিদ্ধান্ত বদলাবে।

কোন level কখন — সিদ্ধান্ত-ছক

Businessউদাহরণযথেষ্ট level
Counter/feed/cachelikes, view-countRC — হারানো increment কেউ টের পায় না
Money movementtransfer, ledgerRR + SELECT FOR UPDATE — per-row নিশ্চয়তা
Cross-row invarianton-call ruleSERIALIZABLE — predicate-ভাঙা আটকাতে snapshot নয়
Reportingend-of-day reportRR — এক consistent ছবি, লেখার ঝামেলা নেই

ভেতরে কী ঘটছে

Snapshot কী — তিনটা তথ্যের packet

snapshot = { horizon: এর নিচের সব tx শেষ, running: এখন চলমান id-রা,
             my_xid : নিজের id — নিজের লেখা আমি সবসময় দেখি }

Visibility (সরলীকৃত): version দৃশ্যমান যদি creator (xmin) committed ও horizon-নিচে, এবং deleter (xmax) committed-but-invisible-to-me না হয়। এক function-ই isolation-এর ইঞ্জিন — RC প্রতি statement-এ snapshot, RR/SER একবারই; পূর্ণ যন্ত্র MVCC লেসনে।

SSI-র এক পর্দা

Write skew ঘটে কারণ দুই tx ভিন্ন row লেখে — snapshot-জগতে সংঘর্ষই নেই। SSI read-ও track করে (SIREAD); rw-antidependency-র বৃত্ত-আশঙ্কায় engine একজনকে abort করে। False positive আছে (retry-ই ঔষধ), তবু lock-চেয়ে-বেশি concurrency টিকে থাকে।

উদাহরণ

একটা interleaving — RC ভাঙে, RR বাঁচে

Sum-report + মাঝপথে-ঢুকে-পড়া transfer (alice=1000, bob=1000):

সময়   Reporter (Session A)                  Transferer (Session B)
────  ─────────────────────────────────    ──────────────────────────
t1    BEGIN;                                BEGIN;
      [RC] snap#1 {a:1000,b:1000}           [RR] snap {a:1000,b:1000}
t2    SELECT alice → 1000
t3                                          UPDATE alice: 1000→600;
t4                                          UPDATE bob: 1000→1400;
t5                                          COMMIT;
t6    SELECT bob → ???

t6-এর উত্তরই সব কথার সার:

  • RC: statement-শুরুর নতুন snapshot — bob = 1400; total 2400 — এমন মুহূর্ত কখনো ছিলই না। Non-repeatable/read-skew ঘটল।
  • RR: t1-এর ছবি জমে — bob = 1000; total 2000 — t1-এর consistent ছবি।

একই statements, দুই উত্তর — snapshot-timing-এর গল্প এই এক trace-এ। Reporter লিখতেও চাইলে? RC-তে B-র change নীরবে অন্তর্ভুক্ত; PG-র RR-এ already-committed row-তে লিখতে গিয়ে A পাবে could not serialize access due to concurrent update — retry-ই পথ।

ছবিতে

Anomaly × Level matrix আর snapshot-এর ছন্দ

                 dirty  non-rep  phantom  lost-upd  write-skew
READ UNCOMMITTED   ✗       ✗        ✗        ✗         ✗      ← PG-তে অস্তিত্বহীন
READ COMMITTED     ✔       ✗        ✗        ✗         ✗      ← MySQL/PG ভিন্ন default
REPEATABLE READ    ✔       ✔        ✔*       ✔         ✗      ← *PG: snapshot আটকায়!
SERIALIZABLE       ✔       ✔        ✔        ✔         ✔      ← SSI বা 2PL

Snapshot: RC = stmt1[s1] stmt2[s2] ...   RR = stmt1[s1] stmt2[s1] ...
Ladder-map — standard-এর দাবি (কালো) বনাম PostgreSQL-র বাস্তবতা (সবুজ)। PG নিচতলা আর মাঝতলায় standard-চেয়ে বেশি দেয়; SERIALIZABLE-ই পূর্ণ প্রতিশ্রুতি।

Matrix-টা ভরাট-করার জিনিস — পরের সেকশনে প্রতিটা ঘর নিজেই ভরবেন।

নিজে চালিয়ে দেখুন

EXPERIMENT

চারটা নাটক মঞ্চে — দুই psql terminal-এ anomaly hunting

PostgreSQL 14+ (psql)· ২৫ মিনিট

Setup: docker run --rm -d --name pg-lab -e POSTGRES_PASSWORD=lab -p 5432:5432 postgres:16; দুই terminal-এ docker exec -it pg-lab psql -U postgres; Terminal A:

CREATE TABLE accounts (id text PRIMARY KEY, bal numeric);
INSERT INTO accounts VALUES ('alice', 1000), ('bob', 1000);
CREATE TABLE doctors (name text PRIMARY KEY, on_call boolean);
INSERT INTO doctors VALUES ('alice', true), ('bob', true);
CREATE TABLE flights (dest text);
INSERT INTO flights SELECT 'DAC' FROM generate_series(1,12);

নাটক ১ — dirty read অসম্ভব (যেকোনো level-এ):

Terminal A                                 Terminal B
─────────────────────────────────────     ──────────────────────
BEGIN ISOLATION LEVEL READ UNCOMMITTED;
UPDATE accounts SET bal=99999 WHERE id='alice';
                                          BEGIN ISOLATION LEVEL READ UNCOMMITTED;
                                          SELECT bal FROM accounts
                                          WHERE id='alice';  -- 1000!
ROLLBACK;                                  ROLLBACK;   -- PG RC-তে নামিয়েছিল

নাটক ২ — non-repeatable: RC-তে ঘটে, RR-এ না:

Terminal A (RC)                            Terminal B
─────────────────────────────────────     ──────────────────────
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT bal WHERE id='alice';   -- 1000
                                          UPDATE accounts SET bal=600
                                          WHERE id='alice'; COMMIT;
SELECT bal WHERE id='alice';   -- 600 !!

ROLLBACK;  BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT bal WHERE id='alice';   -- 600
                                          UPDATE accounts SET bal=300
                                          WHERE id='alice'; COMMIT;
SELECT bal WHERE id='alice';   -- 600 !  এক ছবি
ROLLBACK;

নাটক ৩ — phantom, আর PG-র অস্বাভাবিক জবাব:

Terminal A (RR)                            Terminal B
─────────────────────────────────────     ──────────────────────
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM flights WHERE dest='DAC';  -- 12
                                          INSERT INTO flights VALUES('DAC');
                                          COMMIT;
SELECT count(*) FROM flights
WHERE dest='DAC';              -- 12 !  classic-এ 13 দেখাত
ROLLBACK;

Standard: RR-এ phantom সম্ভব — PG-র snapshot: না।

নাটক ৪ — write skew: RR হারে, SERIALIZABLE জেতে:

Terminal A (RR)              Terminal B (RR)
────────────────────────    ──────────────────
BEGIN ... REPEATABLE READ;  BEGIN ... REPEATABLE READ;
SELECT count(*) FROM        SELECT count(*) FROM
  doctors WHERE on_call;      doctors WHERE on_call;
  -- 2 ≥ 1: নিরাপদ            -- 2 ≥ 1: নিরাপদ
UPDATE doctors SET          UPDATE doctors SET
  on_call=false               on_call=false
  WHERE name='alice';         WHERE name='bob';
COMMIT;                     COMMIT;   -- দুটোই সফল → on_call = 0!

দুই terminal-এই BEGIN ISOLATION LEVEL SERIALIZABLE দিয়ে আবার চালান — একজনের COMMIT ব্যর্থ:

ERROR:  could not serialize access due to read/write dependencies
        among transactions
HINT:   The transaction might succeed if retried.

Retry করুন — দ্বিতীয়জন নতুন ছবি (on_call=1) দেখে সিদ্ধান্ত বদলাবে।

শেষ ধাপ: চার নাটক × তিন level চালিয়ে ৪×৩ ছক নিজের হাতে ভরাট করুন; concept-এর matrix-এর সাথে মিলান — মিলবে, আর মিলানোর পরে সেটা আর মুখস্থ-জিনিস থাকবে না।

এটা কী প্রমাণ করে

Dirty read PG-তে অসম্ভব, non-repeatable read RC-তে ঘটে RR-এ ঘটে না, phantom PG-র RR-এ snapshot-এ আটকায়, আর write skew শুধু SERIALIZABLE ধরে — matrix-এর ঘরগুলো নিজের চোখে ভরাট।

নিজে বানান

BUILD IT

ScheduleValidator — interleaving log পড়ে anomaly ধরা আর serializability বিচার

Python · ●●●○○
  1. Operation log parse করুন: r1(x), w2(y), q1(DAC), c1 জাতীয় notation
  2. Committed-set track করে dirty read detect করুন
  3. Per-tx history-তে non-repeatable আর lost-update pattern-match
  4. Predicate query (q op) দিয়ে phantom detect করুন
  5. Serialization graph + cycle-check দিয়ে conflict-serializable রায় দিন

লক্ষ্য: anomaly detection + conflict-serializability test — নিজের হাতে।

from collections import defaultdict
from itertools import permutations

def parse(log):
    return [(s, int(t[1]), t[0],
             t[t.index('(')+1:-1] if '(' in t else None)
            for s, t in enumerate(log)]

def anomalies(hist):
    """hist: [(step, tx, kind, item)] -> পাওয়া anomaly-র তালিকা"""
    found, commits = [], set()
    seen_read, seen_write = defaultdict(list), defaultdict(list)
    for step, tx, kind, item in hist:
        if kind == 'c': commits.add(tx); continue
        if kind == 'r':
            ws = [w for w in seen_write[item] if w[1] < step]
            if ws and ws[-1][0] not in commits:
                found.append(f"DIRTY READ : T{tx} পড়ল '{item}' "
                             f"T{ws[-1][0]}-এর uncommitted write")
            seen_read[item].append((tx, step))
        elif kind == 'w':
            seen_write[item].append((tx, step))
        elif kind == 'q':              # predicate scan -> phantom
            for s2, tx2, k2, it2 in hist[:step]:
                if tx2 != tx and it2 == item and k2 == 'i':
                    found.append(f"PHANTOM    : T{tx}-র scan-এ "
                                 f"T{tx2}-র নতুন row")
    # non-repeatable: r_i(x) ... w_j(x) ... r_i(x)
    for item, rs in seen_read.items():
        for i in range(len(rs)):
            for j in range(i+1, len(rs)):
                t1, s1 = rs[i]; t2, s2 = rs[j]
                if t1 != t2: continue
                mid = [wt for wt, ws_ in seen_write[item]
                         if t1 != wt and s1 < ws_ < s2]
                if mid:
                    found.append(f"NON-REPEATABLE: T{t1} '{item}' "
                                 f"দুইবার ({s1},{s2}) — মাঝে T{mid[-1]}")
    # lost update: দুই tx দুজনেই পড়ে দুজনেই লিখেছে একই item
    for item, rs in seen_read.items():
        rw = {t for t, _ in rs} & {t for t, _ in seen_write[item]}
        for a, b in permutations(sorted(rw), 2):
            if b > a:
                found.append(f"LOST UPDATE: T{a}, T{b} দুজনেই "
                             f"'{item}' পড়ে লিখেছে")
    return found

def serialization_graph(hist):
    """Conflict edges (ww, wr/rw): একই item, ভিন্ন tx"""
    edges = defaultdict(set)
    writes, reads = defaultdict(list), defaultdict(list)
    for step, tx, kind, item in hist:
        (writes if kind == 'w' else reads)[item].append(tx)
    for item, ws in writes.items():
        for tx1 in ws:
            for tx2 in ws + reads.get(item, []):
                if tx1 != tx2: edges[tx1].add(tx2)
    return edges

def has_cycle(edges):
    WHITE, GRAY, BLACK = 0, 1, 2
    color = defaultdict(int)
    def dfs(u, path):
        color[u] = GRAY
        for v in edges[u]:
            if color[v] == GRAY: return path + [v]
            if color[v] == WHITE:
                c = dfs(v, path + [v])
                if c: return c
        color[u] = BLACK
        return None
    nodes = set(edges) | {v for vs in edges.values() for v in vs}
    for n in sorted(nodes):
        if color[n] == WHITE:
            c = dfs(n, [n])
            if c: return c
    return None

def validate(log):
    hist = parse(log)
    print(f"History: {' '.join(log)}")
    found = anomalies(hist)
    for f in found: print("  ⚠", f)
    if not found: print("  ✔ কোনো চেনা anomaly নেই")
    cyc = has_cycle(serialization_graph(hist))
    if cyc:
        print(f"  ✗ Conflict-SERIALIZABLE নয় — cycle: "
              f"{' -> '.join('T%d'%t for t in cyc)}")
    else:
        print("  ✔ Conflict-serializable (acyclic graph)")

validate(["r1(x)", "r2(x)", "w1(x)", "w2(x)", "c1", "c2"])   # lost update
validate(["w1(x)", "r2(x)", "c2", "a1"])                     # dirty read
validate(["r1(x)", "w2(x)", "c2", "r1(x)", "c1"])            # non-repeatable
validate(["r1(y)", "w1(x)", "w2(x)", "w2(y)", "c2", "c1"])   # SI-ফাঁক

প্রত্যাশিত output:

History: r1(x) r2(x) w1(x) w2(x) c1 c2
  ⚠ LOST UPDATE: T1, T2 দুজনেই 'x' পড়ে লিখেছে
  ✗ Conflict-SERIALIZABLE নয় — cycle: T1 -> T2 -> T1
History: w1(x) r2(x) c2 a1
  ⚠ DIRTY READ : T2 পড়ল 'x' T1-এর uncommitted write
  ✔ Conflict-serializable (acyclic graph)
History: r1(x) w2(x) c2 r1(x) c1
  ⚠ NON-REPEATABLE: T1 'x' দুইবার (0,3) — মাঝে T2
  ✗ Conflict-SERIALIZABLE নয় — cycle: T1 -> T2 -> T1
History: r1(y) w1(x) w2(x) w2(y) c2 c1
  ✔ কোনো চেনা anomaly নেই
  ✗ Conflict-SERIALIZABLE নয় — cycle: T1 -> T2 -> T1

শেষ উদাহরণই শিক্ষণীয়: কোনো anomaly নেই, তবু serializable নয় — anomaly আর non-serializability এক নয়; snapshot isolation ঠিক এই ফাঁকে বাস করে।

নিজে বাড়ান:

  1. Abort-aware graph: a1-এর পরে T1-এর সব edge বাদ দিন
  2. Topological order ছাপান — equivalent serial order
  3. Write-skew detector: ভিন্ন item লিখলেও উভয়ে একই predicate পড়েছে কি না চেক

বাস্তব সিস্টেমে

Lost update-এর production-রূপ: inventory oversell। Flash-sale-এ দুই request stock=1 দেখে দুই order। ঔষধ level নয়, statement-shape: SET stock = stock - 1 WHERE stock > 0 — atomic, RC-তেই সঠিক।

Banking-এর রেসিপি। Ledger-জাতীয় সিস্টেমে RR + FOR UPDATE — snapshot-গতি + lock-নিশ্চয়তা, SERIALIZABLE-latency ছাড়াই।

Distributed SQL-এর উল্টো culture। CockroachDB/YugabyteDB default প্রায় SERIALIZABLE — multi-node-এ anomaly-debug ব্যয়বহুল; scale বদলালে isolation-culture-ও বদলায়।

Jepsen-এর শিক্ষা। Audit-গুলোতে বারবার: marketing বলে “serializable”, implementation weaker guarantee দেয়। Anomaly চেনা-reproduce করাই সেই audit-দক্ষতা — Jepsen-ভাষা এখন আপনার হাতে।

যে ভুলগুলো সবাই করে

“Higher isolation সবসময় ভালো — SERIALIZABLE-ই ব্যবহার করা উচিত।”

(১) retry-loop বাধ্যতামূলক — abort normal flow; (২) long query-র সাথে conflict-rate স্ফীত; (৩) SSI false-positive-এ healthy tx-ও মরে। Per-row correctness-যথেষ্ট workload-এ RC + সঠিক statement অনেক সস্তা — Level বাছেন tolerance দিয়ে, ভয় দিয়ে নয়।

“REPEATABLE READ সব জায়গায় একই আচরণ করে।”

নাম standard-এর, আচরণ implementation-এর: PG-র RR snapshot-based, write-conflict-এ error; classic lock-based RR phantom দেখায়; InnoDB- র RR gap-lock করে — আবার ভিন্ন। Migration-এ “একই নাম”-ই বড় ফাঁদ।

“Phantom আর non-repeatable read একই জিনিস।”

Non-repeatable-এ একই row বদলেছে — row-lock-সমাধেয়; phantom-এ নতুন row সেটে ঢুকেছে — আগে-নেই-এমন row-এর lock অসম্ভব, তাই সমাধান ভিন্ন: gap/predicate lock (InnoDB) বা snapshot (PG)।

“My transaction শুরু হলেই snapshot নেয় — BEGIN-এই।”

PG-তে RR/SER snapshot নেয় প্রথম statement-এ, BEGIN-এ নয়। BEGIN করে ৩০ সেকেন্ড পরে প্রথম query চালালে সেই মুহূর্তের ছবি পাবেন — BEGIN-এর মুহূর্তের নয়।

বুঝেছেন কি না দেখুন

1

পাঁচটা anomaly-র নাম আর প্রতিটার এক-লাইনের signature বলুন।

স্মরণ

Dirty read — uncommitted লেখা পড়া; non-repeatable — একই row দুই উত্তর; phantom — predicate-query-তে নতুন row; lost update — দুই read-modify-write-এ এক write বাষ্প; write skew — ভিন্ন row-এর দুই valid লেখার যৌথ ফল invariant-ভাঙা।

2

PostgreSQL-এ READ UNCOMMITTED চাইলেও dirty read কেন অসম্ভব?

যুক্তি

Storage-ই MVCC — reader committed-version filter-এর পেছনে; uncommitted tuple দেখার path নেই, তাই engine নীরবে RC-তে নেমে যায়। Guarantee language নয় — storage-design-এর উপহার।

3

Example-এর interleaving-এ RC-reporter total 2400 দেখল — এমন মুহূর্ত কখনো ছিলই না। RR-reporter কী দেখবে, কেন?

প্রয়োগ

2000 — t1-এর এক snapshot পুরো tx জুড়ে (transfer অদৃশ্য)। “এক মুহূর্তের সত্যি ছবি” — যা একসাথে পড়া সম্ভব ছিল, আর RR তা-ই দিল।

4

Write skew-এ কেউ একই row লেখেও না — row-conflict detection কেন ব্যর্থ, SSI কী নেয়?

যুক্তি

Row-conflict = একই tuple-এ দুই write — এখানে নেই; ভাঙছে cross-row predicate। SSI read-ও track করে (SIREAD), rw-antidependency-র বৃত্ত খোঁজে — “পড়ে-লেখা” pattern conflict-এর নতুন প্রজাতি।

5

BEGIN; করে ৩০ সেকেন্ড পরে প্রথম SELECT (RR) — snapshot কোন মুহূর্তের? কেন?

প্রয়োগ

SELECT-এর মুহূর্তের — PG-র snapshot জন্ম হয় প্রথম statement-এ, BEGIN- এ নয়; ৩০ সেকেন্ডের commit ছবিতে আছে। First-statement-ই জন্ম-মুহূর্ত।

6

Flash-sale: stock=1, দুই concurrent order। Oversell ঠেকাতে level বাড়াবেন নাকি statement-shape?

ডিজাইন

Statement-shape: UPDATE ... SET n = n-1 WHERE id=? AND n > 0, affected-count দেখে reject — RC-তেই সঠিক, microsecond lock-window, retry-machinery লাগে না। SERIALIZABLE-এও ঠিক থাকত, কিন্তু abort/retry + false-positive-এর খরচ এই one-line সমাধানের চেয়ে দামি। Level শেষ অস্ত্র, প্রথম নয়।

এরপর কী

এরপর কী

Snapshot দিল পড়ার স্বাধীনতা; কিন্তু লেখার সংঘর্ষ যেখানে snapshot আটকায় না — write skew — সেখানে engine-এর অস্ত্র: lock ও তার শৃঙ্খলা-নীতি।

পরের লেসনে — Locking ও Two-Phase Locking:

  • S/X matrix আর intention lock-এর hierarchy; 2PL-এর growing/shrinking phase কেন conflict-serializability দেয়, strict 2PL cascade আটকায়
  • Deadlock — wait-for graph, victim selection, PG-র ১-সেকেন্ড detector
  • Granularity: row/page/table আর escalation

আজ modern পথ (SSI); পরের লেসনে classical পথ — lock-এর deadlock-খেলা।

আরও পড়ুন

  • A Critique of the ANSI SQL Isolation Levels (SIGMOD 1995) — Hal Berenson, Phil Bernstein, Jim Gray, et al. · Anomaly-গুলোর সুনির্দিষ্ট সংজ্ঞা আর standard-এর ফাঁক ধরা পড়ার canonical paper — এই লেসনের taxonomy-র উৎস
  • Database System Concepts — Chapter 15: Concurrency Control — Abraham Silberschatz, Henry F. Korth, S. Sudarshan · Serializability, conflict-equivalence আর concurrency-control theory-র পাঠ্যপুস্তক ভিত্তি
  • PostgreSQL Documentation — Chapter 13: Concurrency Control (Table 13.1) — The PostgreSQL Global Development Group · PG-র প্রতিটা level-এ কী হয় — এই লেসনের matrix-এর প্রামাণ্য সংস্করণ